Merge tests/exiftool_parser/ from platform/packages/apps/Gallery2 to tests/exiftool_parser/
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..cf5b767
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,56 @@
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13
+LOCAL_STATIC_JAVA_LIBRARIES += com.android.gallery3d.common2
+LOCAL_STATIC_JAVA_LIBRARIES += xmp_toolkit
+LOCAL_STATIC_JAVA_LIBRARIES += mp4parser
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v8-renderscript
+
+LOCAL_RENDERSCRIPT_TARGET_API := 18
+LOCAL_RENDERSCRIPT_COMPATIBILITY := 18
+LOCAL_RENDERSCRIPT_FLAGS := -rs-package-name=android.support.v8.renderscript
+
+# Keep track of previously compiled RS files too (from bundled GalleryGoogle).
+prev_compiled_rs_files := $(call all-renderscript-files-under, src)
+
+# We already have these files from GalleryGoogle, so don't install them.
+LOCAL_RENDERSCRIPT_SKIP_INSTALL := $(prev_compiled_rs_files)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src) $(prev_compiled_rs_files)
+LOCAL_SRC_FILES += $(call all-java-files-under, src_pd)
+
+LOCAL_RESOURCE_DIR += $(LOCAL_PATH)/res
+
+LOCAL_AAPT_FLAGS := --auto-add-overlay
+
+LOCAL_PACKAGE_NAME := Gallery2
+
+LOCAL_OVERRIDES_PACKAGES := Gallery Gallery3D GalleryNew3D
+
+LOCAL_SDK_VERSION := current
+
+# If this is an unbundled build (to install seprately) then include
+# the libraries in the APK, otherwise just put them in /system/lib and
+# leave them out of the APK
+ifneq (,$(TARGET_BUILD_APPS))
+  LOCAL_JNI_SHARED_LIBRARIES := libjni_eglfence libjni_filtershow_filters librsjni libjni_jpegstream
+else
+  LOCAL_REQUIRED_MODULES := libjni_eglfence libjni_filtershow_filters libjni_jpegstream
+endif
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under, jni)
+
+ifeq ($(strip $(LOCAL_PACKAGE_OVERRIDES)),)
+
+# Use the following include to make gallery test apk
+include $(call all-makefiles-under, $(LOCAL_PATH))
+
+endif
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..ef1d914
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,409 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest android:versionCode="40030"
+        android:versionName="1.1.40030"
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.gallery3d">
+
+    <original-package android:name="com.android.gallery3d" />
+
+    <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" />
+
+    <permission android:name="com.android.gallery3d.permission.GALLERY_PROVIDER"
+            android:protectionLevel="signatureOrSystem" />
+
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.NFC" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.SET_WALLPAPER" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+    <uses-permission android:name="com.android.gallery3d.permission.GALLERY_PROVIDER" />
+
+    <supports-screens android:smallScreens="false"
+            android:normalScreens="true" android:largeScreens="true"
+            android:anyDensity="true" />
+
+    <application android:icon="@mipmap/ic_launcher_gallery" android:label="@string/app_name"
+            android:name="com.android.gallery3d.app.GalleryAppImpl"
+            android:theme="@style/Theme.Gallery"
+            android:logo="@mipmap/ic_launcher_gallery"
+            android:hardwareAccelerated="true"
+            android:largeHeap="true"
+            android:backupAgent="com.android.camera.CameraBackupAgent"
+            android:restoreAnyVersion="true">
+        <uses-library android:name="com.google.android.media.effects" android:required="false" />
+        <meta-data android:name="com.google.android.backup.api_key"
+                android:value="AEdPqrEAAAAIRIXquXawbz6duuuCIUAZ_YJv1zbFMMcjZ0NoVw" />
+        <activity android:name="com.android.gallery3d.app.MovieActivity"
+                android:label="@string/movie_view_label"
+                android:configChanges="orientation|keyboardHidden|screenSize">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="rtsp" />
+             </intent-filter>
+             <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:scheme="content" />
+                <data android:scheme="file" />
+                <data android:mimeType="video/mpeg4" />
+                <data android:mimeType="video/mp4" />
+                <data android:mimeType="video/3gp" />
+                <data android:mimeType="video/3gpp" />
+                <data android:mimeType="video/3gpp2" />
+                <data android:mimeType="video/webm" />
+                <data android:mimeType="video/avi" />
+                <data android:mimeType="application/sdp" />
+             </intent-filter>
+             <intent-filter>
+                <!-- HTTP live support -->
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:mimeType="audio/x-mpegurl" />
+                <data android:mimeType="audio/mpegurl" />
+                <data android:mimeType="application/vnd.apple.mpegurl" />
+                <data android:mimeType="application/x-mpegurl" />
+             </intent-filter>
+        </activity>
+
+        <activity android:name="com.android.gallery3d.app.Gallery" android:label="@string/app_name"
+                android:configChanges="keyboardHidden|orientation|screenSize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.APP_GALLERY" />
+            </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/*" />
+                <data android:mimeType="video/*" />
+            </intent-filter>
+            <!-- We do NOT support the PICK intent, we add these intent-filter for
+                 backward compatibility. Handle it as GET_CONTENT. -->
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="image/*" />
+                <data android:mimeType="video/*" />
+            </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" />
+                <data android:mimeType="vnd.android.cursor.dir/video" />
+            </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" />
+                <data android:mimeType="vnd.android.cursor.dir/video" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <action android:name="com.android.camera.action.REVIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:scheme="content" />
+                <data android:scheme="file" />
+                <data android:mimeType="image/bmp" />
+                <data android:mimeType="image/jpeg" />
+                <data android:mimeType="image/gif" />
+                <data android:mimeType="image/png" />
+                <data android:mimeType="image/webp" />
+                <data android:mimeType="image/x-ms-bmp" />
+                <data android:mimeType="image/vnd.wap.wbmp" />
+                <data android:mimeType="application/vnd.google.panorama360+jpg" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="com.android.camera.action.REVIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:scheme="content" />
+                <data android:scheme="file" />
+                <data android:mimeType="video/mpeg4" />
+                <data android:mimeType="video/mp4" />
+                <data android:mimeType="video/3gp" />
+                <data android:mimeType="video/3gpp" />
+                <data android:mimeType="video/3gpp2" />
+                <data android:mimeType="application/sdp" />
+            </intent-filter>
+        </activity>
+
+        <!-- we add this activity-alias for shortcut backward compatibility -->
+        <!-- Note: The alias must put after the target activity -->
+        <activity-alias android:name="com.cooliris.media.Gallery"
+                android:targetActivity="com.android.gallery3d.app.Gallery"
+                android:configChanges="keyboardHidden|orientation|screenSize"
+                android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity-alias>
+
+         <!-- This activity receives USB_DEVICE_ATTACHED intents and allows importing
+         media from attached MTP devices, like cameras and camera phones -->
+        <activity android:launchMode="singleInstance"
+            android:taskAffinity="" android:name="com.android.gallery3d.ingest.IngestActivity"
+            android:configChanges="orientation|screenSize"
+            android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+            </intent-filter>
+            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
+                android:resource="@xml/device_filter" />
+        </activity>
+        <service android:name="com.android.gallery3d.ingest.IngestService" />
+
+        <activity android:name="com.android.gallery3d.app.Wallpaper"
+                android:configChanges="keyboardHidden|orientation|screenSize"
+                android:theme="@style/android:Theme.Translucent.NoTitleBar">
+            <intent-filter android:label="@string/camera_setas_wallpaper">
+                <action android:name="android.intent.action.ATTACH_DATA" />
+                <data android:mimeType="image/*" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter android:label="@string/app_name">
+                <action android:name="android.intent.action.SET_WALLPAPER" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="android.wallpaper.preview"
+                    android:resource="@xml/wallpaper_picker_preview" />
+        </activity>
+        <activity android:name="com.android.gallery3d.app.TrimVideo"
+                android:label="@string/trim_label">
+        </activity>
+
+        <permission android:name="com.android.gallery3d.filtershow.permission.READ"
+                    android:protectionLevel="signature" />
+
+        <permission android:name="com.android.gallery3d.filtershow.permission.WRITE"
+                    android:protectionLevel="signature" />
+
+        <provider
+            android:name="com.android.gallery3d.filtershow.provider.SharedImageProvider"
+            android:authorities="com.android.gallery3d.filtershow.provider.SharedImageProvider"
+            android:grantUriPermissions="true"
+            android:readPermission="com.android.gallery3d.filtershow.permission.READ"
+            android:writePermission="com.android.gallery3d.filtershow.permission.WRITE" />
+
+        <service
+                android:name=".filtershow.pipeline.ProcessingService"
+                android:exported="false" />
+
+        <activity
+            android:name="com.android.gallery3d.filtershow.FilterShowActivity"
+            android:label="@string/title_activity_filter_show"
+            android:theme="@style/Theme.FilterShow"
+            android:configChanges="keyboardHidden|orientation|screenSize">
+            <intent-filter>
+                <action android:name="android.intent.action.EDIT" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="image/*" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="action_nextgen_edit" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="image/*" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name="com.android.gallery3d.filtershow.crop.CropActivity"
+            android:label="@string/crop"
+            android:theme="@style/Theme.FilterShow"
+            android:configChanges="keyboardHidden|orientation|screenSize">
+           <intent-filter android:label="@string/crop_label">
+                <action android:name="com.android.camera.action.CROP" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:scheme="content" />
+                <data android:scheme="file" />
+                <data android:scheme="" />
+                <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>
+
+        <uses-library android:name="com.google.android.media.effects"
+                android:required="false" />
+
+        <activity android:name="com.android.gallery3d.settings.GallerySettings"
+                android:theme="@style/Theme.Gallery"
+                android:configChanges="orientation|keyboardHidden|screenSize" />
+
+        <provider android:name="com.android.gallery3d.provider.GalleryProvider"
+                android:syncable="false"
+                android:grantUriPermissions="true"
+                android:exported="true"
+                android:permission="com.android.gallery3d.permission.GALLERY_PROVIDER"
+                android:authorities="com.android.gallery3d.provider" />
+        <provider
+                android:name="com.android.photos.data.PhotoProvider"
+                android:authorities="com.android.gallery3d.photoprovider"
+                android:syncable="false"
+                android:exported="false"/>
+        <activity android:name="com.android.gallery3d.gadget.WidgetClickHandler" />
+        <activity android:name="com.android.gallery3d.app.DialogPicker"
+                android:configChanges="keyboardHidden|orientation|screenSize"
+                android:theme="@style/DialogPickerTheme"/>
+        <activity android:name="com.android.gallery3d.app.AlbumPicker"
+                android:configChanges="keyboardHidden|orientation|screenSize"
+                android:theme="@style/DialogPickerTheme"/>
+        <activity android:name="com.android.gallery3d.gadget.WidgetTypeChooser"
+                android:configChanges="keyboardHidden|orientation|screenSize"
+                android:theme="@style/Theme.Gallery.Dialog"/>
+        <activity android:name="com.android.camera.CameraActivity"
+                android:taskAffinity="com.android.camera.CameraActivity"
+                android:label="@string/camera_label"
+                android:theme="@style/Theme.Camera"
+                android:icon="@mipmap/ic_launcher_camera"
+                android:configChanges="orientation|screenSize|keyboardHidden"
+                android:clearTaskOnLaunch="true"
+                android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+            <intent-filter>
+                <action android:name="android.media.action.IMAGE_CAPTURE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.media.action.STILL_IMAGE_CAMERA" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.keyguard.layout"
+                    android:resource="@layout/keyguard_widget" />
+        </activity>
+
+        <activity android:name="com.android.camera.SecureCameraActivity"
+                android:taskAffinity="com.android.camera.SecureCameraActivity"
+                android:excludeFromRecents="true"
+                android:label="@string/camera_label"
+                android:theme="@style/Theme.Camera"
+                android:icon="@mipmap/ic_launcher_camera"
+                android:configChanges="orientation|screenSize|keyboardHidden"
+                android:clearTaskOnLaunch="true"
+                android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+            <intent-filter>
+                <action android:name="android.media.action.STILL_IMAGE_CAMERA_SECURE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.media.action.IMAGE_CAPTURE_SECURE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.keyguard.layout"
+                    android:resource="@layout/keyguard_widget" />
+        </activity>
+
+        <activity-alias android:icon="@mipmap/ic_launcher_camera"
+                        android:label="@string/camera_label"
+                        android:name="com.android.camera.CameraLauncher"
+                        android:targetActivity="com.android.camera.CameraActivity" >
+            <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-alias>
+
+        <activity-alias android:icon="@mipmap/ic_launcher_camera"
+                        android:label="@string/camera_label"
+                        android:name="com.android.camera.Camera"
+                        android:targetActivity="com.android.camera.CameraActivity" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity-alias>
+
+        <activity-alias android:icon="@mipmap/ic_launcher_video_camera"
+                android:label="@string/video_camera_label"
+                android:name="com.android.camera.VideoCamera"
+                android:targetActivity="com.android.camera.CameraActivity" >
+            <intent-filter>
+                <action android:name="android.media.action.VIDEO_CAMERA" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.media.action.VIDEO_CAPTURE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity-alias>
+
+        <receiver android:name="com.android.gallery3d.gadget.PhotoAppWidgetProvider"
+                android:label="@string/appwidget_title">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider"
+                    android:resource="@xml/widget_info" />
+        </receiver>
+        <receiver android:name="com.android.gallery3d.app.PackagesMonitor">
+            <intent-filter>
+                <action android:name="android.intent.action.PACKAGE_ADDED"/>
+                <action android:name="android.intent.action.PACKAGE_REMOVED"/>
+                <action android:name="android.intent.action.PACKAGE_CHANGED"/>
+                <data android:scheme="package"/>
+            </intent-filter>
+        </receiver>
+        <service android:name="com.android.gallery3d.app.PackagesMonitor$AsyncService"/>
+        <receiver android:name="com.android.camera.CameraButtonIntentReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.CAMERA_BUTTON"/>
+            </intent-filter>
+        </receiver>
+        <receiver android:name="com.android.camera.DisableCameraReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
+        <service android:name="com.android.gallery3d.gadget.WidgetService"
+                android:permission="android.permission.BIND_REMOTEVIEWS"/>
+        <activity android:name="com.android.gallery3d.gadget.WidgetConfigure"
+                android:configChanges="keyboardHidden|orientation|screenSize"
+                android:theme="@style/android:Theme.Translucent.NoTitleBar">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
+            </intent-filter>
+        </activity>
+        <activity android:name="com.android.camera.ProxyLauncher"
+                android:theme="@style/Theme.ProxyLauncher">
+        </activity>
+        <service android:name="com.android.gallery3d.app.BatchService" />
+        <service android:name="com.android.camera.MediaSaveService" />
+    </application>
+</manifest>
diff --git a/CleanSpec.mk b/CleanSpec.mk
new file mode 100644
index 0000000..20db309
--- /dev/null
+++ b/CleanSpec.mk
@@ -0,0 +1,56 @@
+# 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.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list.  These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+#     $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+#     $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list.  E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
+
+# For example:
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/AndroidTests_intermediates)
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/core_intermediates)
+#$(call add-clean-step, find $(OUT_DIR) -type f -name "IGTalkSession*" -print0 | xargs -0 rm -f)
+#$(call add-clean-step, rm -rf $(PRODUCT_OUT)/data/*)
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/APPS/Camera*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Camera*)
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/APPS/Gallery*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Gallery*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Gallery*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Gallery*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Gallery*)
+
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
diff --git a/gallerycommon/Android.mk b/gallerycommon/Android.mk
new file mode 100644
index 0000000..1d34147
--- /dev/null
+++ b/gallerycommon/Android.mk
@@ -0,0 +1,27 @@
+# Copyright 2011, 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.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the com.android.emailcommon static library. At the moment, this includes
+# the emailcommon files themselves plus everything under src/org (apache code).  All of our
+# AIDL files are also compiled into the static library
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := com.android.gallery3d.common2
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 16
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
new file mode 100644
index 0000000..f4de5c9
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.common;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.hardware.Camera;
+import android.os.Build;
+import android.provider.MediaStore.MediaColumns;
+import android.view.View;
+import android.view.WindowManager;
+
+import java.lang.reflect.Field;
+
+public class ApiHelper {
+    public static interface VERSION_CODES {
+        // These value are copied from Build.VERSION_CODES
+        public static final int GINGERBREAD_MR1 = 10;
+        public static final int HONEYCOMB = 11;
+        public static final int HONEYCOMB_MR1 = 12;
+        public static final int HONEYCOMB_MR2 = 13;
+        public static final int ICE_CREAM_SANDWICH = 14;
+        public static final int ICE_CREAM_SANDWICH_MR1 = 15;
+        public static final int JELLY_BEAN = 16;
+        public static final int JELLY_BEAN_MR1 = 17;
+        public static final int JELLY_BEAN_MR2 = 18;
+    }
+
+    public static final boolean AT_LEAST_16 = Build.VERSION.SDK_INT >= 16;
+
+    public static final boolean USE_888_PIXEL_FORMAT =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean ENABLE_PHOTO_EDITOR =
+            Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH;
+
+    public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE =
+            hasField(View.class, "SYSTEM_UI_FLAG_LAYOUT_STABLE");
+
+    public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION =
+            hasField(View.class, "SYSTEM_UI_FLAG_HIDE_NAVIGATION");
+
+    public static final boolean HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT =
+            hasField(MediaColumns.class, "WIDTH");
+
+    public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_FACTORY =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_SET_BEAM_PUSH_URIS =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_SET_DEFALT_BUFFER_SIZE = hasMethod(
+            "android.graphics.SurfaceTexture", "setDefaultBufferSize",
+            int.class, int.class);
+
+    public static final boolean HAS_RELEASE_SURFACE_TEXTURE = hasMethod(
+            "android.graphics.SurfaceTexture", "release");
+
+    public static final boolean HAS_SURFACE_TEXTURE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_MTP =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1;
+
+    public static final boolean HAS_AUTO_FOCUS_MOVE_CALLBACK =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_REMOTE_VIEWS_SERVICE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_INTENT_EXTRA_LOCAL_ONLY =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_SET_SYSTEM_UI_VISIBILITY =
+            hasMethod(View.class, "setSystemUiVisibility", int.class);
+
+    public static final boolean HAS_FACE_DETECTION;
+    static {
+        boolean hasFaceDetection = false;
+        try {
+            Class<?> listenerClass = Class.forName(
+                    "android.hardware.Camera$FaceDetectionListener");
+            hasFaceDetection =
+                    hasMethod(Camera.class, "setFaceDetectionListener", listenerClass) &&
+                    hasMethod(Camera.class, "startFaceDetection") &&
+                    hasMethod(Camera.class, "stopFaceDetection") &&
+                    hasMethod(Camera.Parameters.class, "getMaxNumDetectedFaces");
+        } catch (Throwable t) {
+        }
+        HAS_FACE_DETECTION = hasFaceDetection;
+    }
+
+    public static final boolean HAS_GET_CAMERA_DISABLED =
+            hasMethod(DevicePolicyManager.class, "getCameraDisabled", ComponentName.class);
+
+    public static final boolean HAS_MEDIA_ACTION_SOUND =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_TIME_LAPSE_RECORDING =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_ZOOM_WHEN_RECORDING =
+            Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH;
+
+    public static final boolean HAS_CAMERA_FOCUS_AREA =
+            Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH;
+
+    public static final boolean HAS_CAMERA_METERING_AREA =
+            Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH;
+
+    public static final boolean HAS_MOTION_EVENT_TRANSFORM =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_EFFECTS_RECORDING = false;
+
+    // "Background" filter does not have "context" input port in jelly bean.
+    public static final boolean HAS_EFFECTS_RECORDING_CONTEXT_INPUT =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1;
+
+    public static final boolean HAS_GET_SUPPORTED_VIDEO_SIZE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_SET_ICON_ATTRIBUTE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_MEDIA_PROVIDER_FILES_TABLE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_SURFACE_TEXTURE_RECORDING =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_ACTION_BAR =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    // Ex: View.setTranslationX.
+    public static final boolean HAS_VIEW_TRANSFORM_PROPERTIES =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_CAMERA_HDR =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1;
+
+    public static final boolean HAS_OPTIONS_IN_MUTABLE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean CAN_START_PREVIEW_IN_JPEG_CALLBACK =
+            Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH;
+
+    public static final boolean HAS_VIEW_PROPERTY_ANIMATOR =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1;
+
+    public static final boolean HAS_POST_ON_ANIMATION =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_ANNOUNCE_FOR_ACCESSIBILITY =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_OBJECT_ANIMATION =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_GLES20_REQUIRED =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_ROTATION_ANIMATION =
+            hasField(WindowManager.LayoutParams.class, "rotationAnimation");
+
+    public static final boolean HAS_ORIENTATION_LOCK =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2;
+
+    public static final boolean HAS_CANCELLATION_SIGNAL =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_MEDIA_MUXER =
+                    Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2;
+
+    public static final boolean HAS_DISPLAY_LISTENER =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1;
+
+    public static int getIntFieldIfExists(Class<?> klass, String fieldName,
+            Class<?> obj, int defaultVal) {
+        try {
+            Field f = klass.getDeclaredField(fieldName);
+            return f.getInt(obj);
+        } catch (Exception e) {
+            return defaultVal;
+        }
+    }
+
+    private static boolean hasField(Class<?> klass, String fieldName) {
+        try {
+            klass.getDeclaredField(fieldName);
+            return true;
+        } catch (NoSuchFieldException e) {
+            return false;
+        }
+    }
+
+    private static boolean hasMethod(String className, String methodName,
+            Class<?>... parameterTypes) {
+        try {
+            Class<?> klass = Class.forName(className);
+            klass.getDeclaredMethod(methodName, parameterTypes);
+            return true;
+        } catch (Throwable th) {
+            return false;
+        }
+    }
+
+    private static boolean hasMethod(
+            Class<?> klass, String methodName, Class<?> ... paramTypes) {
+        try {
+            klass.getDeclaredMethod(methodName, paramTypes);
+            return true;
+        } catch (NoSuchMethodException e) {
+            return false;
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/AsyncTaskUtil.java b/gallerycommon/src/com/android/gallery3d/common/AsyncTaskUtil.java
new file mode 100644
index 0000000..b70c4d3
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/AsyncTaskUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.common;
+
+import android.os.AsyncTask;
+import android.os.Build;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.Executor;
+
+/**
+ * Helper class to execute an AsyncTask in parallel if SDK version is 11 or newer.
+ */
+public class AsyncTaskUtil {
+    private static Method sMethodExecuteOnExecutor;
+    private static Executor sExecutor;
+    static {
+        if (Build.VERSION.SDK_INT >= 11) {
+            try {
+                sExecutor = (Executor) AsyncTask.class.getField("THREAD_POOL_EXECUTOR")
+                        .get(null);
+                sMethodExecuteOnExecutor = AsyncTask.class.getMethod(
+                        "executeOnExecutor", Executor.class, Object[].class);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            } catch (NoSuchFieldException e) {
+                throw new RuntimeException(e);
+            } catch (NoSuchMethodException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    public static <Param> void executeInParallel(AsyncTask<Param, ?, ?> task, Param... params) {
+        if (Build.VERSION.SDK_INT < 11) {
+            task.execute(params);
+        } else {
+            try {
+                sMethodExecuteOnExecutor.invoke(task, sExecutor, params);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private AsyncTaskUtil() {
+    }
+}
+
diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
new file mode 100644
index 0000000..a671ed2
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.common;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.os.Build;
+import android.util.FloatMath;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class BitmapUtils {
+    private static final String TAG = "BitmapUtils";
+    private static final int DEFAULT_JPEG_QUALITY = 90;
+    public static final int UNCONSTRAINED = -1;
+
+    private BitmapUtils(){}
+
+    /*
+     * Compute the sample size as a function of minSideLength
+     * and maxNumOfPixels.
+     * minSideLength is used to specify that minimal width or height of a
+     * bitmap.
+     * maxNumOfPixels is used to specify the maximal size in pixels that is
+     * tolerable in terms of memory usage.
+     *
+     * The function returns a sample size based on the constraints.
+     * Both size and minSideLength can be passed in as UNCONSTRAINED,
+     * which indicates no care of the corresponding constraint.
+     * The functions prefers returning a sample size that
+     * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED.
+     *
+     * Also, the function rounds up the sample size to a power of 2 or multiple
+     * of 8 because BitmapFactory only honors sample size this way.
+     * For example, BitmapFactory downsamples an image by 2 even though the
+     * request is 3. So we round up the sample size to avoid OOM.
+     */
+    public static int computeSampleSize(int width, int height,
+            int minSideLength, int maxNumOfPixels) {
+        int initialSize = computeInitialSampleSize(
+                width, height, minSideLength, maxNumOfPixels);
+
+        return initialSize <= 8
+                ? Utils.nextPowerOf2(initialSize)
+                : (initialSize + 7) / 8 * 8;
+    }
+
+    private static int computeInitialSampleSize(int w, int h,
+            int minSideLength, int maxNumOfPixels) {
+        if (maxNumOfPixels == UNCONSTRAINED
+                && minSideLength == UNCONSTRAINED) return 1;
+
+        int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
+                (int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels));
+
+        if (minSideLength == UNCONSTRAINED) {
+            return lowerBound;
+        } else {
+            int sampleSize = Math.min(w / minSideLength, h / minSideLength);
+            return Math.max(sampleSize, lowerBound);
+        }
+    }
+
+    // This computes a sample size which makes the longer side at least
+    // minSideLength long. If that's not possible, return 1.
+    public static int computeSampleSizeLarger(int w, int h,
+            int minSideLength) {
+        int initialSize = Math.max(w / minSideLength, h / minSideLength);
+        if (initialSize <= 1) return 1;
+
+        return initialSize <= 8
+                ? Utils.prevPowerOf2(initialSize)
+                : initialSize / 8 * 8;
+    }
+
+    // Find the min x that 1 / x >= scale
+    public static int computeSampleSizeLarger(float scale) {
+        int initialSize = (int) FloatMath.floor(1f / scale);
+        if (initialSize <= 1) return 1;
+
+        return initialSize <= 8
+                ? Utils.prevPowerOf2(initialSize)
+                : initialSize / 8 * 8;
+    }
+
+    // Find the max x that 1 / x <= scale.
+    public static int computeSampleSize(float scale) {
+        Utils.assertTrue(scale > 0);
+        int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale));
+        return initialSize <= 8
+                ? Utils.nextPowerOf2(initialSize)
+                : (initialSize + 7) / 8 * 8;
+    }
+
+    public static Bitmap resizeBitmapByScale(
+            Bitmap bitmap, float scale, boolean recycle) {
+        int width = Math.round(bitmap.getWidth() * scale);
+        int height = Math.round(bitmap.getHeight() * scale);
+        if (width == bitmap.getWidth()
+                && height == bitmap.getHeight()) return bitmap;
+        Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
+        Canvas canvas = new Canvas(target);
+        canvas.scale(scale, scale);
+        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+        canvas.drawBitmap(bitmap, 0, 0, paint);
+        if (recycle) bitmap.recycle();
+        return target;
+    }
+
+    private static Bitmap.Config getConfig(Bitmap bitmap) {
+        Bitmap.Config config = bitmap.getConfig();
+        if (config == null) {
+            config = Bitmap.Config.ARGB_8888;
+        }
+        return config;
+    }
+
+    public static Bitmap resizeDownBySideLength(
+            Bitmap bitmap, int maxLength, boolean recycle) {
+        int srcWidth = bitmap.getWidth();
+        int srcHeight = bitmap.getHeight();
+        float scale = Math.min(
+                (float) maxLength / srcWidth, (float) maxLength / srcHeight);
+        if (scale >= 1.0f) return bitmap;
+        return resizeBitmapByScale(bitmap, scale, recycle);
+    }
+
+    public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) {
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        if (w == size && h == size) return bitmap;
+
+        // scale the image so that the shorter side equals to the target;
+        // the longer side will be center-cropped.
+        float scale = (float) size / Math.min(w,  h);
+
+        Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
+        int width = Math.round(scale * bitmap.getWidth());
+        int height = Math.round(scale * bitmap.getHeight());
+        Canvas canvas = new Canvas(target);
+        canvas.translate((size - width) / 2f, (size - height) / 2f);
+        canvas.scale(scale, scale);
+        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+        canvas.drawBitmap(bitmap, 0, 0, paint);
+        if (recycle) bitmap.recycle();
+        return target;
+    }
+
+    public static void recycleSilently(Bitmap bitmap) {
+        if (bitmap == null) return;
+        try {
+            bitmap.recycle();
+        } catch (Throwable t) {
+            Log.w(TAG, "unable recycle bitmap", t);
+        }
+    }
+
+    public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) {
+        if (rotation == 0) return source;
+        int w = source.getWidth();
+        int h = source.getHeight();
+        Matrix m = new Matrix();
+        m.postRotate(rotation);
+        Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true);
+        if (recycle) source.recycle();
+        return bitmap;
+    }
+
+    public static Bitmap createVideoThumbnail(String filePath) {
+        // MediaMetadataRetriever is available on API Level 8
+        // but is hidden until API Level 10
+        Class<?> clazz = null;
+        Object instance = null;
+        try {
+            clazz = Class.forName("android.media.MediaMetadataRetriever");
+            instance = clazz.newInstance();
+
+            Method method = clazz.getMethod("setDataSource", String.class);
+            method.invoke(instance, filePath);
+
+            // The method name changes between API Level 9 and 10.
+            if (Build.VERSION.SDK_INT <= 9) {
+                return (Bitmap) clazz.getMethod("captureFrame").invoke(instance);
+            } else {
+                byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance);
+                if (data != null) {
+                    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+                    if (bitmap != null) return bitmap;
+                }
+                return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance);
+            }
+        } catch (IllegalArgumentException ex) {
+            // Assume this is a corrupt video file
+        } catch (RuntimeException ex) {
+            // Assume this is a corrupt video file.
+        } catch (InstantiationException e) {
+            Log.e(TAG, "createVideoThumbnail", e);
+        } catch (InvocationTargetException e) {
+            Log.e(TAG, "createVideoThumbnail", e);
+        } catch (ClassNotFoundException e) {
+            Log.e(TAG, "createVideoThumbnail", e);
+        } catch (NoSuchMethodException e) {
+            Log.e(TAG, "createVideoThumbnail", e);
+        } catch (IllegalAccessException e) {
+            Log.e(TAG, "createVideoThumbnail", e);
+        } finally {
+            try {
+                if (instance != null) {
+                    clazz.getMethod("release").invoke(instance);
+                }
+            } catch (Exception ignored) {
+            }
+        }
+        return null;
+    }
+
+    public static byte[] compressToBytes(Bitmap bitmap) {
+        return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
+    }
+
+    public static byte[] compressToBytes(Bitmap bitmap, int quality) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
+        bitmap.compress(CompressFormat.JPEG, quality, baos);
+        return baos.toByteArray();
+    }
+
+    public static boolean isSupportedByRegionDecoder(String mimeType) {
+        if (mimeType == null) return false;
+        mimeType = mimeType.toLowerCase();
+        return mimeType.startsWith("image/") &&
+                (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp"));
+    }
+
+    public static boolean isRotationSupported(String mimeType) {
+        if (mimeType == null) return false;
+        mimeType = mimeType.toLowerCase();
+        return mimeType.equals("image/jpeg");
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/BlobCache.java b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java
new file mode 100644
index 0000000..3c131e5
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2010 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 an on-disk cache which maps a 64-bits key to a byte array.
+//
+// It consists of three files: one index file and two data files. One of the
+// data files is "active", and the other is "inactive". New entries are
+// appended into the active region until it reaches the size limit. At that
+// point the active file and the inactive file are swapped, and the new active
+// file is truncated to empty (and the index for that file is also cleared).
+// The index is a hash table with linear probing. When the load factor reaches
+// 0.5, it does the same thing like when the size limit is reached.
+//
+// The index file format: (all numbers are stored in little-endian)
+// [0]  Magic number: 0xB3273030
+// [4]  MaxEntries: Max number of hash entries per region.
+// [8]  MaxBytes: Max number of data bytes per region (including header).
+// [12] ActiveRegion: The active growing region: 0 or 1.
+// [16] ActiveEntries: The number of hash entries used in the active region.
+// [20] ActiveBytes: The number of data bytes used in the active region.
+// [24] Version number.
+// [28] Checksum of [0..28).
+// [32] Hash entries for region 0. The size is X = (12 * MaxEntries bytes).
+// [32 + X] Hash entries for region 1. The size is also X.
+//
+// Each hash entry is 12 bytes: 8 bytes key and 4 bytes offset into the data
+// file. The offset is 0 when the slot is free. Note that 0 is a valid value
+// for key. The keys are used directly as index into a hash table, so they
+// should be suitably distributed.
+//
+// Each data file stores data for one region. The data file is concatenated
+// blobs followed by the magic number 0xBD248510.
+//
+// The blob format:
+// [0]  Key of this blob
+// [8]  Checksum of this blob
+// [12] Offset of this blob
+// [16] Length of this blob (not including header)
+// [20] Blob
+//
+// Below are the interface for BlobCache. The instance of this class does not
+// support concurrent use by multiple threads.
+//
+// public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) throws IOException;
+// public void insert(long key, byte[] data) throws IOException;
+// public byte[] lookup(long key) throws IOException;
+// public void lookup(LookupRequest req) throws IOException;
+// public void close();
+// public void syncIndex();
+// public void syncAll();
+// public static void deleteFiles(String path);
+//
+package com.android.gallery3d.common;
+
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteOrder;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.zip.Adler32;
+
+public class BlobCache implements Closeable {
+    private static final String TAG = "BlobCache";
+
+    private static final int MAGIC_INDEX_FILE = 0xB3273030;
+    private static final int MAGIC_DATA_FILE = 0xBD248510;
+
+    // index header offset
+    private static final int IH_MAGIC = 0;
+    private static final int IH_MAX_ENTRIES = 4;
+    private static final int IH_MAX_BYTES = 8;
+    private static final int IH_ACTIVE_REGION = 12;
+    private static final int IH_ACTIVE_ENTRIES = 16;
+    private static final int IH_ACTIVE_BYTES = 20;
+    private static final int IH_VERSION = 24;
+    private static final int IH_CHECKSUM = 28;
+    private static final int INDEX_HEADER_SIZE = 32;
+
+    private static final int DATA_HEADER_SIZE = 4;
+
+    // blob header offset
+    private static final int BH_KEY = 0;
+    private static final int BH_CHECKSUM = 8;
+    private static final int BH_OFFSET = 12;
+    private static final int BH_LENGTH = 16;
+    private static final int BLOB_HEADER_SIZE = 20;
+
+    private RandomAccessFile mIndexFile;
+    private RandomAccessFile mDataFile0;
+    private RandomAccessFile mDataFile1;
+    private FileChannel mIndexChannel;
+    private MappedByteBuffer mIndexBuffer;
+
+    private int mMaxEntries;
+    private int mMaxBytes;
+    private int mActiveRegion;
+    private int mActiveEntries;
+    private int mActiveBytes;
+    private int mVersion;
+
+    private RandomAccessFile mActiveDataFile;
+    private RandomAccessFile mInactiveDataFile;
+    private int mActiveHashStart;
+    private int mInactiveHashStart;
+    private byte[] mIndexHeader = new byte[INDEX_HEADER_SIZE];
+    private byte[] mBlobHeader = new byte[BLOB_HEADER_SIZE];
+    private Adler32 mAdler32 = new Adler32();
+
+    // Creates the cache. Three files will be created:
+    // path + ".idx", path + ".0", and path + ".1"
+    // The ".0" file and the ".1" file each stores data for a region. Each of
+    // them can grow to the size specified by maxBytes. The maxEntries parameter
+    // specifies the maximum number of entries each region can have. If the
+    // "reset" parameter is true, the cache will be cleared before use.
+    public BlobCache(String path, int maxEntries, int maxBytes, boolean reset)
+            throws IOException {
+        this(path, maxEntries, maxBytes, reset, 0);
+    }
+
+    public BlobCache(String path, int maxEntries, int maxBytes, boolean reset,
+            int version) throws IOException {
+        mIndexFile = new RandomAccessFile(path + ".idx", "rw");
+        mDataFile0 = new RandomAccessFile(path + ".0", "rw");
+        mDataFile1 = new RandomAccessFile(path + ".1", "rw");
+        mVersion = version;
+
+        if (!reset && loadIndex()) {
+            return;
+        }
+
+        resetCache(maxEntries, maxBytes);
+
+        if (!loadIndex()) {
+            closeAll();
+            throw new IOException("unable to load index");
+        }
+    }
+
+    // Delete the files associated with the given path previously created
+    // by the BlobCache constructor.
+    public static void deleteFiles(String path) {
+        deleteFileSilently(path + ".idx");
+        deleteFileSilently(path + ".0");
+        deleteFileSilently(path + ".1");
+    }
+
+    private static void deleteFileSilently(String path) {
+        try {
+            new File(path).delete();
+        } catch (Throwable t) {
+            // ignore;
+        }
+    }
+
+    // Close the cache. All resources are released. No other method should be
+    // called after this is called.
+    @Override
+    public void close() {
+        syncAll();
+        closeAll();
+    }
+
+    private void closeAll() {
+        closeSilently(mIndexChannel);
+        closeSilently(mIndexFile);
+        closeSilently(mDataFile0);
+        closeSilently(mDataFile1);
+    }
+
+    // Returns true if loading index is successful. After this method is called,
+    // mIndexHeader and index header in file should be kept sync.
+    private boolean loadIndex() {
+        try {
+            mIndexFile.seek(0);
+            mDataFile0.seek(0);
+            mDataFile1.seek(0);
+
+            byte[] buf = mIndexHeader;
+            if (mIndexFile.read(buf) != INDEX_HEADER_SIZE) {
+                Log.w(TAG, "cannot read header");
+                return false;
+            }
+
+            if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) {
+                Log.w(TAG, "cannot read header magic");
+                return false;
+            }
+
+            if (readInt(buf, IH_VERSION) != mVersion) {
+                Log.w(TAG, "version mismatch");
+                return false;
+            }
+
+            mMaxEntries = readInt(buf, IH_MAX_ENTRIES);
+            mMaxBytes = readInt(buf, IH_MAX_BYTES);
+            mActiveRegion = readInt(buf, IH_ACTIVE_REGION);
+            mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES);
+            mActiveBytes = readInt(buf, IH_ACTIVE_BYTES);
+
+            int sum = readInt(buf, IH_CHECKSUM);
+            if (checkSum(buf, 0, IH_CHECKSUM) != sum) {
+                Log.w(TAG, "header checksum does not match");
+                return false;
+            }
+
+            // Sanity check
+            if (mMaxEntries <= 0) {
+                Log.w(TAG, "invalid max entries");
+                return false;
+            }
+            if (mMaxBytes <= 0) {
+                Log.w(TAG, "invalid max bytes");
+                return false;
+            }
+            if (mActiveRegion != 0 && mActiveRegion != 1) {
+                Log.w(TAG, "invalid active region");
+                return false;
+            }
+            if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) {
+                Log.w(TAG, "invalid active entries");
+                return false;
+            }
+            if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) {
+                Log.w(TAG, "invalid active bytes");
+                return false;
+            }
+            if (mIndexFile.length() !=
+                    INDEX_HEADER_SIZE + mMaxEntries * 12 * 2) {
+                Log.w(TAG, "invalid index file length");
+                return false;
+            }
+
+            // Make sure data file has magic
+            byte[] magic = new byte[4];
+            if (mDataFile0.read(magic) != 4) {
+                Log.w(TAG, "cannot read data file magic");
+                return false;
+            }
+            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
+                Log.w(TAG, "invalid data file magic");
+                return false;
+            }
+            if (mDataFile1.read(magic) != 4) {
+                Log.w(TAG, "cannot read data file magic");
+                return false;
+            }
+            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
+                Log.w(TAG, "invalid data file magic");
+                return false;
+            }
+
+            // Map index file to memory
+            mIndexChannel = mIndexFile.getChannel();
+            mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE,
+                    0, mIndexFile.length());
+            mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+            setActiveVariables();
+            return true;
+        } catch (IOException ex) {
+            Log.e(TAG, "loadIndex failed.", ex);
+            return false;
+        }
+    }
+
+    private void setActiveVariables() throws IOException {
+        mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
+        mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1;
+        mActiveDataFile.setLength(mActiveBytes);
+        mActiveDataFile.seek(mActiveBytes);
+
+        mActiveHashStart = INDEX_HEADER_SIZE;
+        mInactiveHashStart = INDEX_HEADER_SIZE;
+
+        if (mActiveRegion == 0) {
+            mInactiveHashStart += mMaxEntries * 12;
+        } else {
+            mActiveHashStart += mMaxEntries * 12;
+        }
+    }
+
+    private void resetCache(int maxEntries, int maxBytes) throws IOException {
+        mIndexFile.setLength(0);  // truncate to zero the index
+        mIndexFile.setLength(INDEX_HEADER_SIZE + maxEntries * 12 * 2);
+        mIndexFile.seek(0);
+        byte[] buf = mIndexHeader;
+        writeInt(buf, IH_MAGIC, MAGIC_INDEX_FILE);
+        writeInt(buf, IH_MAX_ENTRIES, maxEntries);
+        writeInt(buf, IH_MAX_BYTES, maxBytes);
+        writeInt(buf, IH_ACTIVE_REGION, 0);
+        writeInt(buf, IH_ACTIVE_ENTRIES, 0);
+        writeInt(buf, IH_ACTIVE_BYTES, DATA_HEADER_SIZE);
+        writeInt(buf, IH_VERSION, mVersion);
+        writeInt(buf, IH_CHECKSUM, checkSum(buf, 0, IH_CHECKSUM));
+        mIndexFile.write(buf);
+        // This is only needed if setLength does not zero the extended part.
+        // writeZero(mIndexFile, maxEntries * 12 * 2);
+
+        mDataFile0.setLength(0);
+        mDataFile1.setLength(0);
+        mDataFile0.seek(0);
+        mDataFile1.seek(0);
+        writeInt(buf, 0, MAGIC_DATA_FILE);
+        mDataFile0.write(buf, 0, 4);
+        mDataFile1.write(buf, 0, 4);
+    }
+
+    // Flip the active region and the inactive region.
+    private void flipRegion() throws IOException {
+        mActiveRegion = 1 - mActiveRegion;
+        mActiveEntries = 0;
+        mActiveBytes = DATA_HEADER_SIZE;
+
+        writeInt(mIndexHeader, IH_ACTIVE_REGION, mActiveRegion);
+        writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
+        writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
+        updateIndexHeader();
+
+        setActiveVariables();
+        clearHash(mActiveHashStart);
+        syncIndex();
+    }
+
+    // Sync mIndexHeader to the index file.
+    private void updateIndexHeader() {
+        writeInt(mIndexHeader, IH_CHECKSUM,
+                checkSum(mIndexHeader, 0, IH_CHECKSUM));
+        mIndexBuffer.position(0);
+        mIndexBuffer.put(mIndexHeader);
+    }
+
+    // Clear the hash table starting from the specified offset.
+    private void clearHash(int hashStart) {
+        byte[] zero = new byte[1024];
+        mIndexBuffer.position(hashStart);
+        for (int count = mMaxEntries * 12; count > 0;) {
+            int todo = Math.min(count, 1024);
+            mIndexBuffer.put(zero, 0, todo);
+            count -= todo;
+        }
+    }
+
+    // Inserts a (key, data) pair into the cache.
+    public void insert(long key, byte[] data) throws IOException {
+        if (DATA_HEADER_SIZE + BLOB_HEADER_SIZE + data.length > mMaxBytes) {
+            throw new RuntimeException("blob is too large!");
+        }
+
+        if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes
+                || mActiveEntries * 2 >= mMaxEntries) {
+            flipRegion();
+        }
+
+        if (!lookupInternal(key, mActiveHashStart)) {
+            // If we don't have an existing entry with the same key, increase
+            // the entry count.
+            mActiveEntries++;
+            writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
+        }
+
+        insertInternal(key, data, data.length);
+        updateIndexHeader();
+    }
+
+    public void clearEntry(long key) throws IOException {
+        if (!lookupInternal(key, mActiveHashStart)) {
+            return; // Nothing to clear
+        }
+        byte[] header = mBlobHeader;
+        Arrays.fill(header, (byte) 0);
+        mActiveDataFile.seek(mFileOffset);
+        mActiveDataFile.write(header);
+    }
+
+    // Appends the data to the active file. It also updates the hash entry.
+    // The proper hash entry (suitable for insertion or replacement) must be
+    // pointed by mSlotOffset.
+    private void insertInternal(long key, byte[] data, int length)
+            throws IOException {
+        byte[] header = mBlobHeader;
+        int sum = checkSum(data);
+        writeLong(header, BH_KEY, key);
+        writeInt(header, BH_CHECKSUM, sum);
+        writeInt(header, BH_OFFSET, mActiveBytes);
+        writeInt(header, BH_LENGTH, length);
+        mActiveDataFile.write(header);
+        mActiveDataFile.write(data, 0, length);
+
+        mIndexBuffer.putLong(mSlotOffset, key);
+        mIndexBuffer.putInt(mSlotOffset + 8, mActiveBytes);
+        mActiveBytes += BLOB_HEADER_SIZE + length;
+        writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
+    }
+
+    public static class LookupRequest {
+        public long key;        // input: the key to find
+        public byte[] buffer;   // input/output: the buffer to store the blob
+        public int length;      // output: the length of the blob
+    }
+
+    // This method is for one-off lookup. For repeated lookup, use the version
+    // accepting LookupRequest to avoid repeated memory allocation.
+    private LookupRequest mLookupRequest = new LookupRequest();
+    public byte[] lookup(long key) throws IOException {
+        mLookupRequest.key = key;
+        mLookupRequest.buffer = null;
+        if (lookup(mLookupRequest)) {
+            return mLookupRequest.buffer;
+        } else {
+            return null;
+        }
+    }
+
+    // Returns true if the associated blob for the given key is available.
+    // The blob is stored in the buffer pointed by req.buffer, and the length
+    // is in stored in the req.length variable.
+    //
+    // The user can input a non-null value in req.buffer, and this method will
+    // try to use that buffer. If that buffer is not large enough, this method
+    // will allocate a new buffer and assign it to req.buffer.
+    //
+    // This method tries not to throw IOException even if the data file is
+    // corrupted, but it can still throw IOException if things get strange.
+    public boolean lookup(LookupRequest req) throws IOException {
+        // Look up in the active region first.
+        if (lookupInternal(req.key, mActiveHashStart)) {
+            if (getBlob(mActiveDataFile, mFileOffset, req)) {
+                return true;
+            }
+        }
+
+        // We want to copy the data from the inactive file to the active file
+        // if it's available. So we keep the offset of the hash entry so we can
+        // avoid looking it up again.
+        int insertOffset = mSlotOffset;
+
+        // Look up in the inactive region.
+        if (lookupInternal(req.key, mInactiveHashStart)) {
+            if (getBlob(mInactiveDataFile, mFileOffset, req)) {
+                // If we don't have enough space to insert this blob into
+                // the active file, just return it.
+                if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes
+                    || mActiveEntries * 2 >= mMaxEntries) {
+                    return true;
+                }
+                // Otherwise copy it over.
+                mSlotOffset = insertOffset;
+                try {
+                    insertInternal(req.key, req.buffer, req.length);
+                    mActiveEntries++;
+                    writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
+                    updateIndexHeader();
+                } catch (Throwable t) {
+                    Log.e(TAG, "cannot copy over");
+                }
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+
+    // Copies the blob for the specified offset in the specified file to
+    // req.buffer. If req.buffer is null or too small, allocate a buffer and
+    // assign it to req.buffer.
+    // Returns false if the blob is not available (either the index file is
+    // not sync with the data file, or one of them is corrupted). The length
+    // of the blob is stored in the req.length variable.
+    private boolean getBlob(RandomAccessFile file, int offset,
+            LookupRequest req) throws IOException {
+        byte[] header = mBlobHeader;
+        long oldPosition = file.getFilePointer();
+        try {
+            file.seek(offset);
+            if (file.read(header) != BLOB_HEADER_SIZE) {
+                Log.w(TAG, "cannot read blob header");
+                return false;
+            }
+            long blobKey = readLong(header, BH_KEY);
+            if (blobKey == 0) {
+                return false; // This entry has been cleared.
+            }
+            if (blobKey != req.key) {
+                Log.w(TAG, "blob key does not match: " + blobKey);
+                return false;
+            }
+            int sum = readInt(header, BH_CHECKSUM);
+            int blobOffset = readInt(header, BH_OFFSET);
+            if (blobOffset != offset) {
+                Log.w(TAG, "blob offset does not match: " + blobOffset);
+                return false;
+            }
+            int length = readInt(header, BH_LENGTH);
+            if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) {
+                Log.w(TAG, "invalid blob length: " + length);
+                return false;
+            }
+            if (req.buffer == null || req.buffer.length < length) {
+                req.buffer = new byte[length];
+            }
+
+            byte[] blob = req.buffer;
+            req.length = length;
+
+            if (file.read(blob, 0, length) != length) {
+                Log.w(TAG, "cannot read blob data");
+                return false;
+            }
+            if (checkSum(blob, 0, length) != sum) {
+                Log.w(TAG, "blob checksum does not match: " + sum);
+                return false;
+            }
+            return true;
+        } catch (Throwable t)  {
+            Log.e(TAG, "getBlob failed.", t);
+            return false;
+        } finally {
+            file.seek(oldPosition);
+        }
+    }
+
+    // Tries to look up a key in the specified hash region.
+    // Returns true if the lookup is successful.
+    // The slot offset in the index file is saved in mSlotOffset. If the lookup
+    // is successful, it's the slot found. Otherwise it's the slot suitable for
+    // insertion.
+    // If the lookup is successful, the file offset is also saved in
+    // mFileOffset.
+    private int mSlotOffset;
+    private int mFileOffset;
+    private boolean lookupInternal(long key, int hashStart) {
+        int slot = (int) (key % mMaxEntries);
+        if (slot < 0) slot += mMaxEntries;
+        int slotBegin = slot;
+        while (true) {
+            int offset = hashStart + slot * 12;
+            long candidateKey = mIndexBuffer.getLong(offset);
+            int candidateOffset = mIndexBuffer.getInt(offset + 8);
+            if (candidateOffset == 0) {
+                mSlotOffset = offset;
+                return false;
+            } else if (candidateKey == key) {
+                mSlotOffset = offset;
+                mFileOffset = candidateOffset;
+                return true;
+            } else {
+                if (++slot >= mMaxEntries) {
+                    slot = 0;
+                }
+                if (slot == slotBegin) {
+                    Log.w(TAG, "corrupted index: clear the slot.");
+                    mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0);
+                }
+            }
+        }
+    }
+
+    public void syncIndex() {
+        try {
+            mIndexBuffer.force();
+        } catch (Throwable t) {
+            Log.w(TAG, "sync index failed", t);
+        }
+    }
+
+    public void syncAll() {
+        syncIndex();
+        try {
+            mDataFile0.getFD().sync();
+        } catch (Throwable t) {
+            Log.w(TAG, "sync data file 0 failed", t);
+        }
+        try {
+            mDataFile1.getFD().sync();
+        } catch (Throwable t) {
+            Log.w(TAG, "sync data file 1 failed", t);
+        }
+    }
+
+    // This is for testing only.
+    //
+    // Returns the active count (mActiveEntries). This also verifies that
+    // the active count matches matches what's inside the hash region.
+    int getActiveCount() {
+        int count = 0;
+        for (int i = 0; i < mMaxEntries; i++) {
+            int offset = mActiveHashStart + i * 12;
+            long candidateKey = mIndexBuffer.getLong(offset);
+            int candidateOffset = mIndexBuffer.getInt(offset + 8);
+            if (candidateOffset != 0) ++count;
+        }
+        if (count == mActiveEntries) {
+            return count;
+        } else {
+            Log.e(TAG, "wrong active count: " + mActiveEntries + " vs " + count);
+            return -1;  // signal failure.
+        }
+    }
+
+    int checkSum(byte[] data) {
+        mAdler32.reset();
+        mAdler32.update(data);
+        return (int) mAdler32.getValue();
+    }
+
+    int checkSum(byte[] data, int offset, int nbytes) {
+        mAdler32.reset();
+        mAdler32.update(data, offset, nbytes);
+        return (int) mAdler32.getValue();
+    }
+
+    static void closeSilently(Closeable c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (Throwable t) {
+            // do nothing
+        }
+    }
+
+    static int readInt(byte[] buf, int offset) {
+        return (buf[offset] & 0xff)
+                | ((buf[offset + 1] & 0xff) << 8)
+                | ((buf[offset + 2] & 0xff) << 16)
+                | ((buf[offset + 3] & 0xff) << 24);
+    }
+
+    static long readLong(byte[] buf, int offset) {
+        long result = buf[offset + 7] & 0xff;
+        for (int i = 6; i >= 0; i--) {
+            result = (result << 8) | (buf[offset + i] & 0xff);
+        }
+        return result;
+    }
+
+    static void writeInt(byte[] buf, int offset, int value) {
+        for (int i = 0; i < 4; i++) {
+            buf[offset + i] = (byte) (value & 0xff);
+            value >>= 8;
+        }
+    }
+
+    static void writeLong(byte[] buf, int offset, long value) {
+        for (int i = 0; i < 8; i++) {
+            buf[offset + i] = (byte) (value & 0xff);
+            value >>= 8;
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Entry.java b/gallerycommon/src/com/android/gallery3d/common/Entry.java
new file mode 100644
index 0000000..3f1644e
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Entry.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.common;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+public abstract class Entry {
+    public static final String[] ID_PROJECTION = { "_id" };
+
+    public static interface Columns {
+        public static final String ID = "_id";
+    }
+
+    // The primary key of the entry.
+    @Column("_id")
+    public long id = 0;
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.TYPE)
+    public @interface Table {
+        String value();
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public @interface Column {
+        String value();
+
+        boolean indexed() default false;
+
+        boolean fullText() default false;
+
+        String defaultValue() default "";
+
+        boolean unique() default false;
+    }
+
+    public void clear() {
+        id = 0;
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
new file mode 100644
index 0000000..7bf7e43
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.common;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
+public final class EntrySchema {
+    @SuppressWarnings("unused")
+    private static final String TAG = "EntrySchema";
+
+    public static final int TYPE_STRING = 0;
+    public static final int TYPE_BOOLEAN = 1;
+    public static final int TYPE_SHORT = 2;
+    public static final int TYPE_INT = 3;
+    public static final int TYPE_LONG = 4;
+    public static final int TYPE_FLOAT = 5;
+    public static final int TYPE_DOUBLE = 6;
+    public static final int TYPE_BLOB = 7;
+    private static final String SQLITE_TYPES[] = {
+            "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" };
+
+    private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext";
+
+    private final String mTableName;
+    private final ColumnInfo[] mColumnInfo;
+    private final String[] mProjection;
+    private final boolean mHasFullTextIndex;
+
+    public EntrySchema(Class<? extends Entry> clazz) {
+        // Get table and column metadata from reflection.
+        ColumnInfo[] columns = parseColumnInfo(clazz);
+        mTableName = parseTableName(clazz);
+        mColumnInfo = columns;
+
+        // Cache the list of projection columns and check for full-text columns.
+        String[] projection = {};
+        boolean hasFullTextIndex = false;
+        if (columns != null) {
+            projection = new String[columns.length];
+            for (int i = 0; i != columns.length; ++i) {
+                ColumnInfo column = columns[i];
+                projection[i] = column.name;
+                if (column.fullText) {
+                    hasFullTextIndex = true;
+                }
+            }
+        }
+        mProjection = projection;
+        mHasFullTextIndex = hasFullTextIndex;
+    }
+
+    public String getTableName() {
+        return mTableName;
+    }
+
+    public ColumnInfo[] getColumnInfo() {
+        return mColumnInfo;
+    }
+
+    public String[] getProjection() {
+        return mProjection;
+    }
+
+    public int getColumnIndex(String columnName) {
+        for (ColumnInfo column : mColumnInfo) {
+            if (column.name.equals(columnName)) {
+                return column.projectionIndex;
+            }
+        }
+        return -1;
+    }
+
+    public ColumnInfo getColumn(String columnName) {
+        int index = getColumnIndex(columnName);
+        return (index < 0) ? null : mColumnInfo[index];
+    }
+
+    private void logExecSql(SQLiteDatabase db, String sql) {
+        db.execSQL(sql);
+    }
+
+    public <T extends Entry> T cursorToObject(Cursor cursor, T object) {
+        try {
+            for (ColumnInfo column : mColumnInfo) {
+                int columnIndex = column.projectionIndex;
+                Field field = column.field;
+                switch (column.type) {
+                case TYPE_STRING:
+                    field.set(object, cursor.isNull(columnIndex)
+                            ? null
+                            : cursor.getString(columnIndex));
+                    break;
+                case TYPE_BOOLEAN:
+                    field.setBoolean(object, cursor.getShort(columnIndex) == 1);
+                    break;
+                case TYPE_SHORT:
+                    field.setShort(object, cursor.getShort(columnIndex));
+                    break;
+                case TYPE_INT:
+                    field.setInt(object, cursor.getInt(columnIndex));
+                    break;
+                case TYPE_LONG:
+                    field.setLong(object, cursor.getLong(columnIndex));
+                    break;
+                case TYPE_FLOAT:
+                    field.setFloat(object, cursor.getFloat(columnIndex));
+                    break;
+                case TYPE_DOUBLE:
+                    field.setDouble(object, cursor.getDouble(columnIndex));
+                    break;
+                case TYPE_BLOB:
+                    field.set(object, cursor.isNull(columnIndex)
+                            ? null
+                            : cursor.getBlob(columnIndex));
+                    break;
+                }
+            }
+            return object;
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void setIfNotNull(Field field, Object object, Object value)
+            throws IllegalAccessException {
+        if (value != null) field.set(object, value);
+    }
+
+    /**
+     * Converts the ContentValues to the object. The ContentValues may not
+     * contain values for all the fields in the object.
+     */
+    public <T extends Entry> T valuesToObject(ContentValues values, T object) {
+        try {
+            for (ColumnInfo column : mColumnInfo) {
+                String columnName = column.name;
+                Field field = column.field;
+                switch (column.type) {
+                case TYPE_STRING:
+                    setIfNotNull(field, object, values.getAsString(columnName));
+                    break;
+                case TYPE_BOOLEAN:
+                    setIfNotNull(field, object, values.getAsBoolean(columnName));
+                    break;
+                case TYPE_SHORT:
+                    setIfNotNull(field, object, values.getAsShort(columnName));
+                    break;
+                case TYPE_INT:
+                    setIfNotNull(field, object, values.getAsInteger(columnName));
+                    break;
+                case TYPE_LONG:
+                    setIfNotNull(field, object, values.getAsLong(columnName));
+                    break;
+                case TYPE_FLOAT:
+                    setIfNotNull(field, object, values.getAsFloat(columnName));
+                    break;
+                case TYPE_DOUBLE:
+                    setIfNotNull(field, object, values.getAsDouble(columnName));
+                    break;
+                case TYPE_BLOB:
+                    setIfNotNull(field, object, values.getAsByteArray(columnName));
+                    break;
+                }
+            }
+            return object;
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void objectToValues(Entry object, ContentValues values) {
+        try {
+            for (ColumnInfo column : mColumnInfo) {
+                String columnName = column.name;
+                Field field = column.field;
+                switch (column.type) {
+                case TYPE_STRING:
+                    values.put(columnName, (String) field.get(object));
+                    break;
+                case TYPE_BOOLEAN:
+                    values.put(columnName, field.getBoolean(object));
+                    break;
+                case TYPE_SHORT:
+                    values.put(columnName, field.getShort(object));
+                    break;
+                case TYPE_INT:
+                    values.put(columnName, field.getInt(object));
+                    break;
+                case TYPE_LONG:
+                    values.put(columnName, field.getLong(object));
+                    break;
+                case TYPE_FLOAT:
+                    values.put(columnName, field.getFloat(object));
+                    break;
+                case TYPE_DOUBLE:
+                    values.put(columnName, field.getDouble(object));
+                    break;
+                case TYPE_BLOB:
+                    values.put(columnName, (byte[]) field.get(object));
+                    break;
+                }
+            }
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public String toDebugString(Entry entry) {
+        try {
+            StringBuilder sb = new StringBuilder();
+            sb.append("ID=").append(entry.id);
+            for (ColumnInfo column : mColumnInfo) {
+                String columnName = column.name;
+                Field field = column.field;
+                Object value = field.get(entry);
+                sb.append(" ").append(columnName).append("=")
+                        .append((value == null) ? "null" : value.toString());
+            }
+            return sb.toString();
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public String toDebugString(Entry entry, String... columnNames) {
+        try {
+            StringBuilder sb = new StringBuilder();
+            sb.append("ID=").append(entry.id);
+            for (String columnName : columnNames) {
+                ColumnInfo column = getColumn(columnName);
+                Field field = column.field;
+                Object value = field.get(entry);
+                sb.append(" ").append(columnName).append("=")
+                        .append((value == null) ? "null" : value.toString());
+            }
+            return sb.toString();
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Cursor queryAll(SQLiteDatabase db) {
+        return db.query(mTableName, mProjection, null, null, null, null, null);
+    }
+
+    public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) {
+        Cursor cursor = db.query(mTableName, mProjection, "_id=?",
+                new String[] {Long.toString(id)}, null, null, null);
+        boolean success = false;
+        if (cursor.moveToFirst()) {
+            cursorToObject(cursor, entry);
+            success = true;
+        }
+        cursor.close();
+        return success;
+    }
+
+    public long insertOrReplace(SQLiteDatabase db, Entry entry) {
+        ContentValues values = new ContentValues();
+        objectToValues(entry, values);
+        if (entry.id == 0) {
+            values.remove("_id");
+        }
+        long id = db.replace(mTableName, "_id", values);
+        entry.id = id;
+        return id;
+    }
+
+    public boolean deleteWithId(SQLiteDatabase db, long id) {
+        return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1;
+    }
+
+    public void createTables(SQLiteDatabase db) {
+        // Wrapped class must have a @Table.Definition.
+        String tableName = mTableName;
+        Utils.assertTrue(tableName != null);
+
+        // Add the CREATE TABLE statement for the main table.
+        StringBuilder sql = new StringBuilder("CREATE TABLE ");
+        sql.append(tableName);
+        sql.append(" (_id INTEGER PRIMARY KEY AUTOINCREMENT");
+        StringBuilder unique = new StringBuilder();
+        for (ColumnInfo column : mColumnInfo) {
+            if (!column.isId()) {
+                sql.append(',');
+                sql.append(column.name);
+                sql.append(' ');
+                sql.append(SQLITE_TYPES[column.type]);
+                if (!TextUtils.isEmpty(column.defaultValue)) {
+                    sql.append(" DEFAULT ");
+                    sql.append(column.defaultValue);
+                }
+                if (column.unique) {
+                    if (unique.length() == 0) {
+                        unique.append(column.name);
+                    } else {
+                        unique.append(',').append(column.name);
+                    }
+                }
+            }
+        }
+        if (unique.length() > 0) {
+            sql.append(",UNIQUE(").append(unique).append(')');
+        }
+        sql.append(");");
+        logExecSql(db, sql.toString());
+        sql.setLength(0);
+
+        // Create indexes for all indexed columns.
+        for (ColumnInfo column : mColumnInfo) {
+            // Create an index on the indexed columns.
+            if (column.indexed) {
+                sql.append("CREATE INDEX ");
+                sql.append(tableName);
+                sql.append("_index_");
+                sql.append(column.name);
+                sql.append(" ON ");
+                sql.append(tableName);
+                sql.append(" (");
+                sql.append(column.name);
+                sql.append(");");
+                logExecSql(db, sql.toString());
+                sql.setLength(0);
+            }
+        }
+
+        if (mHasFullTextIndex) {
+            // Add an FTS virtual table if using full-text search.
+            String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX;
+            sql.append("CREATE VIRTUAL TABLE ");
+            sql.append(ftsTableName);
+            sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY");
+            for (ColumnInfo column : mColumnInfo) {
+                if (column.fullText) {
+                    // Add the column to the FTS table.
+                    String columnName = column.name;
+                    sql.append(',');
+                    sql.append(columnName);
+                    sql.append(" TEXT");
+                }
+            }
+            sql.append(");");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+
+            // Build an insert statement that will automatically keep the FTS
+            // table in sync.
+            StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO ");
+            insertSql.append(ftsTableName);
+            insertSql.append(" (_id");
+            for (ColumnInfo column : mColumnInfo) {
+                if (column.fullText) {
+                    insertSql.append(',');
+                    insertSql.append(column.name);
+                }
+            }
+            insertSql.append(") VALUES (new._id");
+            for (ColumnInfo column : mColumnInfo) {
+                if (column.fullText) {
+                    insertSql.append(",new.");
+                    insertSql.append(column.name);
+                }
+            }
+            insertSql.append(");");
+            String insertSqlString = insertSql.toString();
+
+            // Add an insert trigger.
+            sql.append("CREATE TRIGGER ");
+            sql.append(tableName);
+            sql.append("_insert_trigger AFTER INSERT ON ");
+            sql.append(tableName);
+            sql.append(" FOR EACH ROW BEGIN ");
+            sql.append(insertSqlString);
+            sql.append("END;");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+
+            // Add an update trigger.
+            sql.append("CREATE TRIGGER ");
+            sql.append(tableName);
+            sql.append("_update_trigger AFTER UPDATE ON ");
+            sql.append(tableName);
+            sql.append(" FOR EACH ROW BEGIN ");
+            sql.append(insertSqlString);
+            sql.append("END;");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+
+            // Add a delete trigger.
+            sql.append("CREATE TRIGGER ");
+            sql.append(tableName);
+            sql.append("_delete_trigger AFTER DELETE ON ");
+            sql.append(tableName);
+            sql.append(" FOR EACH ROW BEGIN DELETE FROM ");
+            sql.append(ftsTableName);
+            sql.append(" WHERE _id = old._id; END;");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+        }
+    }
+
+    public void dropTables(SQLiteDatabase db) {
+        String tableName = mTableName;
+        StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS ");
+        sql.append(tableName);
+        sql.append(';');
+        logExecSql(db, sql.toString());
+        sql.setLength(0);
+
+        if (mHasFullTextIndex) {
+            sql.append("DROP TABLE IF EXISTS ");
+            sql.append(tableName);
+            sql.append(FULL_TEXT_INDEX_SUFFIX);
+            sql.append(';');
+            logExecSql(db, sql.toString());
+        }
+
+    }
+
+    public void deleteAll(SQLiteDatabase db) {
+        StringBuilder sql = new StringBuilder("DELETE FROM ");
+        sql.append(mTableName);
+        sql.append(";");
+        logExecSql(db, sql.toString());
+    }
+
+    private String parseTableName(Class<? extends Object> clazz) {
+        // Check for a table annotation.
+        Entry.Table table = clazz.getAnnotation(Entry.Table.class);
+        if (table == null) {
+            return null;
+        }
+
+        // Return the table name.
+        return table.value();
+    }
+
+    private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) {
+        ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>();
+        while (clazz != null) {
+            parseColumnInfo(clazz, columns);
+            clazz = clazz.getSuperclass();
+        }
+
+        // Return a list.
+        ColumnInfo[] columnList = new ColumnInfo[columns.size()];
+        columns.toArray(columnList);
+        return columnList;
+    }
+
+    private void parseColumnInfo(Class<? extends Object> clazz, ArrayList<ColumnInfo> columns) {
+        // Gather metadata from each annotated field.
+        Field[] fields = clazz.getDeclaredFields(); // including non-public fields
+        for (int i = 0; i != fields.length; ++i) {
+            // Get column metadata from the annotation.
+            Field field = fields[i];
+            Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class);
+            if (info == null) continue;
+
+            // Determine the field type.
+            int type;
+            Class<?> fieldType = field.getType();
+            if (fieldType == String.class) {
+                type = TYPE_STRING;
+            } else if (fieldType == boolean.class) {
+                type = TYPE_BOOLEAN;
+            } else if (fieldType == short.class) {
+                type = TYPE_SHORT;
+            } else if (fieldType == int.class) {
+                type = TYPE_INT;
+            } else if (fieldType == long.class) {
+                type = TYPE_LONG;
+            } else if (fieldType == float.class) {
+                type = TYPE_FLOAT;
+            } else if (fieldType == double.class) {
+                type = TYPE_DOUBLE;
+            } else if (fieldType == byte[].class) {
+                type = TYPE_BLOB;
+            } else {
+                throw new IllegalArgumentException(
+                        "Unsupported field type for column: " + fieldType.getName());
+            }
+
+            // Add the column to the array.
+            int index = columns.size();
+            columns.add(new ColumnInfo(info.value(), type, info.indexed(), info.unique(),
+                    info.fullText(), info.defaultValue(), field, index));
+        }
+    }
+
+    public static final class ColumnInfo {
+        private static final String ID_KEY = "_id";
+
+        public final String name;
+        public final int type;
+        public final boolean indexed;
+        public final boolean unique;
+        public final boolean fullText;
+        public final String defaultValue;
+        public final Field field;
+        public final int projectionIndex;
+
+        public ColumnInfo(String name, int type, boolean indexed, boolean unique,
+                boolean fullText, String defaultValue, Field field, int projectionIndex) {
+            this.name = name.toLowerCase();
+            this.type = type;
+            this.indexed = indexed;
+            this.unique = unique;
+            this.fullText = fullText;
+            this.defaultValue = defaultValue;
+            this.field = field;
+            this.projectionIndex = projectionIndex;
+
+            field.setAccessible(true); // in order to set non-public fields
+        }
+
+        public boolean isId() {
+            return ID_KEY.equals(name);
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/FileCache.java b/gallerycommon/src/com/android/gallery3d/common/FileCache.java
new file mode 100644
index 0000000..d7deda6
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/FileCache.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.common;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import com.android.gallery3d.common.Entry.Table;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+
+public class FileCache implements Closeable {
+    private static final int LRU_CAPACITY = 4;
+    private static final int MAX_DELETE_COUNT = 16;
+
+    private static final String TAG = "FileCache";
+    private static final String TABLE_NAME = FileEntry.SCHEMA.getTableName();
+    private static final String FILE_PREFIX = "download";
+    private static final String FILE_POSTFIX = ".tmp";
+
+    private static final String QUERY_WHERE =
+            FileEntry.Columns.HASH_CODE + "=? AND " + FileEntry.Columns.CONTENT_URL + "=?";
+    private static final String ID_WHERE = FileEntry.Columns.ID + "=?";
+    private static final String[] PROJECTION_SIZE_SUM =
+            {String.format("sum(%s)", FileEntry.Columns.SIZE)};
+    private static final String FREESPACE_PROJECTION[] = {
+            FileEntry.Columns.ID, FileEntry.Columns.FILENAME,
+            FileEntry.Columns.CONTENT_URL, FileEntry.Columns.SIZE};
+    private static final String FREESPACE_ORDER_BY =
+            String.format("%s ASC", FileEntry.Columns.LAST_ACCESS);
+
+    private final LruCache<String, CacheEntry> mEntryMap =
+            new LruCache<String, CacheEntry>(LRU_CAPACITY);
+
+    private File mRootDir;
+    private long mCapacity;
+    private boolean mInitialized = false;
+    private long mTotalBytes;
+
+    private DatabaseHelper mDbHelper;
+
+    public static final class CacheEntry {
+        private long id;
+        public String contentUrl;
+        public File cacheFile;
+
+        private CacheEntry(long id, String contentUrl, File cacheFile) {
+            this.id = id;
+            this.contentUrl = contentUrl;
+            this.cacheFile = cacheFile;
+        }
+    }
+
+    public static void deleteFiles(Context context, File rootDir, String dbName) {
+        try {
+            context.getDatabasePath(dbName).delete();
+            File[] files = rootDir.listFiles();
+            if (files == null) return;
+            for (File file : rootDir.listFiles()) {
+                String name = file.getName();
+                if (file.isFile() && name.startsWith(FILE_PREFIX)
+                        && name.endsWith(FILE_POSTFIX)) file.delete();
+            }
+        } catch (Throwable t) {
+            Log.w(TAG, "cannot reset database", t);
+        }
+    }
+
+    public FileCache(Context context, File rootDir, String dbName, long capacity) {
+        mRootDir = Utils.checkNotNull(rootDir);
+        mCapacity = capacity;
+        mDbHelper = new DatabaseHelper(context, dbName);
+    }
+
+    @Override
+    public void close() {
+        mDbHelper.close();
+    }
+
+    public void store(String downloadUrl, File file) {
+        if (!mInitialized) initialize();
+
+        Utils.assertTrue(file.getParentFile().equals(mRootDir));
+        FileEntry entry = new FileEntry();
+        entry.hashCode = Utils.crc64Long(downloadUrl);
+        entry.contentUrl = downloadUrl;
+        entry.filename = file.getName();
+        entry.size = file.length();
+        entry.lastAccess = System.currentTimeMillis();
+        if (entry.size >= mCapacity) {
+            file.delete();
+            throw new IllegalArgumentException("file too large: " + entry.size);
+        }
+        synchronized (this) {
+            FileEntry original = queryDatabase(downloadUrl);
+            if (original != null) {
+                file.delete();
+                entry.filename = original.filename;
+                entry.size = original.size;
+            } else {
+                mTotalBytes += entry.size;
+            }
+            FileEntry.SCHEMA.insertOrReplace(
+                    mDbHelper.getWritableDatabase(), entry);
+            if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+        }
+    }
+
+    public CacheEntry lookup(String downloadUrl) {
+        if (!mInitialized) initialize();
+        CacheEntry entry;
+        synchronized (mEntryMap) {
+            entry = mEntryMap.get(downloadUrl);
+        }
+
+        if (entry != null) {
+            synchronized (this) {
+                updateLastAccess(entry.id);
+            }
+            return entry;
+        }
+
+        synchronized (this) {
+            FileEntry file = queryDatabase(downloadUrl);
+            if (file == null) return null;
+            entry = new CacheEntry(
+                    file.id, downloadUrl, new File(mRootDir, file.filename));
+            if (!entry.cacheFile.isFile()) { // file has been removed
+                try {
+                    mDbHelper.getWritableDatabase().delete(
+                            TABLE_NAME, ID_WHERE, new String[] {String.valueOf(file.id)});
+                    mTotalBytes -= file.size;
+                } catch (Throwable t) {
+                    Log.w(TAG, "cannot delete entry: " + file.filename, t);
+                }
+                return null;
+            }
+            synchronized (mEntryMap) {
+                mEntryMap.put(downloadUrl, entry);
+            }
+            return entry;
+        }
+    }
+
+    private FileEntry queryDatabase(String downloadUrl) {
+        long hash = Utils.crc64Long(downloadUrl);
+        String whereArgs[] = new String[] {String.valueOf(hash), downloadUrl};
+        Cursor cursor = mDbHelper.getReadableDatabase().query(TABLE_NAME,
+                FileEntry.SCHEMA.getProjection(),
+                QUERY_WHERE, whereArgs, null, null, null);
+        try {
+            if (!cursor.moveToNext()) return null;
+            FileEntry entry = new FileEntry();
+            FileEntry.SCHEMA.cursorToObject(cursor, entry);
+            updateLastAccess(entry.id);
+            return entry;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void updateLastAccess(long id) {
+        ContentValues values = new ContentValues();
+        values.put(FileEntry.Columns.LAST_ACCESS, System.currentTimeMillis());
+        mDbHelper.getWritableDatabase().update(TABLE_NAME,
+                values,  ID_WHERE, new String[] {String.valueOf(id)});
+    }
+
+    public File createFile() throws IOException {
+        return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir);
+    }
+
+    private synchronized void initialize() {
+        if (mInitialized) return;
+
+        if (!mRootDir.isDirectory()) {
+            mRootDir.mkdirs();
+            if (!mRootDir.isDirectory()) {
+                throw new RuntimeException("cannot create: " + mRootDir.getAbsolutePath());
+            }
+        }
+
+        Cursor cursor = mDbHelper.getReadableDatabase().query(
+                TABLE_NAME, PROJECTION_SIZE_SUM,
+                null, null, null, null, null);
+        try {
+            if (cursor.moveToNext()) mTotalBytes = cursor.getLong(0);
+        } finally {
+            cursor.close();
+        }
+        if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+
+        // Mark initialized when everything above went through. If an exception was thrown,
+        // initialize() will be retried later.
+        mInitialized = true;
+    }
+
+    private void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
+        Cursor cursor = mDbHelper.getReadableDatabase().query(
+                TABLE_NAME, FREESPACE_PROJECTION,
+                null, null, null, null, FREESPACE_ORDER_BY);
+        try {
+            while (maxDeleteFileCount > 0
+                    && mTotalBytes > mCapacity && cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+                String path = cursor.getString(1);
+                String url = cursor.getString(2);
+                long size = cursor.getLong(3);
+
+                synchronized (mEntryMap) {
+                    // if some one still uses it
+                    if (mEntryMap.containsKey(url)) continue;
+                }
+
+                --maxDeleteFileCount;
+                if (new File(mRootDir, path).delete()) {
+                    mTotalBytes -= size;
+                    mDbHelper.getWritableDatabase().delete(TABLE_NAME,
+                            ID_WHERE, new String[]{String.valueOf(id)});
+                } else {
+                    Log.w(TAG, "unable to delete file: " + path);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Table("files")
+    private static class FileEntry extends Entry {
+        public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class);
+
+        public interface Columns extends Entry.Columns {
+            public static final String HASH_CODE = "hash_code";
+            public static final String CONTENT_URL = "content_url";
+            public static final String FILENAME = "filename";
+            public static final String SIZE = "size";
+            public static final String LAST_ACCESS = "last_access";
+        }
+
+        @Column(value = Columns.HASH_CODE, indexed = true)
+        public long hashCode;
+
+        @Column(Columns.CONTENT_URL)
+        public String contentUrl;
+
+        @Column(Columns.FILENAME)
+        public String filename;
+
+        @Column(Columns.SIZE)
+        public long size;
+
+        @Column(value = Columns.LAST_ACCESS, indexed = true)
+        public long lastAccess;
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("hash_code: ").append(hashCode).append(", ")
+                    .append("content_url").append(contentUrl).append(", ")
+                    .append("last_access").append(lastAccess).append(", ")
+                    .append("filename").append(filename).toString();
+        }
+    }
+
+    private final class DatabaseHelper extends SQLiteOpenHelper {
+        public static final int DATABASE_VERSION = 1;
+
+        public DatabaseHelper(Context context, String dbName) {
+            super(context, dbName, null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            FileEntry.SCHEMA.createTables(db);
+
+            // delete old files
+            for (File file : mRootDir.listFiles()) {
+                if (!file.delete()) {
+                    Log.w(TAG, "fail to remove: " + file.getAbsolutePath());
+                }
+            }
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            //reset everything
+            FileEntry.SCHEMA.dropTables(db);
+            onCreate(db);
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java
new file mode 100644
index 0000000..2847e57
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.common;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * MD5-based digest Wrapper.
+ */
+public class Fingerprint {
+    // Instance of the MessageDigest using our specified digest algorithm.
+    private static final MessageDigest DIGESTER;
+
+    /**
+     * Name of the digest algorithm we use in {@link java.security.MessageDigest}
+     */
+    private static final String DIGEST_MD5 = "md5";
+
+    // Version 1 streamId prefix.
+    // Hard coded stream id length limit is 40-chars. Don't ask!
+    private static final String STREAM_ID_CS_PREFIX = "cs_01_";
+
+    // 16 bytes for 128-bit fingerprint
+    private static final int FINGERPRINT_BYTE_LENGTH;
+
+    // length of prefix + 32 hex chars for 128-bit fingerprint
+    private static final int STREAM_ID_CS_01_LENGTH;
+
+    static {
+        try {
+            DIGESTER = MessageDigest.getInstance(DIGEST_MD5);
+            FINGERPRINT_BYTE_LENGTH = DIGESTER.getDigestLength();
+            STREAM_ID_CS_01_LENGTH = STREAM_ID_CS_PREFIX.length()
+                    + (FINGERPRINT_BYTE_LENGTH * 2);
+        } catch (NoSuchAlgorithmException e) {
+            // can't continue, but really shouldn't happen
+            throw new IllegalStateException(e);
+        }
+    }
+
+    // md5 digest bytes.
+    private final byte[] mMd5Digest;
+
+    /**
+     * Creates a new Fingerprint.
+     */
+    public Fingerprint(byte[] bytes) {
+        if ((bytes == null) || (bytes.length != FINGERPRINT_BYTE_LENGTH)) {
+            throw new IllegalArgumentException();
+        }
+        mMd5Digest = bytes;
+    }
+
+    /**
+     * Creates a Fingerprint based on the contents of a file.
+     *
+     * Note that this will close() stream after calculating the digest.
+     * @param byteCount length of original data will be stored at byteCount[0] as a side product
+     *        of the fingerprint calculation
+     */
+    public static Fingerprint fromInputStream(InputStream stream, long[] byteCount)
+            throws IOException {
+        DigestInputStream in = null;
+        long count = 0;
+        try {
+            in = new DigestInputStream(stream, DIGESTER);
+            byte[] bytes = new byte[8192];
+            while (true) {
+                // scan through file to compute a fingerprint.
+                int n = in.read(bytes);
+                if (n < 0) break;
+                count += n;
+            }
+        } finally {
+            if (in != null) in.close();
+        }
+        if ((byteCount != null) && (byteCount.length > 0)) byteCount[0] = count;
+        return new Fingerprint(in.getMessageDigest().digest());
+    }
+
+    /**
+     * Decodes a string stream id to a 128-bit fingerprint.
+     */
+    public static Fingerprint fromStreamId(String streamId) {
+        if ((streamId == null)
+                || !streamId.startsWith(STREAM_ID_CS_PREFIX)
+                || (streamId.length() != STREAM_ID_CS_01_LENGTH)) {
+            throw new IllegalArgumentException("bad streamId: " + streamId);
+        }
+
+        // decode the hex bytes of the fingerprint portion
+        byte[] bytes = new byte[FINGERPRINT_BYTE_LENGTH];
+        int byteIdx = 0;
+        for (int idx = STREAM_ID_CS_PREFIX.length(); idx < STREAM_ID_CS_01_LENGTH;
+                idx += 2) {
+            int value = (toDigit(streamId, idx) << 4) | toDigit(streamId, idx + 1);
+            bytes[byteIdx++] = (byte) (value & 0xff);
+        }
+        return new Fingerprint(bytes);
+    }
+
+    /**
+     * Scans a list of strings for a valid streamId.
+     *
+     * @param streamIdList list of stream id's to be scanned
+     * @return valid fingerprint or null if it can't be found
+     */
+    public static Fingerprint extractFingerprint(List<String> streamIdList) {
+        for (String streamId : streamIdList) {
+            if (streamId.startsWith(STREAM_ID_CS_PREFIX)) {
+                return fromStreamId(streamId);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Encodes a 128-bit fingerprint as a string stream id.
+     *
+     * Stream id string is limited to 40 characters, which could be digits, lower case ASCII and
+     * underscores.
+     */
+    public String toStreamId() {
+        StringBuilder streamId = new StringBuilder(STREAM_ID_CS_PREFIX);
+        appendHexFingerprint(streamId, mMd5Digest);
+        return streamId.toString();
+    }
+
+    public byte[] getBytes() {
+        return mMd5Digest;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof Fingerprint)) return false;
+        Fingerprint other = (Fingerprint) obj;
+        return Arrays.equals(mMd5Digest, other.mMd5Digest);
+    }
+
+    public boolean equals(byte[] md5Digest) {
+        return Arrays.equals(mMd5Digest, md5Digest);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(mMd5Digest);
+    }
+
+    // Utility methods.
+
+    private static int toDigit(String streamId, int index) {
+        int digit = Character.digit(streamId.charAt(index), 16);
+        if (digit < 0) {
+            throw new IllegalArgumentException("illegal hex digit in " + streamId);
+        }
+        return digit;
+    }
+
+    private static void appendHexFingerprint(StringBuilder sb, byte[] bytes) {
+        for (int idx = 0; idx < FINGERPRINT_BYTE_LENGTH; idx++) {
+            int value = bytes[idx];
+            sb.append(Integer.toHexString((value >> 4) & 0x0f));
+            sb.append(Integer.toHexString(value& 0x0f));
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java
new file mode 100644
index 0000000..18b7a88
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.common;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+
+import org.apache.http.HttpVersion;
+import org.apache.http.client.HttpClient;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.params.CoreProtocolPNames;
+import org.apache.http.params.HttpParams;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Constructs {@link HttpClient} instances and isolates client code from API
+ * level differences.
+ */
+public final class HttpClientFactory {
+    // TODO: migrate GDataClient to use this util method instead of apache's
+    // DefaultHttpClient.
+    /**
+     * Creates an HttpClient with the userAgent string constructed from the
+     * package name contained in the context.
+     * @return the client
+     */
+    public static HttpClient newHttpClient(Context context) {
+        return HttpClientFactory.newHttpClient(getUserAgent(context));
+    }
+
+    /**
+     * Creates an HttpClient with the specified userAgent string.
+     * @param userAgent the userAgent string
+     * @return the client
+     */
+    public static HttpClient newHttpClient(String userAgent) {
+        // AndroidHttpClient is available on all platform releases,
+        // but is hidden until API Level 8
+        try {
+            Class<?> clazz = Class.forName("android.net.http.AndroidHttpClient");
+            Method newInstance = clazz.getMethod("newInstance", String.class);
+            Object instance = newInstance.invoke(null, userAgent);
+
+            HttpClient client = (HttpClient) instance;
+
+            // ensure we default to HTTP 1.1
+            HttpParams params = client.getParams();
+            params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
+
+            // AndroidHttpClient sets these two parameters thusly by default:
+            // HttpConnectionParams.setSoTimeout(params, 60 * 1000);
+            // HttpConnectionParams.setConnectionTimeout(params, 60 * 1000);
+
+            // however it doesn't set this one...
+            ConnManagerParams.setTimeout(params, 60 * 1000);
+
+            return client;
+        } catch (InvocationTargetException e) {
+            throw new RuntimeException(e);
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        } catch (NoSuchMethodException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Closes an HttpClient.
+     */
+    public static void close(HttpClient client) {
+        // AndroidHttpClient is available on all platform releases,
+        // but is hidden until API Level 8
+        try {
+            Class<?> clazz = client.getClass();
+            Method method = clazz.getMethod("close", (Class<?>[]) null);
+            method.invoke(client, (Object[]) null);
+        } catch (InvocationTargetException e) {
+            throw new RuntimeException(e);
+        } catch (NoSuchMethodException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static String sUserAgent = null;
+
+    private static String getUserAgent(Context context) {
+        if (sUserAgent == null) {
+            PackageInfo pi;
+            try {
+                pi = context.getPackageManager().getPackageInfo(
+                        context.getPackageName(), 0);
+            } catch (NameNotFoundException e) {
+                throw new IllegalStateException("getPackageInfo failed");
+            }
+            sUserAgent = String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s",
+                    pi.packageName,
+                    pi.versionName,
+                    Build.BRAND,
+                    Build.DEVICE,
+                    Build.MODEL,
+                    Build.ID,
+                    Build.VERSION.SDK_INT,
+                    Build.VERSION.RELEASE,
+                    Build.VERSION.INCREMENTAL);
+        }
+        return sUserAgent;
+    }
+
+    private HttpClientFactory() {
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/LruCache.java b/gallerycommon/src/com/android/gallery3d/common/LruCache.java
new file mode 100644
index 0000000..81dabf7
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/LruCache.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.common;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * An LRU cache which stores recently inserted entries and all entries ever
+ * inserted which still has a strong reference elsewhere.
+ */
+public class LruCache<K, V> {
+
+    private final HashMap<K, V> mLruMap;
+    private final HashMap<K, Entry<K, V>> mWeakMap =
+            new HashMap<K, Entry<K, V>>();
+    private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();
+
+    @SuppressWarnings("serial")
+    public LruCache(final int capacity) {
+        mLruMap = new LinkedHashMap<K, V>(16, 0.75f, true) {
+            @Override
+            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+                return size() > capacity;
+            }
+        };
+    }
+
+    private static class Entry<K, V> extends WeakReference<V> {
+        K mKey;
+
+        public Entry(K key, V value, ReferenceQueue<V> queue) {
+            super(value, queue);
+            mKey = key;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void cleanUpWeakMap() {
+        Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
+        while (entry != null) {
+            mWeakMap.remove(entry.mKey);
+            entry = (Entry<K, V>) mQueue.poll();
+        }
+    }
+
+    public synchronized boolean containsKey(K key) {
+        cleanUpWeakMap();
+        return mWeakMap.containsKey(key);
+    }
+
+    public synchronized V put(K key, V value) {
+        cleanUpWeakMap();
+        mLruMap.put(key, value);
+        Entry<K, V> entry = mWeakMap.put(
+                key, new Entry<K, V>(key, value, mQueue));
+        return entry == null ? null : entry.get();
+    }
+
+    public synchronized V get(K key) {
+        cleanUpWeakMap();
+        V value = mLruMap.get(key);
+        if (value != null) return value;
+        Entry<K, V> entry = mWeakMap.get(key);
+        return entry == null ? null : entry.get();
+    }
+
+    public synchronized void clear() {
+        mLruMap.clear();
+        mWeakMap.clear();
+        mQueue = new ReferenceQueue<V>();
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/OverScroller.java b/gallerycommon/src/com/android/gallery3d/common/OverScroller.java
new file mode 100644
index 0000000..1ab7953
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/OverScroller.java
@@ -0,0 +1,958 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.common;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.util.FloatMath;
+import android.util.Log;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+/**
+ * This class encapsulates scrolling with the ability to overshoot the bounds
+ * of a scrolling operation. This class is a drop-in replacement for
+ * {@link android.widget.Scroller} in most cases.
+ */
+public class OverScroller {
+    private int mMode;
+
+    private final SplineOverScroller mScrollerX;
+    private final SplineOverScroller mScrollerY;
+
+    private Interpolator mInterpolator;
+
+    private final boolean mFlywheel;
+
+    private static final int DEFAULT_DURATION = 250;
+    private static final int SCROLL_MODE = 0;
+    private static final int FLING_MODE = 1;
+
+    /**
+     * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
+     * @param context
+     */
+    public OverScroller(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Creates an OverScroller with flywheel enabled.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     */
+    public OverScroller(Context context, Interpolator interpolator) {
+        this(context, interpolator, true);
+    }
+
+    /**
+     * Creates an OverScroller.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+     * @hide
+     */
+    public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
+        mInterpolator = interpolator;
+        mFlywheel = flywheel;
+        mScrollerX = new SplineOverScroller();
+        mScrollerY = new SplineOverScroller();
+
+        SplineOverScroller.initFromContext(context);
+    }
+
+    /**
+     * Creates an OverScroller with flywheel enabled.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+     * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+     * behavior is no longer supported and this coefficient has no effect.
+     * !deprecated Use {!link #OverScroller(Context, Interpolator, boolean)} instead.
+     */
+    public OverScroller(Context context, Interpolator interpolator,
+            float bounceCoefficientX, float bounceCoefficientY) {
+        this(context, interpolator, true);
+    }
+
+    /**
+     * Creates an OverScroller.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+     * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+     * behavior is no longer supported and this coefficient has no effect.
+     * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+     * !deprecated Use {!link OverScroller(Context, Interpolator, boolean)} instead.
+     */
+    public OverScroller(Context context, Interpolator interpolator,
+            float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
+        this(context, interpolator, flywheel);
+    }
+
+    void setInterpolator(Interpolator interpolator) {
+        mInterpolator = interpolator;
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link ViewConfiguration#getScrollFriction}.
+     *
+     * @param friction A scalar dimension-less value representing the coefficient of
+     *         friction.
+     */
+    public final void setFriction(float friction) {
+        mScrollerX.setFriction(friction);
+        mScrollerY.setFriction(friction);
+    }
+
+    /**
+     *
+     * Returns whether the scroller has finished scrolling.
+     *
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mScrollerX.mFinished && mScrollerY.mFinished;
+    }
+
+    /**
+     * Force the finished field to a particular value. Contrary to
+     * {@link #abortAnimation()}, forcing the animation to finished
+     * does NOT cause the scroller to move to the final x and y
+     * position.
+     *
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mScrollerX.mFinished = mScrollerY.mFinished = finished;
+    }
+
+    /**
+     * Returns the current X offset in the scroll.
+     *
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mScrollerX.mCurrentPosition;
+    }
+
+    /**
+     * Returns the current Y offset in the scroll.
+     *
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mScrollerY.mCurrentPosition;
+    }
+
+    /**
+     * Returns the absolute value of the current velocity.
+     *
+     * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
+     */
+    public float getCurrVelocity() {
+        float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
+        squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
+        return FloatMath.sqrt(squaredNorm);
+    }
+
+    /**
+     * Returns the start X offset in the scroll.
+     *
+     * @return The start X offset as an absolute distance from the origin.
+     */
+    public final int getStartX() {
+        return mScrollerX.mStart;
+    }
+
+    /**
+     * Returns the start Y offset in the scroll.
+     *
+     * @return The start Y offset as an absolute distance from the origin.
+     */
+    public final int getStartY() {
+        return mScrollerY.mStart;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public final int getFinalX() {
+        return mScrollerX.mFinal;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public final int getFinalY() {
+        return mScrollerY.mFinal;
+    }
+
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     *
+     * @return The duration of the scroll in milliseconds.
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScrollers don't necessarily have a fixed duration.
+     *             This function will lie to the best of its ability.
+     */
+    @Deprecated
+    public final int getDuration() {
+        return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
+    }
+
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinalX(int)
+     * @see #setFinalY(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScrollers don't necessarily have a fixed duration.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void extendDuration(int extend) {
+        mScrollerX.extendDuration(extend);
+        mScrollerY.extendDuration(extend);
+    }
+
+    /**
+     * Sets the final position (X) for this scroller.
+     *
+     * @param newX The new X offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalY(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScroller's final position may change during an animation.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void setFinalX(int newX) {
+        mScrollerX.setFinalPosition(newX);
+    }
+
+    /**
+     * Sets the final position (Y) for this scroller.
+     *
+     * @param newY The new Y offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalX(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScroller's final position may change during an animation.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void setFinalY(int newY) {
+        mScrollerY.setFinalPosition(newY);
+    }
+
+    /**
+     * Call this when you want to know the new location. If it returns true, the
+     * animation is not yet finished.
+     */
+    public boolean computeScrollOffset() {
+        if (isFinished()) {
+            return false;
+        }
+
+        switch (mMode) {
+            case SCROLL_MODE:
+                long time = AnimationUtils.currentAnimationTimeMillis();
+                // Any scroller can be used for time, since they were started
+                // together in scroll mode. We use X here.
+                final long elapsedTime = time - mScrollerX.mStartTime;
+
+                final int duration = mScrollerX.mDuration;
+                if (elapsedTime < duration) {
+                    float q = (float) (elapsedTime) / duration;
+
+                    if (mInterpolator == null) {
+                        q = Scroller.viscousFluid(q);
+                    } else {
+                        q = mInterpolator.getInterpolation(q);
+                    }
+
+                    mScrollerX.updateScroll(q);
+                    mScrollerY.updateScroll(q);
+                } else {
+                    abortAnimation();
+                }
+                break;
+
+            case FLING_MODE:
+                if (!mScrollerX.mFinished) {
+                    if (!mScrollerX.update()) {
+                        if (!mScrollerX.continueWhenFinished()) {
+                            mScrollerX.finish();
+                        }
+                    }
+                }
+
+                if (!mScrollerY.mFinished) {
+                    if (!mScrollerY.update()) {
+                        if (!mScrollerY.continueWhenFinished()) {
+                            mScrollerY.finish();
+                        }
+                    }
+                }
+
+                break;
+        }
+
+        return true;
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mMode = SCROLL_MODE;
+        mScrollerX.startScroll(startX, dx, duration);
+        mScrollerY.startScroll(startY, dy, duration);
+    }
+
+    /**
+     * Call this when you want to 'spring back' into a valid coordinate range.
+     *
+     * @param startX Starting X coordinate
+     * @param startY Starting Y coordinate
+     * @param minX Minimum valid X value
+     * @param maxX Maximum valid X value
+     * @param minY Minimum valid Y value
+     * @param maxY Minimum valid Y value
+     * @return true if a springback was initiated, false if startX and startY were
+     *          already within the valid range.
+     */
+    public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
+        mMode = FLING_MODE;
+
+        // Make sure both methods are called.
+        final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
+        final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
+        return spingbackX || spingbackY;
+    }
+
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance traveled will
+     * depend on the initial velocity of the fling.
+     *
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *            second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *            second
+     * @param minX Minimum X value. The scroller will not scroll past this point
+     *            unless overX > 0. If overfling is allowed, it will use minX as
+     *            a springback boundary.
+     * @param maxX Maximum X value. The scroller will not scroll past this point
+     *            unless overX > 0. If overfling is allowed, it will use maxX as
+     *            a springback boundary.
+     * @param minY Minimum Y value. The scroller will not scroll past this point
+     *            unless overY > 0. If overfling is allowed, it will use minY as
+     *            a springback boundary.
+     * @param maxY Maximum Y value. The scroller will not scroll past this point
+     *            unless overY > 0. If overfling is allowed, it will use maxY as
+     *            a springback boundary.
+     * @param overX Overfling range. If > 0, horizontal overfling in either
+     *            direction will be possible.
+     * @param overY Overfling range. If > 0, vertical overfling in either
+     *            direction will be possible.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY, int overX, int overY) {
+        // Continue a scroll or fling in progress
+        if (mFlywheel && !isFinished()) {
+            float oldVelocityX = mScrollerX.mCurrVelocity;
+            float oldVelocityY = mScrollerY.mCurrVelocity;
+            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+                velocityX += oldVelocityX;
+                velocityY += oldVelocityY;
+            }
+        }
+
+        mMode = FLING_MODE;
+        mScrollerX.fling(startX, velocityX, minX, maxX, overX);
+        mScrollerY.fling(startY, velocityY, minY, maxY, overY);
+    }
+
+    /**
+     * Notify the scroller that we've reached a horizontal boundary.
+     * Normally the information to handle this will already be known
+     * when the animation is started, such as in a call to one of the
+     * fling functions. However there are cases where this cannot be known
+     * in advance. This function will transition the current motion and
+     * animate from startX to finalX as appropriate.
+     *
+     * @param startX Starting/current X position
+     * @param finalX Desired final X position
+     * @param overX Magnitude of overscroll allowed. This should be the maximum
+     *              desired distance from finalX. Absolute value - must be positive.
+     */
+    public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
+        mScrollerX.notifyEdgeReached(startX, finalX, overX);
+    }
+
+    /**
+     * Notify the scroller that we've reached a vertical boundary.
+     * Normally the information to handle this will already be known
+     * when the animation is started, such as in a call to one of the
+     * fling functions. However there are cases where this cannot be known
+     * in advance. This function will animate a parabolic motion from
+     * startY to finalY.
+     *
+     * @param startY Starting/current Y position
+     * @param finalY Desired final Y position
+     * @param overY Magnitude of overscroll allowed. This should be the maximum
+     *              desired distance from finalY. Absolute value - must be positive.
+     */
+    public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
+        mScrollerY.notifyEdgeReached(startY, finalY, overY);
+    }
+
+    /**
+     * Returns whether the current Scroller is currently returning to a valid position.
+     * Valid bounds were provided by the
+     * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
+     *
+     * One should check this value before calling
+     * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
+     * to restore a valid position will then be stopped. The caller has to take into account
+     * the fact that the started scroll will start from an overscrolled position.
+     *
+     * @return true when the current position is overscrolled and in the process of
+     *         interpolating back to a valid value.
+     */
+    public boolean isOverScrolled() {
+        return ((!mScrollerX.mFinished &&
+                mScrollerX.mState != SplineOverScroller.SPLINE) ||
+                (!mScrollerY.mFinished &&
+                        mScrollerY.mState != SplineOverScroller.SPLINE));
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating causes the scroller to move to the final x and y
+     * positions.
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mScrollerX.finish();
+        mScrollerY.finish();
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     *
+     * @hide
+     */
+    public int timePassed() {
+        final long time = AnimationUtils.currentAnimationTimeMillis();
+        final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
+        return (int) (time - startTime);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isScrollingInDirection(float xvel, float yvel) {
+        final int dx = mScrollerX.mFinal - mScrollerX.mStart;
+        final int dy = mScrollerY.mFinal - mScrollerY.mStart;
+        return !isFinished() && Math.signum(xvel) == Math.signum(dx) &&
+                Math.signum(yvel) == Math.signum(dy);
+    }
+
+    static class SplineOverScroller {
+        // Initial position
+        private int mStart;
+
+        // Current position
+        private int mCurrentPosition;
+
+        // Final position
+        private int mFinal;
+
+        // Initial velocity
+        private int mVelocity;
+
+        // Current velocity
+        private float mCurrVelocity;
+
+        // Constant current deceleration
+        private float mDeceleration;
+
+        // Animation starting time, in system milliseconds
+        private long mStartTime;
+
+        // Animation duration, in milliseconds
+        private int mDuration;
+
+        // Duration to complete spline component of animation
+        private int mSplineDuration;
+
+        // Distance to travel along spline animation
+        private int mSplineDistance;
+
+        // Whether the animation is currently in progress
+        private boolean mFinished;
+
+        // The allowed overshot distance before boundary is reached.
+        private int mOver;
+
+        // Fling friction
+        private float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+        // Current state of the animation.
+        private int mState = SPLINE;
+
+        // Constant gravity value, used in the deceleration phase.
+        private static final float GRAVITY = 2000.0f;
+
+        // A device specific coefficient adjusted to physical values.
+        private static float PHYSICAL_COEF;
+
+        private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+        private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+        private static final float START_TENSION = 0.5f;
+        private static final float END_TENSION = 1.0f;
+        private static final float P1 = START_TENSION * INFLEXION;
+        private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+        private static final int NB_SAMPLES = 100;
+        private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+        private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+        private static final int SPLINE = 0;
+        private static final int CUBIC = 1;
+        private static final int BALLISTIC = 2;
+
+        static {
+            float x_min = 0.0f;
+            float y_min = 0.0f;
+            for (int i = 0; i < NB_SAMPLES; i++) {
+                final float alpha = (float) i / NB_SAMPLES;
+
+                float x_max = 1.0f;
+                float x, tx, coef;
+                while (true) {
+                    x = x_min + (x_max - x_min) / 2.0f;
+                    coef = 3.0f * x * (1.0f - x);
+                    tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+                    if (Math.abs(tx - alpha) < 1E-5) break;
+                    if (tx > alpha) x_max = x;
+                    else x_min = x;
+                }
+                SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+                float y_max = 1.0f;
+                float y, dy;
+                while (true) {
+                    y = y_min + (y_max - y_min) / 2.0f;
+                    coef = 3.0f * y * (1.0f - y);
+                    dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+                    if (Math.abs(dy - alpha) < 1E-5) break;
+                    if (dy > alpha) y_max = y;
+                    else y_min = y;
+                }
+                SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+            }
+            SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+        }
+
+        static void initFromContext(Context context) {
+            final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+            PHYSICAL_COEF = SensorManager.GRAVITY_EARTH // g (m/s^2)
+                    * 39.37f // inch/meter
+                    * ppi
+                    * 0.84f; // look and feel tuning
+        }
+
+        void setFriction(float friction) {
+            mFlingFriction = friction;
+        }
+
+        SplineOverScroller() {
+            mFinished = true;
+        }
+
+        void updateScroll(float q) {
+            mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
+        }
+
+        /*
+         * Get a signed deceleration that will reduce the velocity.
+         */
+        static private float getDeceleration(int velocity) {
+            return velocity > 0 ? -GRAVITY : GRAVITY;
+        }
+
+        /*
+         * Modifies mDuration to the duration it takes to get from start to newFinal using the
+         * spline interpolation. The previous duration was needed to get to oldFinal.
+         */
+        private void adjustDuration(int start, int oldFinal, int newFinal) {
+            final int oldDistance = oldFinal - start;
+            final int newDistance = newFinal - start;
+            final float x = Math.abs((float) newDistance / oldDistance);
+            final int index = (int) (NB_SAMPLES * x);
+            if (index < NB_SAMPLES) {
+                final float x_inf = (float) index / NB_SAMPLES;
+                final float x_sup = (float) (index + 1) / NB_SAMPLES;
+                final float t_inf = SPLINE_TIME[index];
+                final float t_sup = SPLINE_TIME[index + 1];
+                final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
+                mDuration *= timeCoef;
+            }
+        }
+
+        void startScroll(int start, int distance, int duration) {
+            mFinished = false;
+
+            mStart = start;
+            mFinal = start + distance;
+
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mDuration = duration;
+
+            // Unused
+            mDeceleration = 0.0f;
+            mVelocity = 0;
+        }
+
+        void finish() {
+            mCurrentPosition = mFinal;
+            // Not reset since WebView relies on this value for fast fling.
+            // TODO: restore when WebView uses the fast fling implemented in this class.
+            // mCurrVelocity = 0.0f;
+            mFinished = true;
+        }
+
+        void setFinalPosition(int position) {
+            mFinal = position;
+            mFinished = false;
+        }
+
+        void extendDuration(int extend) {
+            final long time = AnimationUtils.currentAnimationTimeMillis();
+            final int elapsedTime = (int) (time - mStartTime);
+            mDuration = elapsedTime + extend;
+            mFinished = false;
+        }
+
+        boolean springback(int start, int min, int max) {
+            mFinished = true;
+
+            mStart = mFinal = start;
+            mVelocity = 0;
+
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mDuration = 0;
+
+            if (start < min) {
+                startSpringback(start, min, 0);
+            } else if (start > max) {
+                startSpringback(start, max, 0);
+            }
+
+            return !mFinished;
+        }
+
+        private void startSpringback(int start, int end, int velocity) {
+            // mStartTime has been set
+            mFinished = false;
+            mState = CUBIC;
+            mStart = start;
+            mFinal = end;
+            final int delta = start - end;
+            mDeceleration = getDeceleration(delta);
+            // TODO take velocity into account
+            mVelocity = -delta; // only sign is used
+            mOver = Math.abs(delta);
+            mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
+        }
+
+        void fling(int start, int velocity, int min, int max, int over) {
+            mOver = over;
+            mFinished = false;
+            mCurrVelocity = mVelocity = velocity;
+            mDuration = mSplineDuration = 0;
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mCurrentPosition = mStart = start;
+
+            if (start > max || start < min) {
+                startAfterEdge(start, min, max, velocity);
+                return;
+            }
+
+            mState = SPLINE;
+            double totalDistance = 0.0;
+
+            if (velocity != 0) {
+                mDuration = mSplineDuration = getSplineFlingDuration(velocity);
+                totalDistance = getSplineFlingDistance(velocity);
+            }
+
+            mSplineDistance = (int) (totalDistance * Math.signum(velocity));
+            mFinal = start + mSplineDistance;
+
+            // Clamp to a valid final position
+            if (mFinal < min) {
+                adjustDuration(mStart, mFinal, min);
+                mFinal = min;
+            }
+
+            if (mFinal > max) {
+                adjustDuration(mStart, mFinal, max);
+                mFinal = max;
+            }
+        }
+
+        private double getSplineDeceleration(int velocity) {
+            return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * PHYSICAL_COEF));
+        }
+
+        private double getSplineFlingDistance(int velocity) {
+            final double l = getSplineDeceleration(velocity);
+            final double decelMinusOne = DECELERATION_RATE - 1.0;
+            return mFlingFriction * PHYSICAL_COEF * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+        }
+
+        /* Returns the duration, expressed in milliseconds */
+        private int getSplineFlingDuration(int velocity) {
+            final double l = getSplineDeceleration(velocity);
+            final double decelMinusOne = DECELERATION_RATE - 1.0;
+            return (int) (1000.0 * Math.exp(l / decelMinusOne));
+        }
+
+        private void fitOnBounceCurve(int start, int end, int velocity) {
+            // Simulate a bounce that started from edge
+            final float durationToApex = - velocity / mDeceleration;
+            final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration);
+            final float distanceToEdge = Math.abs(end - start);
+            final float totalDuration = (float) Math.sqrt(
+                    2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
+            mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
+            mStart = end;
+            mVelocity = (int) (- mDeceleration * totalDuration);
+        }
+
+        private void startBounceAfterEdge(int start, int end, int velocity) {
+            mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
+            fitOnBounceCurve(start, end, velocity);
+            onEdgeReached();
+        }
+
+        private void startAfterEdge(int start, int min, int max, int velocity) {
+            if (start > min && start < max) {
+                Log.e("OverScroller", "startAfterEdge called from a valid position");
+                mFinished = true;
+                return;
+            }
+            final boolean positive = start > max;
+            final int edge = positive ? max : min;
+            final int overDistance = start - edge;
+            boolean keepIncreasing = overDistance * velocity >= 0;
+            if (keepIncreasing) {
+                // Will result in a bounce or a to_boundary depending on velocity.
+                startBounceAfterEdge(start, edge, velocity);
+            } else {
+                final double totalDistance = getSplineFlingDistance(velocity);
+                if (totalDistance > Math.abs(overDistance)) {
+                    fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
+                } else {
+                    startSpringback(start, edge, velocity);
+                }
+            }
+        }
+
+        void notifyEdgeReached(int start, int end, int over) {
+            // mState is used to detect successive notifications
+            if (mState == SPLINE) {
+                mOver = over;
+                mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                // We were in fling/scroll mode before: current velocity is such that distance to
+                // edge is increasing. This ensures that startAfterEdge will not start a new fling.
+                startAfterEdge(start, end, end, (int) mCurrVelocity);
+            }
+        }
+
+        private void onEdgeReached() {
+            // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
+            float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration));
+            final float sign = Math.signum(mVelocity);
+
+            if (distance > mOver) {
+                // Default deceleration is not sufficient to slow us down before boundary
+                 mDeceleration = - sign * mVelocity * mVelocity / (2.0f * mOver);
+                 distance = mOver;
+            }
+
+            mOver = (int) distance;
+            mState = BALLISTIC;
+            mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
+            mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
+        }
+
+        boolean continueWhenFinished() {
+            switch (mState) {
+                case SPLINE:
+                    // Duration from start to null velocity
+                    if (mDuration < mSplineDuration) {
+                        // If the animation was clamped, we reached the edge
+                        mStart = mFinal;
+                        // TODO Better compute speed when edge was reached
+                        mVelocity = (int) mCurrVelocity;
+                        mDeceleration = getDeceleration(mVelocity);
+                        mStartTime += mDuration;
+                        onEdgeReached();
+                    } else {
+                        // Normal stop, no need to continue
+                        return false;
+                    }
+                    break;
+                case BALLISTIC:
+                    mStartTime += mDuration;
+                    startSpringback(mFinal, mStart, 0);
+                    break;
+                case CUBIC:
+                    return false;
+            }
+
+            update();
+            return true;
+        }
+
+        /*
+         * Update the current position and velocity for current time. Returns
+         * true if update has been done and false if animation duration has been
+         * reached.
+         */
+        boolean update() {
+            final long time = AnimationUtils.currentAnimationTimeMillis();
+            final long currentTime = time - mStartTime;
+
+            if (currentTime > mDuration) {
+                return false;
+            }
+
+            double distance = 0.0;
+            switch (mState) {
+                case SPLINE: {
+                    final float t = (float) currentTime / mSplineDuration;
+                    final int index = (int) (NB_SAMPLES * t);
+                    float distanceCoef = 1.f;
+                    float velocityCoef = 0.f;
+                    if (index < NB_SAMPLES) {
+                        final float t_inf = (float) index / NB_SAMPLES;
+                        final float t_sup = (float) (index + 1) / NB_SAMPLES;
+                        final float d_inf = SPLINE_POSITION[index];
+                        final float d_sup = SPLINE_POSITION[index + 1];
+                        velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
+                        distanceCoef = d_inf + (t - t_inf) * velocityCoef;
+                    }
+
+                    distance = distanceCoef * mSplineDistance;
+                    mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
+                    break;
+                }
+
+                case BALLISTIC: {
+                    final float t = currentTime / 1000.0f;
+                    mCurrVelocity = mVelocity + mDeceleration * t;
+                    distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+                    break;
+                }
+
+                case CUBIC: {
+                    final float t = (float) (currentTime) / mDuration;
+                    final float t2 = t * t;
+                    final float sign = Math.signum(mVelocity);
+                    distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
+                    mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
+                    break;
+                }
+            }
+
+            mCurrentPosition = mStart + (int) Math.round(distance);
+
+            return true;
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Scroller.java b/gallerycommon/src/com/android/gallery3d/common/Scroller.java
new file mode 100644
index 0000000..6cefd6f
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Scroller.java
@@ -0,0 +1,507 @@
+/*
+ * 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.gallery3d.common;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.os.Build;
+import android.util.FloatMath;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+
+/**
+ * This class encapsulates scrolling.  The duration of the scroll
+ * can be passed in the constructor and specifies the maximum time that
+ * the scrolling animation should take.  Past this time, the scrolling is
+ * automatically moved to its final stage and computeScrollOffset()
+ * will always return false to indicate that scrolling is over.
+ */
+public class Scroller  {
+    private int mMode;
+
+    private int mStartX;
+    private int mStartY;
+    private int mFinalX;
+    private int mFinalY;
+
+    private int mMinX;
+    private int mMaxX;
+    private int mMinY;
+    private int mMaxY;
+
+    private int mCurrX;
+    private int mCurrY;
+    private long mStartTime;
+    private int mDuration;
+    private float mDurationReciprocal;
+    private float mDeltaX;
+    private float mDeltaY;
+    private boolean mFinished;
+    private Interpolator mInterpolator;
+    private boolean mFlywheel;
+
+    private float mVelocity;
+
+    private static final int DEFAULT_DURATION = 250;
+    private static final int SCROLL_MODE = 0;
+    private static final int FLING_MODE = 1;
+
+    private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9));
+    private static float ALPHA = 800; // pixels / seconds
+    private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance)
+    private static float END_TENSION = 1.0f - START_TENSION;
+    private static final int NB_SAMPLES = 100;
+    private static final float[] SPLINE = new float[NB_SAMPLES + 1];
+
+    private float mDeceleration;
+    private final float mPpi;
+
+    static {
+        float x_min = 0.0f;
+        for (int i = 0; i <= NB_SAMPLES; i++) {
+            final float t = (float) i / NB_SAMPLES;
+            float x_max = 1.0f;
+            float x, tx, coef;
+            while (true) {
+                x = x_min + (x_max - x_min) / 2.0f;
+                coef = 3.0f * x * (1.0f - x);
+                tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x;
+                if (Math.abs(tx - t) < 1E-5) break;
+                if (tx > t) x_max = x;
+                else x_min = x;
+            }
+            final float d = coef + x * x * x;
+            SPLINE[i] = d;
+        }
+        SPLINE[NB_SAMPLES] = 1.0f;
+
+        // This controls the viscous fluid effect (how much of it)
+        sViscousFluidScale = 8.0f;
+        // must be set to 1.0 (used in viscousFluid())
+        sViscousFluidNormalize = 1.0f;
+        sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
+    }
+
+    private static float sViscousFluidScale;
+    private static float sViscousFluidNormalize;
+
+    /**
+     * Create a Scroller with the default duration and interpolator.
+     */
+    public Scroller(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
+     * be in effect for apps targeting Honeycomb or newer.
+     */
+    public Scroller(Context context, Interpolator interpolator) {
+        this(context, interpolator,
+                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. Specify whether or
+     * not to support progressive "flywheel" behavior in flinging.
+     */
+    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
+        mFinished = true;
+        mInterpolator = interpolator;
+        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
+        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
+        mFlywheel = flywheel;
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link ViewConfiguration#getScrollFriction}.
+     *
+     * @param friction A scalar dimension-less value representing the coefficient of
+     *         friction.
+     */
+    public final void setFriction(float friction) {
+        mDeceleration = computeDeceleration(friction);
+    }
+
+    private float computeDeceleration(float friction) {
+        return SensorManager.GRAVITY_EARTH   // g (m/s^2)
+                      * 39.37f               // inch/meter
+                      * mPpi                 // pixels per inch
+                      * friction;
+    }
+
+    /**
+     *
+     * Returns whether the scroller has finished scrolling.
+     *
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mFinished;
+    }
+
+    /**
+     * Force the finished field to a particular value.
+     *
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mFinished = finished;
+    }
+
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     *
+     * @return The duration of the scroll in milliseconds.
+     */
+    public final int getDuration() {
+        return mDuration;
+    }
+
+    /**
+     * Returns the current X offset in the scroll.
+     *
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mCurrX;
+    }
+
+    /**
+     * Returns the current Y offset in the scroll.
+     *
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mCurrY;
+    }
+
+    /**
+     * Returns the current velocity.
+     *
+     * @return The original velocity less the deceleration. Result may be
+     * negative.
+     */
+    public float getCurrVelocity() {
+        return mVelocity - mDeceleration * timePassed() / 2000.0f;
+    }
+
+    /**
+     * Returns the start X offset in the scroll.
+     *
+     * @return The start X offset as an absolute distance from the origin.
+     */
+    public final int getStartX() {
+        return mStartX;
+    }
+
+    /**
+     * Returns the start Y offset in the scroll.
+     *
+     * @return The start Y offset as an absolute distance from the origin.
+     */
+    public final int getStartY() {
+        return mStartY;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public final int getFinalX() {
+        return mFinalX;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public final int getFinalY() {
+        return mFinalY;
+    }
+
+    /**
+     * Call this when you want to know the new location.  If it returns true,
+     * the animation is not yet finished.  loc will be altered to provide the
+     * new location.
+     */
+    public boolean computeScrollOffset() {
+        if (mFinished) {
+            return false;
+        }
+
+        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+
+        if (timePassed < mDuration) {
+            switch (mMode) {
+            case SCROLL_MODE:
+                float x = timePassed * mDurationReciprocal;
+
+                if (mInterpolator == null)
+                    x = viscousFluid(x);
+                else
+                    x = mInterpolator.getInterpolation(x);
+
+                mCurrX = mStartX + Math.round(x * mDeltaX);
+                mCurrY = mStartY + Math.round(x * mDeltaY);
+                break;
+            case FLING_MODE:
+                final float t = (float) timePassed / mDuration;
+                final int index = (int) (NB_SAMPLES * t);
+                final float t_inf = (float) index / NB_SAMPLES;
+                final float t_sup = (float) (index + 1) / NB_SAMPLES;
+                final float d_inf = SPLINE[index];
+                final float d_sup = SPLINE[index + 1];
+                final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);
+
+                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
+                // Pin to mMinX <= mCurrX <= mMaxX
+                mCurrX = Math.min(mCurrX, mMaxX);
+                mCurrX = Math.max(mCurrX, mMinX);
+
+                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
+                // Pin to mMinY <= mCurrY <= mMaxY
+                mCurrY = Math.min(mCurrY, mMaxY);
+                mCurrY = Math.max(mCurrY, mMinY);
+
+                if (mCurrX == mFinalX && mCurrY == mFinalY) {
+                    mFinished = true;
+                }
+
+                break;
+            }
+        }
+        else {
+            mCurrX = mFinalX;
+            mCurrY = mFinalY;
+            mFinished = true;
+        }
+        return true;
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mMode = SCROLL_MODE;
+        mFinished = false;
+        mDuration = duration;
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+        mFinalX = startX + dx;
+        mFinalY = startY + dy;
+        mDeltaX = dx;
+        mDeltaY = dy;
+        mDurationReciprocal = 1.0f / mDuration;
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance travelled will
+     * depend on the initial velocity of the fling.
+     *
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *        second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *        second
+     * @param minX Minimum X value. The scroller will not scroll past this
+     *        point.
+     * @param maxX Maximum X value. The scroller will not scroll past this
+     *        point.
+     * @param minY Minimum Y value. The scroller will not scroll past this
+     *        point.
+     * @param maxY Maximum Y value. The scroller will not scroll past this
+     *        point.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        // Continue a scroll or fling in progress
+        if (mFlywheel && !mFinished) {
+            float oldVel = getCurrVelocity();
+
+            float dx = mFinalX - mStartX;
+            float dy = mFinalY - mStartY;
+            float hyp = FloatMath.sqrt(dx * dx + dy * dy);
+
+            float ndx = dx / hyp;
+            float ndy = dy / hyp;
+
+            float oldVelocityX = ndx * oldVel;
+            float oldVelocityY = ndy * oldVel;
+            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+                velocityX += oldVelocityX;
+                velocityY += oldVelocityY;
+            }
+        }
+
+        mMode = FLING_MODE;
+        mFinished = false;
+
+        float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
+
+        mVelocity = velocity;
+        final double l = Math.log(START_TENSION * velocity / ALPHA);
+        mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0)));
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+
+        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
+        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
+
+        int totalDistance =
+                (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
+
+        mMinX = minX;
+        mMaxX = maxX;
+        mMinY = minY;
+        mMaxY = maxY;
+
+        mFinalX = startX + Math.round(totalDistance * coeffX);
+        // Pin to mMinX <= mFinalX <= mMaxX
+        mFinalX = Math.min(mFinalX, mMaxX);
+        mFinalX = Math.max(mFinalX, mMinX);
+
+        mFinalY = startY + Math.round(totalDistance * coeffY);
+        // Pin to mMinY <= mFinalY <= mMaxY
+        mFinalY = Math.min(mFinalY, mMaxY);
+        mFinalY = Math.max(mFinalY, mMinY);
+    }
+
+    static float viscousFluid(float x)
+    {
+        x *= sViscousFluidScale;
+        if (x < 1.0f) {
+            x -= (1.0f - (float)Math.exp(-x));
+        } else {
+            float start = 0.36787944117f;   // 1/e == exp(-1)
+            x = 1.0f - (float)Math.exp(1.0f - x);
+            x = start + x * (1.0f - start);
+        }
+        x *= sViscousFluidNormalize;
+        return x;
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating cause the scroller to move to the final x and y
+     * position
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mCurrX = mFinalX;
+        mCurrY = mFinalY;
+        mFinished = true;
+    }
+
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinalX(int)
+     * @see #setFinalY(int)
+     */
+    public void extendDuration(int extend) {
+        int passed = timePassed();
+        mDuration = passed + extend;
+        mDurationReciprocal = 1.0f / mDuration;
+        mFinished = false;
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     */
+    public int timePassed() {
+        return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+    }
+
+    /**
+     * Sets the final position (X) for this scroller.
+     *
+     * @param newX The new X offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalY(int)
+     */
+    public void setFinalX(int newX) {
+        mFinalX = newX;
+        mDeltaX = mFinalX - mStartX;
+        mFinished = false;
+    }
+
+    /**
+     * Sets the final position (Y) for this scroller.
+     *
+     * @param newY The new Y offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalX(int)
+     */
+    public void setFinalY(int newY) {
+        mFinalY = newY;
+        mDeltaY = mFinalY - mStartY;
+        mFinished = false;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isScrollingInDirection(float xvel, float yvel) {
+        return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
+                Math.signum(yvel) == Math.signum(mFinalY - mStartY);
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Utils.java b/gallerycommon/src/com/android/gallery3d/common/Utils.java
new file mode 100644
index 0000000..614a081
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Utils.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.common;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+public class Utils {
+    private static final String TAG = "Utils";
+    private static final String DEBUG_TAG = "GalleryDebug";
+
+    private static final long POLY64REV = 0x95AC9329AC4BC9B5L;
+    private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL;
+
+    private static long[] sCrcTable = new long[256];
+
+    private static final boolean IS_DEBUG_BUILD =
+            Build.TYPE.equals("eng") || Build.TYPE.equals("userdebug");
+
+    private static final String MASK_STRING = "********************************";
+
+    // Throws AssertionError if the input is false.
+    public static void assertTrue(boolean cond) {
+        if (!cond) {
+            throw new AssertionError();
+        }
+    }
+
+    // Throws AssertionError with the message. We had a method having the form
+    //   assertTrue(boolean cond, String message, Object ... args);
+    // However a call to that method will cause memory allocation even if the
+    // condition is false (due to autoboxing generated by "Object ... args"),
+    // so we don't use that anymore.
+    public static void fail(String message, Object ... args) {
+        throw new AssertionError(
+                args.length == 0 ? message : String.format(message, args));
+    }
+
+    // Throws NullPointerException if the input is null.
+    public static <T> T checkNotNull(T object) {
+        if (object == null) throw new NullPointerException();
+        return object;
+    }
+
+    // Returns true if two input Object are both null or equal
+    // to each other.
+    public static boolean equals(Object a, Object b) {
+        return (a == b) || (a == null ? false : a.equals(b));
+    }
+
+    // Returns the next power of two.
+    // Returns the input if it is already power of 2.
+    // Throws IllegalArgumentException if the input is <= 0 or
+    // the answer overflows.
+    public static int nextPowerOf2(int n) {
+        if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException("n is invalid: " + n);
+        n -= 1;
+        n |= n >> 16;
+        n |= n >> 8;
+        n |= n >> 4;
+        n |= n >> 2;
+        n |= n >> 1;
+        return n + 1;
+    }
+
+    // Returns the previous power of two.
+    // Returns the input if it is already power of 2.
+    // Throws IllegalArgumentException if the input is <= 0
+    public static int prevPowerOf2(int n) {
+        if (n <= 0) throw new IllegalArgumentException();
+        return Integer.highestOneBit(n);
+    }
+
+    // Returns the input value x clamped to the range [min, max].
+    public static int clamp(int x, int min, int max) {
+        if (x > max) return max;
+        if (x < min) return min;
+        return x;
+    }
+
+    // Returns the input value x clamped to the range [min, max].
+    public static float clamp(float x, float min, float max) {
+        if (x > max) return max;
+        if (x < min) return min;
+        return x;
+    }
+
+    // Returns the input value x clamped to the range [min, max].
+    public static long clamp(long x, long min, long max) {
+        if (x > max) return max;
+        if (x < min) return min;
+        return x;
+    }
+
+    public static boolean isOpaque(int color) {
+        return color >>> 24 == 0xFF;
+    }
+
+    public static void swap(int[] array, int i, int j) {
+        int temp = array[i];
+        array[i] = array[j];
+        array[j] = temp;
+    }
+
+    /**
+     * A function thats returns a 64-bit crc for string
+     *
+     * @param in input string
+     * @return a 64-bit crc value
+     */
+    public static final long crc64Long(String in) {
+        if (in == null || in.length() == 0) {
+            return 0;
+        }
+        return crc64Long(getBytes(in));
+    }
+
+    static {
+        // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c
+        long part;
+        for (int i = 0; i < 256; i++) {
+            part = i;
+            for (int j = 0; j < 8; j++) {
+                long x = ((int) part & 1) != 0 ? POLY64REV : 0;
+                part = (part >> 1) ^ x;
+            }
+            sCrcTable[i] = part;
+        }
+    }
+
+    public static final long crc64Long(byte[] buffer) {
+        long crc = INITIALCRC;
+        for (int k = 0, n = buffer.length; k < n; ++k) {
+            crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8);
+        }
+        return crc;
+    }
+
+    public static byte[] getBytes(String in) {
+        byte[] result = new byte[in.length() * 2];
+        int output = 0;
+        for (char ch : in.toCharArray()) {
+            result[output++] = (byte) (ch & 0xFF);
+            result[output++] = (byte) (ch >> 8);
+        }
+        return result;
+    }
+
+    public static void closeSilently(Closeable c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (IOException t) {
+            Log.w(TAG, "close fail ", t);
+        }
+    }
+
+    public static int compare(long a, long b) {
+        return a < b ? -1 : a == b ? 0 : 1;
+    }
+
+    public static int ceilLog2(float value) {
+        int i;
+        for (i = 0; i < 31; i++) {
+            if ((1 << i) >= value) break;
+        }
+        return i;
+    }
+
+    public static int floorLog2(float value) {
+        int i;
+        for (i = 0; i < 31; i++) {
+            if ((1 << i) > value) break;
+        }
+        return i - 1;
+    }
+
+    public static void closeSilently(ParcelFileDescriptor fd) {
+        try {
+            if (fd != null) fd.close();
+        } catch (Throwable t) {
+            Log.w(TAG, "fail to close", t);
+        }
+    }
+
+    public static void closeSilently(Cursor cursor) {
+        try {
+            if (cursor != null) cursor.close();
+        } catch (Throwable t) {
+            Log.w(TAG, "fail to close", t);
+        }
+    }
+
+    public static float interpolateAngle(
+            float source, float target, float progress) {
+        // interpolate the angle from source to target
+        // We make the difference in the range of [-179, 180], this is the
+        // shortest path to change source to target.
+        float diff = target - source;
+        if (diff < 0) diff += 360f;
+        if (diff > 180) diff -= 360f;
+
+        float result = source + diff * progress;
+        return result < 0 ? result + 360f : result;
+    }
+
+    public static float interpolateScale(
+            float source, float target, float progress) {
+        return source + progress * (target - source);
+    }
+
+    public static String ensureNotNull(String value) {
+        return value == null ? "" : value;
+    }
+
+    public static float parseFloatSafely(String content, float defaultValue) {
+        if (content == null) return defaultValue;
+        try {
+            return Float.parseFloat(content);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    public static int parseIntSafely(String content, int defaultValue) {
+        if (content == null) return defaultValue;
+        try {
+            return Integer.parseInt(content);
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    public static boolean isNullOrEmpty(String exifMake) {
+        return TextUtils.isEmpty(exifMake);
+    }
+
+    public static void waitWithoutInterrupt(Object object) {
+        try {
+            object.wait();
+        } catch (InterruptedException e) {
+            Log.w(TAG, "unexpected interrupt: " + object);
+        }
+    }
+
+    public static boolean handleInterrruptedException(Throwable e) {
+        // A helper to deal with the interrupt exception
+        // If an interrupt detected, we will setup the bit again.
+        if (e instanceof InterruptedIOException
+                || e instanceof InterruptedException) {
+            Thread.currentThread().interrupt();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return String with special XML characters escaped.
+     */
+    public static String escapeXml(String s) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0, len = s.length(); i < len; ++i) {
+            char c = s.charAt(i);
+            switch (c) {
+                case '<':  sb.append("&lt;"); break;
+                case '>':  sb.append("&gt;"); break;
+                case '\"': sb.append("&quot;"); break;
+                case '\'': sb.append("&#039;"); break;
+                case '&':  sb.append("&amp;"); break;
+                default: sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    public static String getUserAgent(Context context) {
+        PackageInfo packageInfo;
+        try {
+            packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+        } catch (NameNotFoundException e) {
+            throw new IllegalStateException("getPackageInfo failed");
+        }
+        return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s",
+                packageInfo.packageName,
+                packageInfo.versionName,
+                Build.BRAND,
+                Build.DEVICE,
+                Build.MODEL,
+                Build.ID,
+                Build.VERSION.SDK_INT,
+                Build.VERSION.RELEASE,
+                Build.VERSION.INCREMENTAL);
+    }
+
+    public static String[] copyOf(String[] source, int newSize) {
+        String[] result = new String[newSize];
+        newSize = Math.min(source.length, newSize);
+        System.arraycopy(source, 0, result, 0, newSize);
+        return result;
+    }
+
+    // Mask information for debugging only. It returns <code>info.toString()</code> directly
+    // for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****")
+    // in release build to protect the information (e.g. for privacy issue).
+    public static String maskDebugInfo(Object info) {
+        if (info == null) return null;
+        String s = info.toString();
+        int length = Math.min(s.length(), MASK_STRING.length());
+        return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length);
+    }
+
+    // This method should be ONLY used for debugging.
+    public static void debug(String message, Object ... args) {
+        Log.v(DEBUG_TAG, String.format(message, args));
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ByteBufferInputStream.java b/gallerycommon/src/com/android/gallery3d/exif/ByteBufferInputStream.java
new file mode 100644
index 0000000..7fb9f22
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ByteBufferInputStream.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+class ByteBufferInputStream extends InputStream {
+
+    private ByteBuffer mBuf;
+
+    public ByteBufferInputStream(ByteBuffer buf) {
+        mBuf = buf;
+    }
+
+    @Override
+    public int read() {
+        if (!mBuf.hasRemaining()) {
+            return -1;
+        }
+        return mBuf.get() & 0xFF;
+    }
+
+    @Override
+    public int read(byte[] bytes, int off, int len) {
+        if (!mBuf.hasRemaining()) {
+            return -1;
+        }
+
+        len = Math.min(len, mBuf.remaining());
+        mBuf.get(bytes, off, len);
+        return len;
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java b/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java
new file mode 100644
index 0000000..dfd4a1a
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+
+class CountedDataInputStream extends FilterInputStream {
+
+    private int mCount = 0;
+
+    // allocate a byte buffer for a long value;
+    private final byte mByteArray[] = new byte[8];
+    private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
+
+    protected CountedDataInputStream(InputStream in) {
+        super(in);
+    }
+
+    public int getReadByteCount() {
+        return mCount;
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        int r = in.read(b);
+        mCount += (r >= 0) ? r : 0;
+        return r;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        int r = in.read(b, off, len);
+        mCount += (r >= 0) ? r : 0;
+        return r;
+    }
+
+    @Override
+    public int read() throws IOException {
+        int r = in.read();
+        mCount += (r >= 0) ? 1 : 0;
+        return r;
+    }
+
+    @Override
+    public long skip(long length) throws IOException {
+        long skip = in.skip(length);
+        mCount += skip;
+        return skip;
+    }
+
+    public void skipOrThrow(long length) throws IOException {
+        if (skip(length) != length) throw new EOFException();
+    }
+
+    public void skipTo(long target) throws IOException {
+        long cur = mCount;
+        long diff = target - cur;
+        assert(diff >= 0);
+        skipOrThrow(diff);
+    }
+
+    public void readOrThrow(byte[] b, int off, int len) throws IOException {
+        int r = read(b, off, len);
+        if (r != len) throw new EOFException();
+    }
+
+    public void readOrThrow(byte[] b) throws IOException {
+        readOrThrow(b, 0, b.length);
+    }
+
+    public void setByteOrder(ByteOrder order) {
+        mByteBuffer.order(order);
+    }
+
+    public ByteOrder getByteOrder() {
+        return mByteBuffer.order();
+    }
+
+    public short readShort() throws IOException {
+        readOrThrow(mByteArray, 0 ,2);
+        mByteBuffer.rewind();
+        return mByteBuffer.getShort();
+    }
+
+    public int readUnsignedShort() throws IOException {
+        return readShort() & 0xffff;
+    }
+
+    public int readInt() throws IOException {
+        readOrThrow(mByteArray, 0 , 4);
+        mByteBuffer.rewind();
+        return mByteBuffer.getInt();
+    }
+
+    public long readUnsignedInt() throws IOException {
+        return readInt() & 0xffffffffL;
+    }
+
+    public long readLong() throws IOException {
+        readOrThrow(mByteArray, 0 , 8);
+        mByteBuffer.rewind();
+        return mByteBuffer.getLong();
+    }
+
+    public String readString(int n) throws IOException {
+        byte buf[] = new byte[n];
+        readOrThrow(buf);
+        return new String(buf, "UTF8");
+    }
+
+    public String readString(int n, Charset charset) throws IOException {
+        byte buf[] = new byte[n];
+        readOrThrow(buf);
+        return new String(buf, charset);
+    }
+}
\ No newline at end of file
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifData.java b/gallerycommon/src/com/android/gallery3d/exif/ExifData.java
new file mode 100644
index 0000000..8422382
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifData.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class stores the EXIF header in IFDs according to the JPEG
+ * specification. It is the result produced by {@link ExifReader}.
+ *
+ * @see ExifReader
+ * @see IfdData
+ */
+class ExifData {
+    private static final String TAG = "ExifData";
+    private static final byte[] USER_COMMENT_ASCII = {
+            0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00
+    };
+    private static final byte[] USER_COMMENT_JIS = {
+            0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00
+    };
+    private static final byte[] USER_COMMENT_UNICODE = {
+            0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00
+    };
+
+    private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
+    private byte[] mThumbnail;
+    private ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>();
+    private final ByteOrder mByteOrder;
+
+    ExifData(ByteOrder order) {
+        mByteOrder = order;
+    }
+
+    /**
+     * Gets the compressed thumbnail. Returns null if there is no compressed
+     * thumbnail.
+     *
+     * @see #hasCompressedThumbnail()
+     */
+    protected byte[] getCompressedThumbnail() {
+        return mThumbnail;
+    }
+
+    /**
+     * Sets the compressed thumbnail.
+     */
+    protected void setCompressedThumbnail(byte[] thumbnail) {
+        mThumbnail = thumbnail;
+    }
+
+    /**
+     * Returns true it this header contains a compressed thumbnail.
+     */
+    protected boolean hasCompressedThumbnail() {
+        return mThumbnail != null;
+    }
+
+    /**
+     * Adds an uncompressed strip.
+     */
+    protected void setStripBytes(int index, byte[] strip) {
+        if (index < mStripBytes.size()) {
+            mStripBytes.set(index, strip);
+        } else {
+            for (int i = mStripBytes.size(); i < index; i++) {
+                mStripBytes.add(null);
+            }
+            mStripBytes.add(strip);
+        }
+    }
+
+    /**
+     * Gets the strip count.
+     */
+    protected int getStripCount() {
+        return mStripBytes.size();
+    }
+
+    /**
+     * Gets the strip at the specified index.
+     *
+     * @exceptions #IndexOutOfBoundException
+     */
+    protected byte[] getStrip(int index) {
+        return mStripBytes.get(index);
+    }
+
+    /**
+     * Returns true if this header contains uncompressed strip.
+     */
+    protected boolean hasUncompressedStrip() {
+        return mStripBytes.size() != 0;
+    }
+
+    /**
+     * Gets the byte order.
+     */
+    protected ByteOrder getByteOrder() {
+        return mByteOrder;
+    }
+
+    /**
+     * Returns the {@link IfdData} object corresponding to a given IFD if it
+     * exists or null.
+     */
+    protected IfdData getIfdData(int ifdId) {
+        if (ExifTag.isValidIfd(ifdId)) {
+            return mIfdDatas[ifdId];
+        }
+        return null;
+    }
+
+    /**
+     * Adds IFD data. If IFD data of the same type already exists, it will be
+     * replaced by the new data.
+     */
+    protected void addIfdData(IfdData data) {
+        mIfdDatas[data.getId()] = data;
+    }
+
+    /**
+     * Returns the {@link IfdData} object corresponding to a given IFD or
+     * generates one if none exist.
+     */
+    protected IfdData getOrCreateIfdData(int ifdId) {
+        IfdData ifdData = mIfdDatas[ifdId];
+        if (ifdData == null) {
+            ifdData = new IfdData(ifdId);
+            mIfdDatas[ifdId] = ifdData;
+        }
+        return ifdData;
+    }
+
+    /**
+     * Returns the tag with a given TID in the given IFD if the tag exists.
+     * Otherwise returns null.
+     */
+    protected ExifTag getTag(short tag, int ifd) {
+        IfdData ifdData = mIfdDatas[ifd];
+        return (ifdData == null) ? null : ifdData.getTag(tag);
+    }
+
+    /**
+     * Adds the given ExifTag to its default IFD and returns an existing ExifTag
+     * with the same TID or null if none exist.
+     */
+    protected ExifTag addTag(ExifTag tag) {
+        if (tag != null) {
+            int ifd = tag.getIfd();
+            return addTag(tag, ifd);
+        }
+        return null;
+    }
+
+    /**
+     * Adds the given ExifTag to the given IFD and returns an existing ExifTag
+     * with the same TID or null if none exist.
+     */
+    protected ExifTag addTag(ExifTag tag, int ifdId) {
+        if (tag != null && ExifTag.isValidIfd(ifdId)) {
+            IfdData ifdData = getOrCreateIfdData(ifdId);
+            return ifdData.setTag(tag);
+        }
+        return null;
+    }
+
+    protected void clearThumbnailAndStrips() {
+        mThumbnail = null;
+        mStripBytes.clear();
+    }
+
+    /**
+     * Removes the thumbnail and its related tags. IFD1 will be removed.
+     */
+    protected void removeThumbnailData() {
+        clearThumbnailAndStrips();
+        mIfdDatas[IfdId.TYPE_IFD_1] = null;
+    }
+
+    /**
+     * Removes the tag with a given TID and IFD.
+     */
+    protected void removeTag(short tagId, int ifdId) {
+        IfdData ifdData = mIfdDatas[ifdId];
+        if (ifdData == null) {
+            return;
+        }
+        ifdData.removeTag(tagId);
+    }
+
+    /**
+     * Decodes the user comment tag into string as specified in the EXIF
+     * standard. Returns null if decoding failed.
+     */
+    protected String getUserComment() {
+        IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
+        if (ifdData == null) {
+            return null;
+        }
+        ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT));
+        if (tag == null) {
+            return null;
+        }
+        if (tag.getComponentCount() < 8) {
+            return null;
+        }
+
+        byte[] buf = new byte[tag.getComponentCount()];
+        tag.getBytes(buf);
+
+        byte[] code = new byte[8];
+        System.arraycopy(buf, 0, code, 0, 8);
+
+        try {
+            if (Arrays.equals(code, USER_COMMENT_ASCII)) {
+                return new String(buf, 8, buf.length - 8, "US-ASCII");
+            } else if (Arrays.equals(code, USER_COMMENT_JIS)) {
+                return new String(buf, 8, buf.length - 8, "EUC-JP");
+            } else if (Arrays.equals(code, USER_COMMENT_UNICODE)) {
+                return new String(buf, 8, buf.length - 8, "UTF-16");
+            } else {
+                return null;
+            }
+        } catch (UnsupportedEncodingException e) {
+            Log.w(TAG, "Failed to decode the user comment");
+            return null;
+        }
+    }
+
+    /**
+     * Returns a list of all {@link ExifTag}s in the ExifData or null if there
+     * are none.
+     */
+    protected List<ExifTag> getAllTags() {
+        ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
+        for (IfdData d : mIfdDatas) {
+            if (d != null) {
+                ExifTag[] tags = d.getAllTags();
+                if (tags != null) {
+                    for (ExifTag t : tags) {
+                        ret.add(t);
+                    }
+                }
+            }
+        }
+        if (ret.size() == 0) {
+            return null;
+        }
+        return ret;
+    }
+
+    /**
+     * Returns a list of all {@link ExifTag}s in a given IFD or null if there
+     * are none.
+     */
+    protected List<ExifTag> getAllTagsForIfd(int ifd) {
+        IfdData d = mIfdDatas[ifd];
+        if (d == null) {
+            return null;
+        }
+        ExifTag[] tags = d.getAllTags();
+        if (tags == null) {
+            return null;
+        }
+        ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length);
+        for (ExifTag t : tags) {
+            ret.add(t);
+        }
+        if (ret.size() == 0) {
+            return null;
+        }
+        return ret;
+    }
+
+    /**
+     * Returns a list of all {@link ExifTag}s with a given TID or null if there
+     * are none.
+     */
+    protected List<ExifTag> getAllTagsForTagId(short tag) {
+        ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
+        for (IfdData d : mIfdDatas) {
+            if (d != null) {
+                ExifTag t = d.getTag(tag);
+                if (t != null) {
+                    ret.add(t);
+                }
+            }
+        }
+        if (ret.size() == 0) {
+            return null;
+        }
+        return ret;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof ExifData) {
+            ExifData data = (ExifData) obj;
+            if (data.mByteOrder != mByteOrder ||
+                    data.mStripBytes.size() != mStripBytes.size() ||
+                    !Arrays.equals(data.mThumbnail, mThumbnail)) {
+                return false;
+            }
+            for (int i = 0; i < mStripBytes.size(); i++) {
+                if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) {
+                    return false;
+                }
+            }
+            for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+                IfdData ifd1 = data.getIfdData(i);
+                IfdData ifd2 = getIfdData(i);
+                if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java b/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java
new file mode 100644
index 0000000..a1cf0fc
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java
@@ -0,0 +1,2407 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.exif;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.SparseIntArray;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel.MapMode;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * This class provides methods and constants for reading and writing jpeg file
+ * metadata. It contains a collection of ExifTags, and a collection of
+ * definitions for creating valid ExifTags. The collection of ExifTags can be
+ * updated by: reading new ones from a file, deleting or adding existing ones,
+ * or building new ExifTags from a tag definition. These ExifTags can be written
+ * to a valid jpeg image as exif metadata.
+ * <p>
+ * Each ExifTag has a tag ID (TID) and is stored in a specific image file
+ * directory (IFD) as specified by the exif standard. A tag definition can be
+ * looked up with a constant that is a combination of TID and IFD. This
+ * definition has information about the type, number of components, and valid
+ * IFDs for a tag.
+ *
+ * @see ExifTag
+ */
+public class ExifInterface {
+    public static final int TAG_NULL = -1;
+    public static final int IFD_NULL = -1;
+    public static final int DEFINITION_NULL = 0;
+
+    /**
+     * Tag constants for Jeita EXIF 2.2
+     */
+
+    // IFD 0
+    public static final int TAG_IMAGE_WIDTH =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0100);
+    public static final int TAG_IMAGE_LENGTH =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0101); // Image height
+    public static final int TAG_BITS_PER_SAMPLE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0102);
+    public static final int TAG_COMPRESSION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0103);
+    public static final int TAG_PHOTOMETRIC_INTERPRETATION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0106);
+    public static final int TAG_IMAGE_DESCRIPTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x010E);
+    public static final int TAG_MAKE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x010F);
+    public static final int TAG_MODEL =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0110);
+    public static final int TAG_STRIP_OFFSETS =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0111);
+    public static final int TAG_ORIENTATION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0112);
+    public static final int TAG_SAMPLES_PER_PIXEL =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0115);
+    public static final int TAG_ROWS_PER_STRIP =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0116);
+    public static final int TAG_STRIP_BYTE_COUNTS =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0117);
+    public static final int TAG_X_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x011A);
+    public static final int TAG_Y_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x011B);
+    public static final int TAG_PLANAR_CONFIGURATION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x011C);
+    public static final int TAG_RESOLUTION_UNIT =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0128);
+    public static final int TAG_TRANSFER_FUNCTION =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x012D);
+    public static final int TAG_SOFTWARE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0131);
+    public static final int TAG_DATE_TIME =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0132);
+    public static final int TAG_ARTIST =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x013B);
+    public static final int TAG_WHITE_POINT =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x013E);
+    public static final int TAG_PRIMARY_CHROMATICITIES =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x013F);
+    public static final int TAG_Y_CB_CR_COEFFICIENTS =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0211);
+    public static final int TAG_Y_CB_CR_SUB_SAMPLING =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0212);
+    public static final int TAG_Y_CB_CR_POSITIONING =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0213);
+    public static final int TAG_REFERENCE_BLACK_WHITE =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x0214);
+    public static final int TAG_COPYRIGHT =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x8298);
+    public static final int TAG_EXIF_IFD =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x8769);
+    public static final int TAG_GPS_IFD =
+        defineTag(IfdId.TYPE_IFD_0, (short) 0x8825);
+    // IFD 1
+    public static final int TAG_JPEG_INTERCHANGE_FORMAT =
+        defineTag(IfdId.TYPE_IFD_1, (short) 0x0201);
+    public static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH =
+        defineTag(IfdId.TYPE_IFD_1, (short) 0x0202);
+    // IFD Exif Tags
+    public static final int TAG_EXPOSURE_TIME =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829A);
+    public static final int TAG_F_NUMBER =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829D);
+    public static final int TAG_EXPOSURE_PROGRAM =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8822);
+    public static final int TAG_SPECTRAL_SENSITIVITY =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8824);
+    public static final int TAG_ISO_SPEED_RATINGS =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8827);
+    public static final int TAG_OECF =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8828);
+    public static final int TAG_EXIF_VERSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9000);
+    public static final int TAG_DATE_TIME_ORIGINAL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9003);
+    public static final int TAG_DATE_TIME_DIGITIZED =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9004);
+    public static final int TAG_COMPONENTS_CONFIGURATION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9101);
+    public static final int TAG_COMPRESSED_BITS_PER_PIXEL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9102);
+    public static final int TAG_SHUTTER_SPEED_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9201);
+    public static final int TAG_APERTURE_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9202);
+    public static final int TAG_BRIGHTNESS_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9203);
+    public static final int TAG_EXPOSURE_BIAS_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9204);
+    public static final int TAG_MAX_APERTURE_VALUE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9205);
+    public static final int TAG_SUBJECT_DISTANCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9206);
+    public static final int TAG_METERING_MODE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9207);
+    public static final int TAG_LIGHT_SOURCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9208);
+    public static final int TAG_FLASH =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9209);
+    public static final int TAG_FOCAL_LENGTH =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x920A);
+    public static final int TAG_SUBJECT_AREA =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9214);
+    public static final int TAG_MAKER_NOTE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x927C);
+    public static final int TAG_USER_COMMENT =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9286);
+    public static final int TAG_SUB_SEC_TIME =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9290);
+    public static final int TAG_SUB_SEC_TIME_ORIGINAL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9291);
+    public static final int TAG_SUB_SEC_TIME_DIGITIZED =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9292);
+    public static final int TAG_FLASHPIX_VERSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA000);
+    public static final int TAG_COLOR_SPACE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA001);
+    public static final int TAG_PIXEL_X_DIMENSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA002);
+    public static final int TAG_PIXEL_Y_DIMENSION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA003);
+    public static final int TAG_RELATED_SOUND_FILE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA004);
+    public static final int TAG_INTEROPERABILITY_IFD =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005);
+    public static final int TAG_FLASH_ENERGY =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20B);
+    public static final int TAG_SPATIAL_FREQUENCY_RESPONSE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20C);
+    public static final int TAG_FOCAL_PLANE_X_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20E);
+    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20F);
+    public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA210);
+    public static final int TAG_SUBJECT_LOCATION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA214);
+    public static final int TAG_EXPOSURE_INDEX =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA215);
+    public static final int TAG_SENSING_METHOD =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA217);
+    public static final int TAG_FILE_SOURCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA300);
+    public static final int TAG_SCENE_TYPE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA301);
+    public static final int TAG_CFA_PATTERN =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA302);
+    public static final int TAG_CUSTOM_RENDERED =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA401);
+    public static final int TAG_EXPOSURE_MODE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA402);
+    public static final int TAG_WHITE_BALANCE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA403);
+    public static final int TAG_DIGITAL_ZOOM_RATIO =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA404);
+    public static final int TAG_FOCAL_LENGTH_IN_35_MM_FILE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA405);
+    public static final int TAG_SCENE_CAPTURE_TYPE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA406);
+    public static final int TAG_GAIN_CONTROL =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA407);
+    public static final int TAG_CONTRAST =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA408);
+    public static final int TAG_SATURATION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA409);
+    public static final int TAG_SHARPNESS =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40A);
+    public static final int TAG_DEVICE_SETTING_DESCRIPTION =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40B);
+    public static final int TAG_SUBJECT_DISTANCE_RANGE =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40C);
+    public static final int TAG_IMAGE_UNIQUE_ID =
+        defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA420);
+    // IFD GPS tags
+    public static final int TAG_GPS_VERSION_ID =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 0);
+    public static final int TAG_GPS_LATITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 1);
+    public static final int TAG_GPS_LATITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 2);
+    public static final int TAG_GPS_LONGITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 3);
+    public static final int TAG_GPS_LONGITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 4);
+    public static final int TAG_GPS_ALTITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 5);
+    public static final int TAG_GPS_ALTITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 6);
+    public static final int TAG_GPS_TIME_STAMP =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 7);
+    public static final int TAG_GPS_SATTELLITES =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 8);
+    public static final int TAG_GPS_STATUS =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 9);
+    public static final int TAG_GPS_MEASURE_MODE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 10);
+    public static final int TAG_GPS_DOP =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 11);
+    public static final int TAG_GPS_SPEED_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 12);
+    public static final int TAG_GPS_SPEED =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 13);
+    public static final int TAG_GPS_TRACK_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 14);
+    public static final int TAG_GPS_TRACK =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 15);
+    public static final int TAG_GPS_IMG_DIRECTION_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 16);
+    public static final int TAG_GPS_IMG_DIRECTION =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 17);
+    public static final int TAG_GPS_MAP_DATUM =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 18);
+    public static final int TAG_GPS_DEST_LATITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 19);
+    public static final int TAG_GPS_DEST_LATITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 20);
+    public static final int TAG_GPS_DEST_LONGITUDE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 21);
+    public static final int TAG_GPS_DEST_LONGITUDE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 22);
+    public static final int TAG_GPS_DEST_BEARING_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 23);
+    public static final int TAG_GPS_DEST_BEARING =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 24);
+    public static final int TAG_GPS_DEST_DISTANCE_REF =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 25);
+    public static final int TAG_GPS_DEST_DISTANCE =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 26);
+    public static final int TAG_GPS_PROCESSING_METHOD =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 27);
+    public static final int TAG_GPS_AREA_INFORMATION =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 28);
+    public static final int TAG_GPS_DATE_STAMP =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 29);
+    public static final int TAG_GPS_DIFFERENTIAL =
+        defineTag(IfdId.TYPE_IFD_GPS, (short) 30);
+    // IFD Interoperability tags
+    public static final int TAG_INTEROPERABILITY_INDEX =
+        defineTag(IfdId.TYPE_IFD_INTEROPERABILITY, (short) 1);
+
+    /**
+     * Tags that contain offset markers. These are included in the banned
+     * defines.
+     */
+    private static HashSet<Short> sOffsetTags = new HashSet<Short>();
+    static {
+        sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD));
+        sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD));
+        sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT));
+        sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD));
+        sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS));
+    }
+
+    /**
+     * Tags with definitions that cannot be overridden (banned defines).
+     */
+    protected static HashSet<Short> sBannedDefines = new HashSet<Short>(sOffsetTags);
+    static {
+        sBannedDefines.add(getTrueTagKey(TAG_NULL));
+        sBannedDefines.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+        sBannedDefines.add(getTrueTagKey(TAG_STRIP_BYTE_COUNTS));
+    }
+
+    /**
+     * Returns the constant representing a tag with a given TID and default IFD.
+     */
+    public static int defineTag(int ifdId, short tagId) {
+        return (tagId & 0x0000ffff) | (ifdId << 16);
+    }
+
+    /**
+     * Returns the TID for a tag constant.
+     */
+    public static short getTrueTagKey(int tag) {
+        // Truncate
+        return (short) tag;
+    }
+
+    /**
+     * Returns the default IFD for a tag constant.
+     */
+    public static int getTrueIfd(int tag) {
+        return tag >>> 16;
+    }
+
+    /**
+     * Constants for {@link TAG_ORIENTATION}. They can be interpreted as
+     * follows:
+     * <ul>
+     * <li>TOP_LEFT is the normal orientation.</li>
+     * <li>TOP_RIGHT is a left-right mirror.</li>
+     * <li>BOTTOM_LEFT is a 180 degree rotation.</li>
+     * <li>BOTTOM_RIGHT is a top-bottom mirror.</li>
+     * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.</li>
+     * <li>RIGHT_TOP is a 90 degree clockwise rotation.</li>
+     * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.</li>
+     * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.</li>
+     * </ul>
+     */
+    public static interface Orientation {
+        public static final short TOP_LEFT = 1;
+        public static final short TOP_RIGHT = 2;
+        public static final short BOTTOM_LEFT = 3;
+        public static final short BOTTOM_RIGHT = 4;
+        public static final short LEFT_TOP = 5;
+        public static final short RIGHT_TOP = 6;
+        public static final short LEFT_BOTTOM = 7;
+        public static final short RIGHT_BOTTOM = 8;
+    }
+
+    /**
+     * Constants for {@link TAG_Y_CB_CR_POSITIONING}
+     */
+    public static interface YCbCrPositioning {
+        public static final short CENTERED = 1;
+        public static final short CO_SITED = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_COMPRESSION}
+     */
+    public static interface Compression {
+        public static final short UNCOMPRESSION = 1;
+        public static final short JPEG = 6;
+    }
+
+    /**
+     * Constants for {@link TAG_RESOLUTION_UNIT}
+     */
+    public static interface ResolutionUnit {
+        public static final short INCHES = 2;
+        public static final short CENTIMETERS = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_PHOTOMETRIC_INTERPRETATION}
+     */
+    public static interface PhotometricInterpretation {
+        public static final short RGB = 2;
+        public static final short YCBCR = 6;
+    }
+
+    /**
+     * Constants for {@link TAG_PLANAR_CONFIGURATION}
+     */
+    public static interface PlanarConfiguration {
+        public static final short CHUNKY = 1;
+        public static final short PLANAR = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_EXPOSURE_PROGRAM}
+     */
+    public static interface ExposureProgram {
+        public static final short NOT_DEFINED = 0;
+        public static final short MANUAL = 1;
+        public static final short NORMAL_PROGRAM = 2;
+        public static final short APERTURE_PRIORITY = 3;
+        public static final short SHUTTER_PRIORITY = 4;
+        public static final short CREATIVE_PROGRAM = 5;
+        public static final short ACTION_PROGRAM = 6;
+        public static final short PROTRAIT_MODE = 7;
+        public static final short LANDSCAPE_MODE = 8;
+    }
+
+    /**
+     * Constants for {@link TAG_METERING_MODE}
+     */
+    public static interface MeteringMode {
+        public static final short UNKNOWN = 0;
+        public static final short AVERAGE = 1;
+        public static final short CENTER_WEIGHTED_AVERAGE = 2;
+        public static final short SPOT = 3;
+        public static final short MULTISPOT = 4;
+        public static final short PATTERN = 5;
+        public static final short PARTAIL = 6;
+        public static final short OTHER = 255;
+    }
+
+    /**
+     * Constants for {@link TAG_FLASH} As the definition in Jeita EXIF 2.2
+     * standard, we can treat this constant as bitwise flag.
+     * <p>
+     * e.g.
+     * <p>
+     * short flash = FIRED | RETURN_STROBE_RETURN_LIGHT_DETECTED |
+     * MODE_AUTO_MODE
+     */
+    public static interface Flash {
+        // LSB
+        public static final short DID_NOT_FIRED = 0;
+        public static final short FIRED = 1;
+        // 1st~2nd bits
+        public static final short RETURN_NO_STROBE_RETURN_DETECTION_FUNCTION = 0 << 1;
+        public static final short RETURN_STROBE_RETURN_LIGHT_NOT_DETECTED = 2 << 1;
+        public static final short RETURN_STROBE_RETURN_LIGHT_DETECTED = 3 << 1;
+        // 3rd~4th bits
+        public static final short MODE_UNKNOWN = 0 << 3;
+        public static final short MODE_COMPULSORY_FLASH_FIRING = 1 << 3;
+        public static final short MODE_COMPULSORY_FLASH_SUPPRESSION = 2 << 3;
+        public static final short MODE_AUTO_MODE = 3 << 3;
+        // 5th bit
+        public static final short FUNCTION_PRESENT = 0 << 5;
+        public static final short FUNCTION_NO_FUNCTION = 1 << 5;
+        // 6th bit
+        public static final short RED_EYE_REDUCTION_NO_OR_UNKNOWN = 0 << 6;
+        public static final short RED_EYE_REDUCTION_SUPPORT = 1 << 6;
+    }
+
+    /**
+     * Constants for {@link TAG_COLOR_SPACE}
+     */
+    public static interface ColorSpace {
+        public static final short SRGB = 1;
+        public static final short UNCALIBRATED = (short) 0xFFFF;
+    }
+
+    /**
+     * Constants for {@link TAG_EXPOSURE_MODE}
+     */
+    public static interface ExposureMode {
+        public static final short AUTO_EXPOSURE = 0;
+        public static final short MANUAL_EXPOSURE = 1;
+        public static final short AUTO_BRACKET = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_WHITE_BALANCE}
+     */
+    public static interface WhiteBalance {
+        public static final short AUTO = 0;
+        public static final short MANUAL = 1;
+    }
+
+    /**
+     * Constants for {@link TAG_SCENE_CAPTURE_TYPE}
+     */
+    public static interface SceneCapture {
+        public static final short STANDARD = 0;
+        public static final short LANDSCAPE = 1;
+        public static final short PROTRAIT = 2;
+        public static final short NIGHT_SCENE = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_COMPONENTS_CONFIGURATION}
+     */
+    public static interface ComponentsConfiguration {
+        public static final short NOT_EXIST = 0;
+        public static final short Y = 1;
+        public static final short CB = 2;
+        public static final short CR = 3;
+        public static final short R = 4;
+        public static final short G = 5;
+        public static final short B = 6;
+    }
+
+    /**
+     * Constants for {@link TAG_LIGHT_SOURCE}
+     */
+    public static interface LightSource {
+        public static final short UNKNOWN = 0;
+        public static final short DAYLIGHT = 1;
+        public static final short FLUORESCENT = 2;
+        public static final short TUNGSTEN = 3;
+        public static final short FLASH = 4;
+        public static final short FINE_WEATHER = 9;
+        public static final short CLOUDY_WEATHER = 10;
+        public static final short SHADE = 11;
+        public static final short DAYLIGHT_FLUORESCENT = 12;
+        public static final short DAY_WHITE_FLUORESCENT = 13;
+        public static final short COOL_WHITE_FLUORESCENT = 14;
+        public static final short WHITE_FLUORESCENT = 15;
+        public static final short STANDARD_LIGHT_A = 17;
+        public static final short STANDARD_LIGHT_B = 18;
+        public static final short STANDARD_LIGHT_C = 19;
+        public static final short D55 = 20;
+        public static final short D65 = 21;
+        public static final short D75 = 22;
+        public static final short D50 = 23;
+        public static final short ISO_STUDIO_TUNGSTEN = 24;
+        public static final short OTHER = 255;
+    }
+
+    /**
+     * Constants for {@link TAG_SENSING_METHOD}
+     */
+    public static interface SensingMethod {
+        public static final short NOT_DEFINED = 1;
+        public static final short ONE_CHIP_COLOR = 2;
+        public static final short TWO_CHIP_COLOR = 3;
+        public static final short THREE_CHIP_COLOR = 4;
+        public static final short COLOR_SEQUENTIAL_AREA = 5;
+        public static final short TRILINEAR = 7;
+        public static final short COLOR_SEQUENTIAL_LINEAR = 8;
+    }
+
+    /**
+     * Constants for {@link TAG_FILE_SOURCE}
+     */
+    public static interface FileSource {
+        public static final short DSC = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_SCENE_TYPE}
+     */
+    public static interface SceneType {
+        public static final short DIRECT_PHOTOGRAPHED = 1;
+    }
+
+    /**
+     * Constants for {@link TAG_GAIN_CONTROL}
+     */
+    public static interface GainControl {
+        public static final short NONE = 0;
+        public static final short LOW_UP = 1;
+        public static final short HIGH_UP = 2;
+        public static final short LOW_DOWN = 3;
+        public static final short HIGH_DOWN = 4;
+    }
+
+    /**
+     * Constants for {@link TAG_CONTRAST}
+     */
+    public static interface Contrast {
+        public static final short NORMAL = 0;
+        public static final short SOFT = 1;
+        public static final short HARD = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_SATURATION}
+     */
+    public static interface Saturation {
+        public static final short NORMAL = 0;
+        public static final short LOW = 1;
+        public static final short HIGH = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_SHARPNESS}
+     */
+    public static interface Sharpness {
+        public static final short NORMAL = 0;
+        public static final short SOFT = 1;
+        public static final short HARD = 2;
+    }
+
+    /**
+     * Constants for {@link TAG_SUBJECT_DISTANCE}
+     */
+    public static interface SubjectDistance {
+        public static final short UNKNOWN = 0;
+        public static final short MACRO = 1;
+        public static final short CLOSE_VIEW = 2;
+        public static final short DISTANT_VIEW = 3;
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_LATITUDE_REF},
+     * {@link TAG_GPS_DEST_LATITUDE_REF}
+     */
+    public static interface GpsLatitudeRef {
+        public static final String NORTH = "N";
+        public static final String SOUTH = "S";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_LONGITUDE_REF},
+     * {@link TAG_GPS_DEST_LONGITUDE_REF}
+     */
+    public static interface GpsLongitudeRef {
+        public static final String EAST = "E";
+        public static final String WEST = "W";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_ALTITUDE_REF}
+     */
+    public static interface GpsAltitudeRef {
+        public static final short SEA_LEVEL = 0;
+        public static final short SEA_LEVEL_NEGATIVE = 1;
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_STATUS}
+     */
+    public static interface GpsStatus {
+        public static final String IN_PROGRESS = "A";
+        public static final String INTEROPERABILITY = "V";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_MEASURE_MODE}
+     */
+    public static interface GpsMeasureMode {
+        public static final String MODE_2_DIMENSIONAL = "2";
+        public static final String MODE_3_DIMENSIONAL = "3";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_SPEED_REF},
+     * {@link TAG_GPS_DEST_DISTANCE_REF}
+     */
+    public static interface GpsSpeedRef {
+        public static final String KILOMETERS = "K";
+        public static final String MILES = "M";
+        public static final String KNOTS = "N";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_TRACK_REF},
+     * {@link TAG_GPS_IMG_DIRECTION_REF}, {@link TAG_GPS_DEST_BEARING_REF}
+     */
+    public static interface GpsTrackRef {
+        public static final String TRUE_DIRECTION = "T";
+        public static final String MAGNETIC_DIRECTION = "M";
+    }
+
+    /**
+     * Constants for {@link TAG_GPS_DIFFERENTIAL}
+     */
+    public static interface GpsDifferential {
+        public static final short WITHOUT_DIFFERENTIAL_CORRECTION = 0;
+        public static final short DIFFERENTIAL_CORRECTION_APPLIED = 1;
+    }
+
+    private static final String NULL_ARGUMENT_STRING = "Argument is null";
+    private ExifData mData = new ExifData(DEFAULT_BYTE_ORDER);
+    public static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
+
+    public ExifInterface() {
+        mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+
+    /**
+     * Reads the exif tags from a byte array, clearing this ExifInterface
+     * object's existing exif tags.
+     *
+     * @param jpeg a byte array containing a jpeg compressed image.
+     * @throws IOException
+     */
+    public void readExif(byte[] jpeg) throws IOException {
+        readExif(new ByteArrayInputStream(jpeg));
+    }
+
+    /**
+     * Reads the exif tags from an InputStream, clearing this ExifInterface
+     * object's existing exif tags.
+     *
+     * @param inStream an InputStream containing a jpeg compressed image.
+     * @throws IOException
+     */
+    public void readExif(InputStream inStream) throws IOException {
+        if (inStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        ExifData d = null;
+        try {
+            d = new ExifReader(this).read(inStream);
+        } catch (ExifInvalidFormatException e) {
+            throw new IOException("Invalid exif format : " + e);
+        }
+        mData = d;
+    }
+
+    /**
+     * Reads the exif tags from a file, clearing this ExifInterface object's
+     * existing exif tags.
+     *
+     * @param inFileName a string representing the filepath to jpeg file.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void readExif(String inFileName) throws FileNotFoundException, IOException {
+        if (inFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        InputStream is = null;
+        try {
+            is = (InputStream) new BufferedInputStream(new FileInputStream(inFileName));
+            readExif(is);
+        } catch (IOException e) {
+            closeSilently(is);
+            throw e;
+        }
+        is.close();
+    }
+
+    /**
+     * Sets the exif tags, clearing this ExifInterface object's existing exif
+     * tags.
+     *
+     * @param tags a collection of exif tags to set.
+     */
+    public void setExif(Collection<ExifTag> tags) {
+        clearExif();
+        setTags(tags);
+    }
+
+    /**
+     * Clears this ExifInterface object's existing exif tags.
+     */
+    public void clearExif() {
+        mData = new ExifData(DEFAULT_BYTE_ORDER);
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg image,
+     * removing prior exif tags.
+     *
+     * @param jpeg a byte array containing a jpeg compressed image.
+     * @param exifOutStream an OutputStream to which the jpeg image with added
+     *            exif tags will be written.
+     * @throws IOException
+     */
+    public void writeExif(byte[] jpeg, OutputStream exifOutStream) throws IOException {
+        if (jpeg == null || exifOutStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = getExifWriterStream(exifOutStream);
+        s.write(jpeg, 0, jpeg.length);
+        s.flush();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg compressed
+     * bitmap, removing prior exif tags.
+     *
+     * @param bmap a bitmap to compress and write exif into.
+     * @param exifOutStream the OutputStream to which the jpeg image with added
+     *            exif tags will be written.
+     * @throws IOException
+     */
+    public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException {
+        if (bmap == null || exifOutStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = getExifWriterStream(exifOutStream);
+        bmap.compress(Bitmap.CompressFormat.JPEG, 90, s);
+        s.flush();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg stream,
+     * removing prior exif tags.
+     *
+     * @param jpegStream an InputStream containing a jpeg compressed image.
+     * @param exifOutStream an OutputStream to which the jpeg image with added
+     *            exif tags will be written.
+     * @throws IOException
+     */
+    public void writeExif(InputStream jpegStream, OutputStream exifOutStream) throws IOException {
+        if (jpegStream == null || exifOutStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = getExifWriterStream(exifOutStream);
+        doExifStreamIO(jpegStream, s);
+        s.flush();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg image,
+     * removing prior exif tags.
+     *
+     * @param jpeg a byte array containing a jpeg compressed image.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(byte[] jpeg, String exifOutFileName) throws FileNotFoundException,
+            IOException {
+        if (jpeg == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = null;
+        try {
+            s = getExifWriterStream(exifOutFileName);
+            s.write(jpeg, 0, jpeg.length);
+            s.flush();
+        } catch (IOException e) {
+            closeSilently(s);
+            throw e;
+        }
+        s.close();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg compressed
+     * bitmap, removing prior exif tags.
+     *
+     * @param bmap a bitmap to compress and write exif into.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(Bitmap bmap, String exifOutFileName) throws FileNotFoundException,
+            IOException {
+        if (bmap == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = null;
+        try {
+            s = getExifWriterStream(exifOutFileName);
+            bmap.compress(Bitmap.CompressFormat.JPEG, 90, s);
+            s.flush();
+        } catch (IOException e) {
+            closeSilently(s);
+            throw e;
+        }
+        s.close();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg stream,
+     * removing prior exif tags.
+     *
+     * @param jpegStream an InputStream containing a jpeg compressed image.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(InputStream jpegStream, String exifOutFileName)
+            throws FileNotFoundException, IOException {
+        if (jpegStream == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream s = null;
+        try {
+            s = getExifWriterStream(exifOutFileName);
+            doExifStreamIO(jpegStream, s);
+            s.flush();
+        } catch (IOException e) {
+            closeSilently(s);
+            throw e;
+        }
+        s.close();
+    }
+
+    /**
+     * Writes the tags from this ExifInterface object into a jpeg file, removing
+     * prior exif tags.
+     *
+     * @param jpegFileName a String containing the filepath for a jpeg file.
+     * @param exifOutFileName a String containing the filepath to which the jpeg
+     *            image with added exif tags will be written.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public void writeExif(String jpegFileName, String exifOutFileName)
+            throws FileNotFoundException, IOException {
+        if (jpegFileName == null || exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        InputStream is = null;
+        try {
+            is = new FileInputStream(jpegFileName);
+            writeExif(is, exifOutFileName);
+        } catch (IOException e) {
+            closeSilently(is);
+            throw e;
+        }
+        is.close();
+    }
+
+    /**
+     * Wraps an OutputStream object with an ExifOutputStream. Exif tags in this
+     * ExifInterface object will be added to a jpeg image written to this
+     * stream, removing prior exif tags. Other methods of this ExifInterface
+     * object should not be called until the returned OutputStream has been
+     * closed.
+     *
+     * @param outStream an OutputStream to wrap.
+     * @return an OutputStream that wraps the outStream parameter, and adds exif
+     *         metadata. A jpeg image should be written to this stream.
+     */
+    public OutputStream getExifWriterStream(OutputStream outStream) {
+        if (outStream == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        ExifOutputStream eos = new ExifOutputStream(outStream, this);
+        eos.setExifData(mData);
+        return eos;
+    }
+
+    /**
+     * Returns an OutputStream object that writes to a file. Exif tags in this
+     * ExifInterface object will be added to a jpeg image written to this
+     * stream, removing prior exif tags. Other methods of this ExifInterface
+     * object should not be called until the returned OutputStream has been
+     * closed.
+     *
+     * @param exifOutFileName an String containing a filepath for a jpeg file.
+     * @return an OutputStream that writes to the exifOutFileName file, and adds
+     *         exif metadata. A jpeg image should be written to this stream.
+     * @throws FileNotFoundException
+     */
+    public OutputStream getExifWriterStream(String exifOutFileName) throws FileNotFoundException {
+        if (exifOutFileName == null) {
+            throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+        }
+        OutputStream out = null;
+        try {
+            out = (OutputStream) new FileOutputStream(exifOutFileName);
+        } catch (FileNotFoundException e) {
+            closeSilently(out);
+            throw e;
+        }
+        return getExifWriterStream(out);
+    }
+
+    /**
+     * Attempts to do an in-place rewrite the exif metadata in a file for the
+     * given tags. If tags do not exist or do not have the same size as the
+     * existing exif tags, this method will fail.
+     *
+     * @param filename a String containing a filepath for a jpeg file with exif
+     *            tags to rewrite.
+     * @param tags tags that will be written into the jpeg file over existing
+     *            tags if possible.
+     * @return true if success, false if could not overwrite. If false, no
+     *         changes are made to the file.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public boolean rewriteExif(String filename, Collection<ExifTag> tags)
+            throws FileNotFoundException, IOException {
+        RandomAccessFile file = null;
+        InputStream is = null;
+        boolean ret;
+        try {
+            File temp = new File(filename);
+            is = new BufferedInputStream(new FileInputStream(temp));
+
+            // Parse beginning of APP1 in exif to find size of exif header.
+            ExifParser parser = null;
+            try {
+                parser = ExifParser.parse(is, this);
+            } catch (ExifInvalidFormatException e) {
+                throw new IOException("Invalid exif format : ", e);
+            }
+            long exifSize = parser.getOffsetToExifEndFromSOF();
+
+            // Free up resources
+            is.close();
+            is = null;
+
+            // Open file for memory mapping.
+            file = new RandomAccessFile(temp, "rw");
+            long fileLength = file.length();
+            if (fileLength < exifSize) {
+                throw new IOException("Filesize changed during operation");
+            }
+
+            // Map only exif header into memory.
+            ByteBuffer buf = file.getChannel().map(MapMode.READ_WRITE, 0, exifSize);
+
+            // Attempt to overwrite tag values without changing lengths (avoids
+            // file copy).
+            ret = rewriteExif(buf, tags);
+        } catch (IOException e) {
+            closeSilently(file);
+            throw e;
+        } finally {
+            closeSilently(is);
+        }
+        file.close();
+        return ret;
+    }
+
+    /**
+     * Attempts to do an in-place rewrite the exif metadata in a ByteBuffer for
+     * the given tags. If tags do not exist or do not have the same size as the
+     * existing exif tags, this method will fail.
+     *
+     * @param buf a ByteBuffer containing a jpeg file with existing exif tags to
+     *            rewrite.
+     * @param tags tags that will be written into the jpeg ByteBuffer over
+     *            existing tags if possible.
+     * @return true if success, false if could not overwrite. If false, no
+     *         changes are made to the ByteBuffer.
+     * @throws IOException
+     */
+    public boolean rewriteExif(ByteBuffer buf, Collection<ExifTag> tags) throws IOException {
+        ExifModifier mod = null;
+        try {
+            mod = new ExifModifier(buf, this);
+            for (ExifTag t : tags) {
+                mod.modifyTag(t);
+            }
+            return mod.commit();
+        } catch (ExifInvalidFormatException e) {
+            throw new IOException("Invalid exif format : " + e);
+        }
+    }
+
+    /**
+     * Attempts to do an in-place rewrite of the exif metadata. If this fails,
+     * fall back to overwriting file. This preserves tags that are not being
+     * rewritten.
+     *
+     * @param filename a String containing a filepath for a jpeg file.
+     * @param tags tags that will be written into the jpeg file over existing
+     *            tags if possible.
+     * @throws FileNotFoundException
+     * @throws IOException
+     * @see #rewriteExif
+     */
+    public void forceRewriteExif(String filename, Collection<ExifTag> tags)
+            throws FileNotFoundException,
+            IOException {
+        // Attempt in-place write
+        if (!rewriteExif(filename, tags)) {
+            // Fall back to doing a copy
+            ExifData tempData = mData;
+            mData = new ExifData(DEFAULT_BYTE_ORDER);
+            FileInputStream is = null;
+            ByteArrayOutputStream bytes = null;
+            try {
+                is = new FileInputStream(filename);
+                bytes = new ByteArrayOutputStream();
+                doExifStreamIO(is, bytes);
+                byte[] imageBytes = bytes.toByteArray();
+                readExif(imageBytes);
+                setTags(tags);
+                writeExif(imageBytes, filename);
+            } catch (IOException e) {
+                closeSilently(is);
+                throw e;
+            } finally {
+                is.close();
+                // Prevent clobbering of mData
+                mData = tempData;
+            }
+        }
+    }
+
+    /**
+     * Attempts to do an in-place rewrite of the exif metadata using the tags in
+     * this ExifInterface object. If this fails, fall back to overwriting file.
+     * This preserves tags that are not being rewritten.
+     *
+     * @param filename a String containing a filepath for a jpeg file.
+     * @throws FileNotFoundException
+     * @throws IOException
+     * @see #rewriteExif
+     */
+    public void forceRewriteExif(String filename) throws FileNotFoundException, IOException {
+        forceRewriteExif(filename, getAllTags());
+    }
+
+    /**
+     * Get the exif tags in this ExifInterface object or null if none exist.
+     *
+     * @return a List of {@link ExifTag}s.
+     */
+    public List<ExifTag> getAllTags() {
+        return mData.getAllTags();
+    }
+
+    /**
+     * Returns a list of ExifTags that share a TID (which can be obtained by
+     * calling {@link #getTrueTagKey} on a defined tag constant) or null if none
+     * exist.
+     *
+     * @param tagId a TID as defined in the exif standard (or with
+     *            {@link #defineTag}).
+     * @return a List of {@link ExifTag}s.
+     */
+    public List<ExifTag> getTagsForTagId(short tagId) {
+        return mData.getAllTagsForTagId(tagId);
+    }
+
+    /**
+     * Returns a list of ExifTags that share an IFD (which can be obtained by
+     * calling {@link #getTrueIFD} on a defined tag constant) or null if none
+     * exist.
+     *
+     * @param ifdId an IFD as defined in the exif standard (or with
+     *            {@link #defineTag}).
+     * @return a List of {@link ExifTag}s.
+     */
+    public List<ExifTag> getTagsForIfdId(int ifdId) {
+        return mData.getAllTagsForIfd(ifdId);
+    }
+
+    /**
+     * Gets an ExifTag for an IFD other than the tag's default.
+     *
+     * @see #getTag
+     */
+    public ExifTag getTag(int tagId, int ifdId) {
+        if (!ExifTag.isValidIfd(ifdId)) {
+            return null;
+        }
+        return mData.getTag(getTrueTagKey(tagId), ifdId);
+    }
+
+    /**
+     * Returns the ExifTag in that tag's default IFD for a defined tag constant
+     * or null if none exists.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return an {@link ExifTag} or null if none exists.
+     */
+    public ExifTag getTag(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTag(tagId, ifdId);
+    }
+
+    /**
+     * Gets a tag value for an IFD other than the tag's default.
+     *
+     * @see #getTagValue
+     */
+    public Object getTagValue(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        return (t == null) ? null : t.getValue();
+    }
+
+    /**
+     * Returns the value of the ExifTag in that tag's default IFD for a defined
+     * tag constant or null if none exists or the value could not be cast into
+     * the return type.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the value of the ExifTag or null if none exists.
+     */
+    public Object getTagValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagValue(tagId, ifdId);
+    }
+
+    /*
+     * Getter methods that are similar to getTagValue. Null is returned if the
+     * tag value cannot be cast into the return type.
+     */
+
+    /**
+     * @see #getTagValue
+     */
+    public String getTagStringValue(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsString();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public String getTagStringValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagStringValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Long getTagLongValue(int tagId, int ifdId) {
+        long[] l = getTagLongValues(tagId, ifdId);
+        if (l == null || l.length <= 0) {
+            return null;
+        }
+        return new Long(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Long getTagLongValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagLongValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Integer getTagIntValue(int tagId, int ifdId) {
+        int[] l = getTagIntValues(tagId, ifdId);
+        if (l == null || l.length <= 0) {
+            return null;
+        }
+        return new Integer(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Integer getTagIntValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagIntValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Byte getTagByteValue(int tagId, int ifdId) {
+        byte[] l = getTagByteValues(tagId, ifdId);
+        if (l == null || l.length <= 0) {
+            return null;
+        }
+        return new Byte(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Byte getTagByteValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagByteValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational getTagRationalValue(int tagId, int ifdId) {
+        Rational[] l = getTagRationalValues(tagId, ifdId);
+        if (l == null || l.length == 0) {
+            return null;
+        }
+        return new Rational(l[0]);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational getTagRationalValue(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagRationalValue(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public long[] getTagLongValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsLongs();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public long[] getTagLongValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagLongValues(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public int[] getTagIntValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsInts();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public int[] getTagIntValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagIntValues(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public byte[] getTagByteValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsBytes();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public byte[] getTagByteValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagByteValues(tagId, ifdId);
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational[] getTagRationalValues(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return null;
+        }
+        return t.getValueAsRationals();
+    }
+
+    /**
+     * @see #getTagValue
+     */
+    public Rational[] getTagRationalValues(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return getTagRationalValues(tagId, ifdId);
+    }
+
+    /**
+     * Checks whether a tag has a defined number of elements.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return true if the tag has a defined number of elements.
+     */
+    public boolean isTagCountDefined(int tagId) {
+        int info = getTagInfo().get(tagId);
+        // No value in info can be zero, as all tags have a non-zero type
+        if (info == 0) {
+            return false;
+        }
+        return getComponentCountFromInfo(info) != ExifTag.SIZE_UNDEFINED;
+    }
+
+    /**
+     * Gets the defined number of elements for a tag.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the number of elements or {@link ExifTag#SIZE_UNDEFINED} if the
+     *         tag or the number of elements is not defined.
+     */
+    public int getDefinedTagCount(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0) {
+            return ExifTag.SIZE_UNDEFINED;
+        }
+        return getComponentCountFromInfo(info);
+    }
+
+    /**
+     * Gets the number of elements for an ExifTag in a given IFD.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD containing the ExifTag to check.
+     * @return the number of elements in the ExifTag, if the tag's size is
+     *         undefined this will return the actual number of elements that is
+     *         in the ExifTag's value.
+     */
+    public int getActualTagCount(int tagId, int ifdId) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return 0;
+        }
+        return t.getComponentCount();
+    }
+
+    /**
+     * Gets the default IFD for a tag.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the default IFD for a tag definition or {@link #IFD_NULL} if no
+     *         definition exists.
+     */
+    public int getDefinedTagDefaultIfd(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == DEFINITION_NULL) {
+            return IFD_NULL;
+        }
+        return getTrueIfd(tagId);
+    }
+
+    /**
+     * Gets the defined type for a tag.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @return the type.
+     * @see ExifTag#getDataType()
+     */
+    public short getDefinedTagType(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0) {
+            return -1;
+        }
+        return getTypeFromInfo(info);
+    }
+
+    /**
+     * Returns true if tag TID is one of the following: {@link TAG_EXIF_IFD},
+     * {@link TAG_GPS_IFD}, {@link TAG_JPEG_INTERCHANGE_FORMAT},
+     * {@link TAG_STRIP_OFFSETS}, {@link TAG_INTEROPERABILITY_IFD}
+     * <p>
+     * Note: defining tags with these TID's is disallowed.
+     *
+     * @param tag a tag's TID (can be obtained from a defined tag constant with
+     *            {@link #getTrueTagKey}).
+     * @return true if the TID is that of an offset tag.
+     */
+    protected static boolean isOffsetTag(short tag) {
+        return sOffsetTags.contains(tag);
+    }
+
+    /**
+     * Creates a tag for a defined tag constant in a given IFD if that IFD is
+     * allowed for the tag.  This method will fail anytime the appropriate
+     * {@link ExifTag#setValue} for this tag's datatype would fail.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD that the tag should be in.
+     * @param val the value of the tag to set.
+     * @return an ExifTag object or null if one could not be constructed.
+     * @see #buildTag
+     */
+    public ExifTag buildTag(int tagId, int ifdId, Object val) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0 || val == null) {
+            return null;
+        }
+        short type = getTypeFromInfo(info);
+        int definedCount = getComponentCountFromInfo(info);
+        boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED);
+        if (!ExifInterface.isIfdAllowed(info, ifdId)) {
+            return null;
+        }
+        ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount);
+        if (!t.setValue(val)) {
+            return null;
+        }
+        return t;
+    }
+
+    /**
+     * Creates a tag for a defined tag constant in the tag's default IFD.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param val the tag's value.
+     * @return an ExifTag object.
+     */
+    public ExifTag buildTag(int tagId, Object val) {
+        int ifdId = getTrueIfd(tagId);
+        return buildTag(tagId, ifdId, val);
+    }
+
+    protected ExifTag buildUninitializedTag(int tagId) {
+        int info = getTagInfo().get(tagId);
+        if (info == 0) {
+            return null;
+        }
+        short type = getTypeFromInfo(info);
+        int definedCount = getComponentCountFromInfo(info);
+        boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED);
+        int ifdId = getTrueIfd(tagId);
+        ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount);
+        return t;
+    }
+
+    /**
+     * Sets the value of an ExifTag if it exists in the given IFD. The value
+     * must be the correct type and length for that ExifTag.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD that the ExifTag is in.
+     * @param val the value to set.
+     * @return true if success, false if the ExifTag doesn't exist or the value
+     *         is the wrong type/length.
+     * @see #setTagValue
+     */
+    public boolean setTagValue(int tagId, int ifdId, Object val) {
+        ExifTag t = getTag(tagId, ifdId);
+        if (t == null) {
+            return false;
+        }
+        return t.setValue(val);
+    }
+
+    /**
+     * Sets the value of an ExifTag if it exists it's default IFD. The value
+     * must be the correct type and length for that ExifTag.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param val the value to set.
+     * @return true if success, false if the ExifTag doesn't exist or the value
+     *         is the wrong type/length.
+     */
+    public boolean setTagValue(int tagId, Object val) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        return setTagValue(tagId, ifdId, val);
+    }
+
+    /**
+     * Puts an ExifTag into this ExifInterface object's tags, removing a
+     * previous ExifTag with the same TID and IFD. The IFD it is put into will
+     * be the one the tag was created with in {@link #buildTag}.
+     *
+     * @param tag an ExifTag to put into this ExifInterface's tags.
+     * @return the previous ExifTag with the same TID and IFD or null if none
+     *         exists.
+     */
+    public ExifTag setTag(ExifTag tag) {
+        return mData.addTag(tag);
+    }
+
+    /**
+     * Puts a collection of ExifTags into this ExifInterface objects's tags. Any
+     * previous ExifTags with the same TID and IFDs will be removed.
+     *
+     * @param tags a Collection of ExifTags.
+     * @see #setTag
+     */
+    public void setTags(Collection<ExifTag> tags) {
+        for (ExifTag t : tags) {
+            setTag(t);
+        }
+    }
+
+    /**
+     * Removes the ExifTag for a tag constant from the given IFD.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     * @param ifdId the IFD of the ExifTag to remove.
+     */
+    public void deleteTag(int tagId, int ifdId) {
+        mData.removeTag(getTrueTagKey(tagId), ifdId);
+    }
+
+    /**
+     * Removes the ExifTag for a tag constant from that tag's default IFD.
+     *
+     * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     */
+    public void deleteTag(int tagId) {
+        int ifdId = getDefinedTagDefaultIfd(tagId);
+        deleteTag(tagId, ifdId);
+    }
+
+    /**
+     * Creates a new tag definition in this ExifInterface object for a given TID
+     * and default IFD. Creating a definition with the same TID and default IFD
+     * as a previous definition will override it.
+     *
+     * @param tagId the TID for the tag.
+     * @param defaultIfd the default IFD for the tag.
+     * @param tagType the type of the tag (see {@link ExifTag#getDataType()}).
+     * @param defaultComponentCount the number of elements of this tag's type in
+     *            the tags value.
+     * @param allowedIfds the IFD's this tag is allowed to be put in.
+     * @return the defined tag constant (e.g. {@link #TAG_IMAGE_WIDTH}) or
+     *         {@link #TAG_NULL} if the definition could not be made.
+     */
+    public int setTagDefinition(short tagId, int defaultIfd, short tagType,
+            short defaultComponentCount, int[] allowedIfds) {
+        if (sBannedDefines.contains(tagId)) {
+            return TAG_NULL;
+        }
+        if (ExifTag.isValidType(tagType) && ExifTag.isValidIfd(defaultIfd)) {
+            int tagDef = defineTag(defaultIfd, tagId);
+            if (tagDef == TAG_NULL) {
+                return TAG_NULL;
+            }
+            int[] otherDefs = getTagDefinitionsForTagId(tagId);
+            SparseIntArray infos = getTagInfo();
+            // Make sure defaultIfd is in allowedIfds
+            boolean defaultCheck = false;
+            for (int i : allowedIfds) {
+                if (defaultIfd == i) {
+                    defaultCheck = true;
+                }
+                if (!ExifTag.isValidIfd(i)) {
+                    return TAG_NULL;
+                }
+            }
+            if (!defaultCheck) {
+                return TAG_NULL;
+            }
+
+            int ifdFlags = getFlagsFromAllowedIfds(allowedIfds);
+            // Make sure no identical tags can exist in allowedIfds
+            if (otherDefs != null) {
+                for (int def : otherDefs) {
+                    int tagInfo = infos.get(def);
+                    int allowedFlags = getAllowedIfdFlagsFromInfo(tagInfo);
+                    if ((ifdFlags & allowedFlags) != 0) {
+                        return TAG_NULL;
+                    }
+                }
+            }
+            getTagInfo().put(tagDef, ifdFlags << 24 | (tagType << 16) | defaultComponentCount);
+            return tagDef;
+        }
+        return TAG_NULL;
+    }
+
+    protected int getTagDefinition(short tagId, int defaultIfd) {
+        return getTagInfo().get(defineTag(defaultIfd, tagId));
+    }
+
+    protected int[] getTagDefinitionsForTagId(short tagId) {
+        int[] ifds = IfdData.getIfds();
+        int[] defs = new int[ifds.length];
+        int counter = 0;
+        SparseIntArray infos = getTagInfo();
+        for (int i : ifds) {
+            int def = defineTag(i, tagId);
+            if (infos.get(def) != DEFINITION_NULL) {
+                defs[counter++] = def;
+            }
+        }
+        if (counter == 0) {
+            return null;
+        }
+
+        return Arrays.copyOfRange(defs, 0, counter);
+    }
+
+    protected int getTagDefinitionForTag(ExifTag tag) {
+        short type = tag.getDataType();
+        int count = tag.getComponentCount();
+        int ifd = tag.getIfd();
+        return getTagDefinitionForTag(tag.getTagId(), type, count, ifd);
+    }
+
+    protected int getTagDefinitionForTag(short tagId, short type, int count, int ifd) {
+        int[] defs = getTagDefinitionsForTagId(tagId);
+        if (defs == null) {
+            return TAG_NULL;
+        }
+        SparseIntArray infos = getTagInfo();
+        int ret = TAG_NULL;
+        for (int i : defs) {
+            int info = infos.get(i);
+            short def_type = getTypeFromInfo(info);
+            int def_count = getComponentCountFromInfo(info);
+            int[] def_ifds = getAllowedIfdsFromInfo(info);
+            boolean valid_ifd = false;
+            for (int j : def_ifds) {
+                if (j == ifd) {
+                    valid_ifd = true;
+                    break;
+                }
+            }
+            if (valid_ifd && type == def_type
+                    && (count == def_count || def_count == ExifTag.SIZE_UNDEFINED)) {
+                ret = i;
+                break;
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Removes a tag definition for given defined tag constant.
+     *
+     * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+     */
+    public void removeTagDefinition(int tagId) {
+        getTagInfo().delete(tagId);
+    }
+
+    /**
+     * Resets tag definitions to the default ones.
+     */
+    public void resetTagDefinitions() {
+        mTagInfo = null;
+    }
+
+    /**
+     * Returns the thumbnail from IFD1 as a bitmap, or null if none exists.
+     *
+     * @return the thumbnail as a bitmap.
+     */
+    public Bitmap getThumbnailBitmap() {
+        if (mData.hasCompressedThumbnail()) {
+            byte[] thumb = mData.getCompressedThumbnail();
+            return BitmapFactory.decodeByteArray(thumb, 0, thumb.length);
+        } else if (mData.hasUncompressedStrip()) {
+            // TODO: implement uncompressed
+        }
+        return null;
+    }
+
+    /**
+     * Returns the thumbnail from IFD1 as a byte array, or null if none exists.
+     * The bytes may either be an uncompressed strip as specified in the exif
+     * standard or a jpeg compressed image.
+     *
+     * @return the thumbnail as a byte array.
+     */
+    public byte[] getThumbnailBytes() {
+        if (mData.hasCompressedThumbnail()) {
+            return mData.getCompressedThumbnail();
+        } else if (mData.hasUncompressedStrip()) {
+            // TODO: implement this
+        }
+        return null;
+    }
+
+    /**
+     * Returns the thumbnail if it is jpeg compressed, or null if none exists.
+     *
+     * @return the thumbnail as a byte array.
+     */
+    public byte[] getThumbnail() {
+        return mData.getCompressedThumbnail();
+    }
+
+    /**
+     * Check if thumbnail is compressed.
+     *
+     * @return true if the thumbnail is compressed.
+     */
+    public boolean isThumbnailCompressed() {
+        return mData.hasCompressedThumbnail();
+    }
+
+    /**
+     * Check if thumbnail exists.
+     *
+     * @return true if a compressed thumbnail exists.
+     */
+    public boolean hasThumbnail() {
+        // TODO: add back in uncompressed strip
+        return mData.hasCompressedThumbnail();
+    }
+
+    // TODO: uncompressed thumbnail setters
+
+    /**
+     * Sets the thumbnail to be a jpeg compressed image. Clears any prior
+     * thumbnail.
+     *
+     * @param thumb a byte array containing a jpeg compressed image.
+     * @return true if the thumbnail was set.
+     */
+    public boolean setCompressedThumbnail(byte[] thumb) {
+        mData.clearThumbnailAndStrips();
+        mData.setCompressedThumbnail(thumb);
+        return true;
+    }
+
+    /**
+     * Sets the thumbnail to be a jpeg compressed bitmap. Clears any prior
+     * thumbnail.
+     *
+     * @param thumb a bitmap to compress to a jpeg thumbnail.
+     * @return true if the thumbnail was set.
+     */
+    public boolean setCompressedThumbnail(Bitmap thumb) {
+        ByteArrayOutputStream thumbnail = new ByteArrayOutputStream();
+        if (!thumb.compress(Bitmap.CompressFormat.JPEG, 90, thumbnail)) {
+            return false;
+        }
+        return setCompressedThumbnail(thumbnail.toByteArray());
+    }
+
+    /**
+     * Clears the compressed thumbnail if it exists.
+     */
+    public void removeCompressedThumbnail() {
+        mData.setCompressedThumbnail(null);
+    }
+
+    // Convenience methods:
+
+    /**
+     * Decodes the user comment tag into string as specified in the EXIF
+     * standard. Returns null if decoding failed.
+     */
+    public String getUserComment() {
+        return mData.getUserComment();
+    }
+
+    /**
+     * Returns the Orientation ExifTag value for a given number of degrees.
+     *
+     * @param degrees the amount an image is rotated in degrees.
+     */
+    public static short getOrientationValueForRotation(int degrees) {
+        degrees %= 360;
+        if (degrees < 0) {
+            degrees += 360;
+        }
+        if (degrees < 90) {
+            return Orientation.TOP_LEFT; // 0 degrees
+        } else if (degrees < 180) {
+            return Orientation.RIGHT_TOP; // 90 degrees cw
+        } else if (degrees < 270) {
+            return Orientation.BOTTOM_LEFT; // 180 degrees
+        } else {
+            return Orientation.RIGHT_BOTTOM; // 270 degrees cw
+        }
+    }
+
+    /**
+     * Returns the rotation degrees corresponding to an ExifTag Orientation
+     * value.
+     *
+     * @param orientation the ExifTag Orientation value.
+     */
+    public static int getRotationForOrientationValue(short orientation) {
+        switch (orientation) {
+            case Orientation.TOP_LEFT:
+                return 0;
+            case Orientation.RIGHT_TOP:
+                return 90;
+            case Orientation.BOTTOM_LEFT:
+                return 180;
+            case Orientation.RIGHT_BOTTOM:
+                return 270;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * Gets the double representation of the GPS latitude or longitude
+     * coordinate.
+     *
+     * @param coordinate an array of 3 Rationals representing the degrees,
+     *            minutes, and seconds of the GPS location as defined in the
+     *            exif specification.
+     * @param reference a GPS reference reperesented by a String containing "N",
+     *            "S", "E", or "W".
+     * @return the GPS coordinate represented as degrees + minutes/60 +
+     *         seconds/3600
+     */
+    public static double convertLatOrLongToDouble(Rational[] coordinate, String reference) {
+        try {
+            double degrees = coordinate[0].toDouble();
+            double minutes = coordinate[1].toDouble();
+            double seconds = coordinate[2].toDouble();
+            double result = degrees + minutes / 60.0 + seconds / 3600.0;
+            if ((reference.equals("S") || reference.equals("W"))) {
+                return -result;
+            }
+            return result;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Gets the GPS latitude and longitude as a pair of doubles from this
+     * ExifInterface object's tags, or null if the necessary tags do not exist.
+     *
+     * @return an array of 2 doubles containing the latitude, and longitude
+     *         respectively.
+     * @see #convertLatOrLongToDouble
+     */
+    public double[] getLatLongAsDoubles() {
+        Rational[] latitude = getTagRationalValues(TAG_GPS_LATITUDE);
+        String latitudeRef = getTagStringValue(TAG_GPS_LATITUDE_REF);
+        Rational[] longitude = getTagRationalValues(TAG_GPS_LONGITUDE);
+        String longitudeRef = getTagStringValue(TAG_GPS_LONGITUDE_REF);
+        if (latitude == null || longitude == null || latitudeRef == null || longitudeRef == null
+                || latitude.length < 3 || longitude.length < 3) {
+            return null;
+        }
+        double[] latLon = new double[2];
+        latLon[0] = convertLatOrLongToDouble(latitude, latitudeRef);
+        latLon[1] = convertLatOrLongToDouble(longitude, longitudeRef);
+        return latLon;
+    }
+
+    private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
+    private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss";
+    private final DateFormat mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR);
+    private final DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
+    private final Calendar mGPSTimeStampCalendar = Calendar
+            .getInstance(TimeZone.getTimeZone("UTC"));
+
+    /**
+     * Creates, formats, and sets the DateTimeStamp tag for one of:
+     * {@link #TAG_DATE_TIME}, {@link #TAG_DATE_TIME_DIGITIZED},
+     * {@link #TAG_DATE_TIME_ORIGINAL}.
+     *
+     * @param tagId one of the DateTimeStamp tags.
+     * @param timestamp a timestamp to format.
+     * @param timezone a TimeZone object.
+     * @return true if success, false if the tag could not be set.
+     */
+    public boolean addDateTimeStampTag(int tagId, long timestamp, TimeZone timezone) {
+        if (tagId == TAG_DATE_TIME || tagId == TAG_DATE_TIME_DIGITIZED
+                || tagId == TAG_DATE_TIME_ORIGINAL) {
+            mDateTimeStampFormat.setTimeZone(timezone);
+            ExifTag t = buildTag(tagId, mDateTimeStampFormat.format(timestamp));
+            if (t == null) {
+                return false;
+            }
+            setTag(t);
+        } else {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Creates and sets all to the GPS tags for a give latitude and longitude.
+     *
+     * @param latitude a GPS latitude coordinate.
+     * @param longitude a GPS longitude coordinate.
+     * @return true if success, false if they could not be created or set.
+     */
+    public boolean addGpsTags(double latitude, double longitude) {
+        ExifTag latTag = buildTag(TAG_GPS_LATITUDE, toExifLatLong(latitude));
+        ExifTag longTag = buildTag(TAG_GPS_LONGITUDE, toExifLatLong(longitude));
+        ExifTag latRefTag = buildTag(TAG_GPS_LATITUDE_REF,
+                latitude >= 0 ? ExifInterface.GpsLatitudeRef.NORTH
+                        : ExifInterface.GpsLatitudeRef.SOUTH);
+        ExifTag longRefTag = buildTag(TAG_GPS_LONGITUDE_REF,
+                longitude >= 0 ? ExifInterface.GpsLongitudeRef.EAST
+                        : ExifInterface.GpsLongitudeRef.WEST);
+        if (latTag == null || longTag == null || latRefTag == null || longRefTag == null) {
+            return false;
+        }
+        setTag(latTag);
+        setTag(longTag);
+        setTag(latRefTag);
+        setTag(longRefTag);
+        return true;
+    }
+
+    /**
+     * Creates and sets the GPS timestamp tag.
+     *
+     * @param timestamp a GPS timestamp.
+     * @return true if success, false if could not be created or set.
+     */
+    public boolean addGpsDateTimeStampTag(long timestamp) {
+        ExifTag t = buildTag(TAG_GPS_DATE_STAMP, mGPSDateStampFormat.format(timestamp));
+        if (t == null) {
+            return false;
+        }
+        setTag(t);
+        mGPSTimeStampCalendar.setTimeInMillis(timestamp);
+        t = buildTag(TAG_GPS_TIME_STAMP, new Rational[] {
+                new Rational(mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1),
+                new Rational(mGPSTimeStampCalendar.get(Calendar.MINUTE), 1),
+                new Rational(mGPSTimeStampCalendar.get(Calendar.SECOND), 1)
+        });
+        if (t == null) {
+            return false;
+        }
+        setTag(t);
+        return true;
+    }
+
+    private static Rational[] toExifLatLong(double value) {
+        // convert to the format dd/1 mm/1 ssss/100
+        value = Math.abs(value);
+        int degrees = (int) value;
+        value = (value - degrees) * 60;
+        int minutes = (int) value;
+        value = (value - minutes) * 6000;
+        int seconds = (int) value;
+        return new Rational[] {
+                new Rational(degrees, 1), new Rational(minutes, 1), new Rational(seconds, 100)
+        };
+    }
+
+    private void doExifStreamIO(InputStream is, OutputStream os) throws IOException {
+        byte[] buf = new byte[1024];
+        int ret = is.read(buf, 0, 1024);
+        while (ret != -1) {
+            os.write(buf, 0, ret);
+            ret = is.read(buf, 0, 1024);
+        }
+    }
+
+    protected static void closeSilently(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (Throwable e) {
+                // ignored
+            }
+        }
+    }
+
+    private SparseIntArray mTagInfo = null;
+
+    protected SparseIntArray getTagInfo() {
+        if (mTagInfo == null) {
+            mTagInfo = new SparseIntArray();
+            initTagInfo();
+        }
+        return mTagInfo;
+    }
+
+    private void initTagInfo() {
+        /**
+         * We put tag information in a 4-bytes integer. The first byte a bitmask
+         * representing the allowed IFDs of the tag, the second byte is the data
+         * type, and the last two byte are a short value indicating the default
+         * component count of this tag.
+         */
+        // IFD0 tags
+        int[] ifdAllowedIfds = {
+                IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1
+        };
+        int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_MAKE,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_WIDTH,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_LENGTH,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_BITS_PER_SAMPLE,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_COMPRESSION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16
+                | 1);
+        mTagInfo.put(ExifInterface.TAG_SAMPLES_PER_PIXEL,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PLANAR_CONFIGURATION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_Y_CB_CR_POSITIONING,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_X_RESOLUTION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_Y_RESOLUTION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_RESOLUTION_UNIT,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_ROWS_PER_STRIP,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_TRANSFER_FUNCTION,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3 * 256);
+        mTagInfo.put(ExifInterface.TAG_WHITE_POINT,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_PRIMARY_CHROMATICITIES,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6);
+        mTagInfo.put(ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_REFERENCE_BLACK_WHITE,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6);
+        mTagInfo.put(ExifInterface.TAG_DATE_TIME,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | 20);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_DESCRIPTION,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_MAKE,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_MODEL,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SOFTWARE,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_ARTIST,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_COPYRIGHT,
+                ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_EXIF_IFD,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_IFD,
+                ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        // IFD1 tags
+        int[] ifd1AllowedIfds = {
+            IfdId.TYPE_IFD_1
+        };
+        int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
+                ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+                ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        // Exif tags
+        int[] exifAllowedIfds = {
+            IfdId.TYPE_IFD_EXIF
+        };
+        int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_EXIF_VERSION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_FLASHPIX_VERSION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_COLOR_SPACE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_COMPONENTS_CONFIGURATION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PIXEL_X_DIMENSION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_PIXEL_Y_DIMENSION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_MAKER_NOTE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_USER_COMMENT,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_RELATED_SOUND_FILE,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 13);
+        mTagInfo.put(ExifInterface.TAG_DATE_TIME_ORIGINAL,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 20);
+        mTagInfo.put(ExifInterface.TAG_DATE_TIME_DIGITIZED,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 20);
+        mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_ORIGINAL,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_DIGITIZED,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_IMAGE_UNIQUE_ID,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | 33);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_TIME,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_F_NUMBER,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_PROGRAM,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SPECTRAL_SENSITIVITY,
+                exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_ISO_SPEED_RATINGS,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_OECF,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE,
+                exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_APERTURE_VALUE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_BRIGHTNESS_VALUE,
+                exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
+                exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_MAX_APERTURE_VALUE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_METERING_MODE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_LIGHT_SOURCE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FLASH,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_AREA,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_FLASH_ENERGY,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_LOCATION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_INDEX,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SENSING_METHOD,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FILE_SOURCE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SCENE_TYPE,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_CFA_PATTERN,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_CUSTOM_RENDERED,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_EXPOSURE_MODE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_WHITE_BALANCE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH_IN_35_MM_FILE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SCENE_CAPTURE_TYPE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GAIN_CONTROL,
+                exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_CONTRAST,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SATURATION,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_SHARPNESS,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
+                exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
+                exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags
+                | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+        // GPS tag
+        int[] gpsAllowedIfds = {
+            IfdId.TYPE_IFD_GPS
+        };
+        int gpsFlags = getFlagsFromAllowedIfds(gpsAllowedIfds) << 24;
+        mTagInfo.put(ExifInterface.TAG_GPS_VERSION_ID,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 4);
+        mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE,
+                gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE,
+                gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE_REF,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_TIME_STAMP,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3);
+        mTagInfo.put(ExifInterface.TAG_GPS_SATTELLITES,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_STATUS,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_MEASURE_MODE,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DOP,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_SPEED_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_SPEED,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_TRACK_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_TRACK,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_MAP_DATUM,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+        mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+        mTagInfo.put(ExifInterface.TAG_GPS_PROCESSING_METHOD,
+                gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_AREA_INFORMATION,
+                gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+        mTagInfo.put(ExifInterface.TAG_GPS_DATE_STAMP,
+                gpsFlags | ExifTag.TYPE_ASCII << 16 | 11);
+        mTagInfo.put(ExifInterface.TAG_GPS_DIFFERENTIAL,
+                gpsFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 11);
+        // Interoperability tag
+        int[] interopAllowedIfds = {
+            IfdId.TYPE_IFD_INTEROPERABILITY
+        };
+        int interopFlags = getFlagsFromAllowedIfds(interopAllowedIfds) << 24;
+        mTagInfo.put(TAG_INTEROPERABILITY_INDEX, interopFlags | ExifTag.TYPE_ASCII << 16
+                | ExifTag.SIZE_UNDEFINED);
+    }
+
+    protected static int getAllowedIfdFlagsFromInfo(int info) {
+        return info >>> 24;
+    }
+
+    protected static int[] getAllowedIfdsFromInfo(int info) {
+        int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+        int[] ifds = IfdData.getIfds();
+        ArrayList<Integer> l = new ArrayList<Integer>();
+        for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+            int flag = (ifdFlags >> i) & 1;
+            if (flag == 1) {
+                l.add(ifds[i]);
+            }
+        }
+        if (l.size() <= 0) {
+            return null;
+        }
+        int[] ret = new int[l.size()];
+        int j = 0;
+        for (int i : l) {
+            ret[j++] = i;
+        }
+        return ret;
+    }
+
+    protected static boolean isIfdAllowed(int info, int ifd) {
+        int[] ifds = IfdData.getIfds();
+        int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+        for (int i = 0; i < ifds.length; i++) {
+            if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected static int getFlagsFromAllowedIfds(int[] allowedIfds) {
+        if (allowedIfds == null || allowedIfds.length == 0) {
+            return 0;
+        }
+        int flags = 0;
+        int[] ifds = IfdData.getIfds();
+        for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+            for (int j : allowedIfds) {
+                if (ifds[i] == j) {
+                    flags |= 1 << i;
+                    break;
+                }
+            }
+        }
+        return flags;
+    }
+
+    protected static short getTypeFromInfo(int info) {
+        return (short) ((info >> 16) & 0x0ff);
+    }
+
+    protected static int getComponentCountFromInfo(int info) {
+        return info & 0x0ffff;
+    }
+
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java b/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java
new file mode 100644
index 0000000..bf923ec
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+public class ExifInvalidFormatException extends Exception {
+    public ExifInvalidFormatException(String meg) {
+        super(meg);
+    }
+}
\ No newline at end of file
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifModifier.java b/gallerycommon/src/com/android/gallery3d/exif/ExifModifier.java
new file mode 100644
index 0000000..f00362b
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifModifier.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+class ExifModifier {
+    public static final String TAG = "ExifModifier";
+    public static final boolean DEBUG = false;
+    private final ByteBuffer mByteBuffer;
+    private final ExifData mTagToModified;
+    private final List<TagOffset> mTagOffsets = new ArrayList<TagOffset>();
+    private final ExifInterface mInterface;
+    private int mOffsetBase;
+
+    private static class TagOffset {
+        final int mOffset;
+        final ExifTag mTag;
+
+        TagOffset(ExifTag tag, int offset) {
+            mTag = tag;
+            mOffset = offset;
+        }
+    }
+
+    protected ExifModifier(ByteBuffer byteBuffer, ExifInterface iRef) throws IOException,
+            ExifInvalidFormatException {
+        mByteBuffer = byteBuffer;
+        mOffsetBase = byteBuffer.position();
+        mInterface = iRef;
+        InputStream is = null;
+        try {
+            is = new ByteBufferInputStream(byteBuffer);
+            // Do not require any IFD;
+            ExifParser parser = ExifParser.parse(is, mInterface);
+            mTagToModified = new ExifData(parser.getByteOrder());
+            mOffsetBase += parser.getTiffStartPosition();
+            mByteBuffer.position(0);
+        } finally {
+            ExifInterface.closeSilently(is);
+        }
+    }
+
+    protected ByteOrder getByteOrder() {
+        return mTagToModified.getByteOrder();
+    }
+
+    protected boolean commit() throws IOException, ExifInvalidFormatException {
+        InputStream is = null;
+        try {
+            is = new ByteBufferInputStream(mByteBuffer);
+            int flag = 0;
+            IfdData[] ifdDatas = new IfdData[] {
+                    mTagToModified.getIfdData(IfdId.TYPE_IFD_0),
+                    mTagToModified.getIfdData(IfdId.TYPE_IFD_1),
+                    mTagToModified.getIfdData(IfdId.TYPE_IFD_EXIF),
+                    mTagToModified.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY),
+                    mTagToModified.getIfdData(IfdId.TYPE_IFD_GPS)
+            };
+
+            if (ifdDatas[IfdId.TYPE_IFD_0] != null) {
+                flag |= ExifParser.OPTION_IFD_0;
+            }
+            if (ifdDatas[IfdId.TYPE_IFD_1] != null) {
+                flag |= ExifParser.OPTION_IFD_1;
+            }
+            if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) {
+                flag |= ExifParser.OPTION_IFD_EXIF;
+            }
+            if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) {
+                flag |= ExifParser.OPTION_IFD_GPS;
+            }
+            if (ifdDatas[IfdId.TYPE_IFD_INTEROPERABILITY] != null) {
+                flag |= ExifParser.OPTION_IFD_INTEROPERABILITY;
+            }
+
+            ExifParser parser = ExifParser.parse(is, flag, mInterface);
+            int event = parser.next();
+            IfdData currIfd = null;
+            while (event != ExifParser.EVENT_END) {
+                switch (event) {
+                    case ExifParser.EVENT_START_OF_IFD:
+                        currIfd = ifdDatas[parser.getCurrentIfd()];
+                        if (currIfd == null) {
+                            parser.skipRemainingTagsInCurrentIfd();
+                        }
+                        break;
+                    case ExifParser.EVENT_NEW_TAG:
+                        ExifTag oldTag = parser.getTag();
+                        ExifTag newTag = currIfd.getTag(oldTag.getTagId());
+                        if (newTag != null) {
+                            if (newTag.getComponentCount() != oldTag.getComponentCount()
+                                    || newTag.getDataType() != oldTag.getDataType()) {
+                                return false;
+                            } else {
+                                mTagOffsets.add(new TagOffset(newTag, oldTag.getOffset()));
+                                currIfd.removeTag(oldTag.getTagId());
+                                if (currIfd.getTagCount() == 0) {
+                                    parser.skipRemainingTagsInCurrentIfd();
+                                }
+                            }
+                        }
+                        break;
+                }
+                event = parser.next();
+            }
+            for (IfdData ifd : ifdDatas) {
+                if (ifd != null && ifd.getTagCount() > 0) {
+                    return false;
+                }
+            }
+            modify();
+        } finally {
+            ExifInterface.closeSilently(is);
+        }
+        return true;
+    }
+
+    private void modify() {
+        mByteBuffer.order(getByteOrder());
+        for (TagOffset tagOffset : mTagOffsets) {
+            writeTagValue(tagOffset.mTag, tagOffset.mOffset);
+        }
+    }
+
+    private void writeTagValue(ExifTag tag, int offset) {
+        if (DEBUG) {
+            Log.v(TAG, "modifying tag to: \n" + tag.toString());
+            Log.v(TAG, "at offset: " + offset);
+        }
+        mByteBuffer.position(offset + mOffsetBase);
+        switch (tag.getDataType()) {
+            case ExifTag.TYPE_ASCII:
+                byte buf[] = tag.getStringByte();
+                if (buf.length == tag.getComponentCount()) {
+                    buf[buf.length - 1] = 0;
+                    mByteBuffer.put(buf);
+                } else {
+                    mByteBuffer.put(buf);
+                    mByteBuffer.put((byte) 0);
+                }
+                break;
+            case ExifTag.TYPE_LONG:
+            case ExifTag.TYPE_UNSIGNED_LONG:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    mByteBuffer.putInt((int) tag.getValueAt(i));
+                }
+                break;
+            case ExifTag.TYPE_RATIONAL:
+            case ExifTag.TYPE_UNSIGNED_RATIONAL:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    Rational v = tag.getRational(i);
+                    mByteBuffer.putInt((int) v.getNumerator());
+                    mByteBuffer.putInt((int) v.getDenominator());
+                }
+                break;
+            case ExifTag.TYPE_UNDEFINED:
+            case ExifTag.TYPE_UNSIGNED_BYTE:
+                buf = new byte[tag.getComponentCount()];
+                tag.getBytes(buf);
+                mByteBuffer.put(buf);
+                break;
+            case ExifTag.TYPE_UNSIGNED_SHORT:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    mByteBuffer.putShort((short) tag.getValueAt(i));
+                }
+                break;
+        }
+    }
+
+    public void modifyTag(ExifTag tag) {
+        mTagToModified.addTag(tag);
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java b/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java
new file mode 100644
index 0000000..7ca05f2
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.util.Log;
+
+import java.io.BufferedOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+/**
+ * This class provides a way to replace the Exif header of a JPEG image.
+ * <p>
+ * Below is an example of writing EXIF data into a file
+ *
+ * <pre>
+ * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
+ *     OutputStream os = null;
+ *     try {
+ *         os = new FileOutputStream(path);
+ *         ExifOutputStream eos = new ExifOutputStream(os);
+ *         // Set the exif header
+ *         eos.setExifData(exif);
+ *         // Write the original jpeg out, the header will be add into the file.
+ *         eos.write(jpeg);
+ *     } catch (FileNotFoundException e) {
+ *         e.printStackTrace();
+ *     } catch (IOException e) {
+ *         e.printStackTrace();
+ *     } finally {
+ *         if (os != null) {
+ *             try {
+ *                 os.close();
+ *             } catch (IOException e) {
+ *                 e.printStackTrace();
+ *             }
+ *         }
+ *     }
+ * }
+ * </pre>
+ */
+class ExifOutputStream extends FilterOutputStream {
+    private static final String TAG = "ExifOutputStream";
+    private static final boolean DEBUG = false;
+    private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
+
+    private static final int STATE_SOI = 0;
+    private static final int STATE_FRAME_HEADER = 1;
+    private static final int STATE_JPEG_DATA = 2;
+
+    private static final int EXIF_HEADER = 0x45786966;
+    private static final short TIFF_HEADER = 0x002A;
+    private static final short TIFF_BIG_ENDIAN = 0x4d4d;
+    private static final short TIFF_LITTLE_ENDIAN = 0x4949;
+    private static final short TAG_SIZE = 12;
+    private static final short TIFF_HEADER_SIZE = 8;
+    private static final int MAX_EXIF_SIZE = 65535;
+
+    private ExifData mExifData;
+    private int mState = STATE_SOI;
+    private int mByteToSkip;
+    private int mByteToCopy;
+    private byte[] mSingleByteArray = new byte[1];
+    private ByteBuffer mBuffer = ByteBuffer.allocate(4);
+    private final ExifInterface mInterface;
+
+    protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
+        super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
+        mInterface = iRef;
+    }
+
+    /**
+     * Sets the ExifData to be written into the JPEG file. Should be called
+     * before writing image data.
+     */
+    protected void setExifData(ExifData exifData) {
+        mExifData = exifData;
+    }
+
+    /**
+     * Gets the Exif header to be written into the JPEF file.
+     */
+    protected ExifData getExifData() {
+        return mExifData;
+    }
+
+    private int requestByteToBuffer(int requestByteCount, byte[] buffer
+            , int offset, int length) {
+        int byteNeeded = requestByteCount - mBuffer.position();
+        int byteToRead = length > byteNeeded ? byteNeeded : length;
+        mBuffer.put(buffer, offset, byteToRead);
+        return byteToRead;
+    }
+
+    /**
+     * Writes the image out. The input data should be a valid JPEG format. After
+     * writing, it's Exif header will be replaced by the given header.
+     */
+    @Override
+    public void write(byte[] buffer, int offset, int length) throws IOException {
+        while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
+                && length > 0) {
+            if (mByteToSkip > 0) {
+                int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
+                length -= byteToProcess;
+                mByteToSkip -= byteToProcess;
+                offset += byteToProcess;
+            }
+            if (mByteToCopy > 0) {
+                int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
+                out.write(buffer, offset, byteToProcess);
+                length -= byteToProcess;
+                mByteToCopy -= byteToProcess;
+                offset += byteToProcess;
+            }
+            if (length == 0) {
+                return;
+            }
+            switch (mState) {
+                case STATE_SOI:
+                    int byteRead = requestByteToBuffer(2, buffer, offset, length);
+                    offset += byteRead;
+                    length -= byteRead;
+                    if (mBuffer.position() < 2) {
+                        return;
+                    }
+                    mBuffer.rewind();
+                    if (mBuffer.getShort() != JpegHeader.SOI) {
+                        throw new IOException("Not a valid jpeg image, cannot write exif");
+                    }
+                    out.write(mBuffer.array(), 0, 2);
+                    mState = STATE_FRAME_HEADER;
+                    mBuffer.rewind();
+                    writeExifData();
+                    break;
+                case STATE_FRAME_HEADER:
+                    // We ignore the APP1 segment and copy all other segments
+                    // until SOF tag.
+                    byteRead = requestByteToBuffer(4, buffer, offset, length);
+                    offset += byteRead;
+                    length -= byteRead;
+                    // Check if this image data doesn't contain SOF.
+                    if (mBuffer.position() == 2) {
+                        short tag = mBuffer.getShort();
+                        if (tag == JpegHeader.EOI) {
+                            out.write(mBuffer.array(), 0, 2);
+                            mBuffer.rewind();
+                        }
+                    }
+                    if (mBuffer.position() < 4) {
+                        return;
+                    }
+                    mBuffer.rewind();
+                    short marker = mBuffer.getShort();
+                    if (marker == JpegHeader.APP1) {
+                        mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
+                        mState = STATE_JPEG_DATA;
+                    } else if (!JpegHeader.isSofMarker(marker)) {
+                        out.write(mBuffer.array(), 0, 4);
+                        mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
+                    } else {
+                        out.write(mBuffer.array(), 0, 4);
+                        mState = STATE_JPEG_DATA;
+                    }
+                    mBuffer.rewind();
+            }
+        }
+        if (length > 0) {
+            out.write(buffer, offset, length);
+        }
+    }
+
+    /**
+     * Writes the one bytes out. The input data should be a valid JPEG format.
+     * After writing, it's Exif header will be replaced by the given header.
+     */
+    @Override
+    public void write(int oneByte) throws IOException {
+        mSingleByteArray[0] = (byte) (0xff & oneByte);
+        write(mSingleByteArray);
+    }
+
+    /**
+     * Equivalent to calling write(buffer, 0, buffer.length).
+     */
+    @Override
+    public void write(byte[] buffer) throws IOException {
+        write(buffer, 0, buffer.length);
+    }
+
+    private void writeExifData() throws IOException {
+        if (mExifData == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "Writing exif data...");
+        }
+        ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
+        createRequiredIfdAndTag();
+        int exifSize = calculateAllOffset();
+        if (exifSize + 8 > MAX_EXIF_SIZE) {
+            throw new IOException("Exif header is too large (>64Kb)");
+        }
+        OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
+        dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+        dataOutputStream.writeShort(JpegHeader.APP1);
+        dataOutputStream.writeShort((short) (exifSize + 8));
+        dataOutputStream.writeInt(EXIF_HEADER);
+        dataOutputStream.writeShort((short) 0x0000);
+        if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
+            dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
+        } else {
+            dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
+        }
+        dataOutputStream.setByteOrder(mExifData.getByteOrder());
+        dataOutputStream.writeShort(TIFF_HEADER);
+        dataOutputStream.writeInt(8);
+        writeAllTags(dataOutputStream);
+        writeThumbnail(dataOutputStream);
+        for (ExifTag t : nullTags) {
+            mExifData.addTag(t);
+        }
+    }
+
+    private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
+        ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
+        for(ExifTag t : data.getAllTags()) {
+            if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
+                data.removeTag(t.getTagId(), t.getIfd());
+                nullTags.add(t);
+            }
+        }
+        return nullTags;
+    }
+
+    private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
+        if (mExifData.hasCompressedThumbnail()) {
+            dataOutputStream.write(mExifData.getCompressedThumbnail());
+        } else if (mExifData.hasUncompressedStrip()) {
+            for (int i = 0; i < mExifData.getStripCount(); i++) {
+                dataOutputStream.write(mExifData.getStrip(i));
+            }
+        }
+    }
+
+    private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
+        writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
+        writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
+        IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
+        if (interoperabilityIfd != null) {
+            writeIfd(interoperabilityIfd, dataOutputStream);
+        }
+        IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
+        if (gpsIfd != null) {
+            writeIfd(gpsIfd, dataOutputStream);
+        }
+        IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
+        if (ifd1 != null) {
+            writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
+        }
+    }
+
+    private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
+            throws IOException {
+        ExifTag[] tags = ifd.getAllTags();
+        dataOutputStream.writeShort((short) tags.length);
+        for (ExifTag tag : tags) {
+            dataOutputStream.writeShort(tag.getTagId());
+            dataOutputStream.writeShort(tag.getDataType());
+            dataOutputStream.writeInt(tag.getComponentCount());
+            if (DEBUG) {
+                Log.v(TAG, "\n" + tag.toString());
+            }
+            if (tag.getDataSize() > 4) {
+                dataOutputStream.writeInt(tag.getOffset());
+            } else {
+                ExifOutputStream.writeTagValue(tag, dataOutputStream);
+                for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
+                    dataOutputStream.write(0);
+                }
+            }
+        }
+        dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
+        for (ExifTag tag : tags) {
+            if (tag.getDataSize() > 4) {
+                ExifOutputStream.writeTagValue(tag, dataOutputStream);
+            }
+        }
+    }
+
+    private int calculateOffsetOfIfd(IfdData ifd, int offset) {
+        offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
+        ExifTag[] tags = ifd.getAllTags();
+        for (ExifTag tag : tags) {
+            if (tag.getDataSize() > 4) {
+                tag.setOffset(offset);
+                offset += tag.getDataSize();
+            }
+        }
+        return offset;
+    }
+
+    private void createRequiredIfdAndTag() throws IOException {
+        // IFD0 is required for all file
+        IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
+        if (ifd0 == null) {
+            ifd0 = new IfdData(IfdId.TYPE_IFD_0);
+            mExifData.addIfdData(ifd0);
+        }
+        ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
+        if (exifOffsetTag == null) {
+            throw new IOException("No definition for crucial exif tag: "
+                    + ExifInterface.TAG_EXIF_IFD);
+        }
+        ifd0.setTag(exifOffsetTag);
+
+        // Exif IFD is required for all files.
+        IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
+        if (exifIfd == null) {
+            exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
+            mExifData.addIfdData(exifIfd);
+        }
+
+        // GPS IFD
+        IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
+        if (gpsIfd != null) {
+            ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
+            if (gpsOffsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_GPS_IFD);
+            }
+            ifd0.setTag(gpsOffsetTag);
+        }
+
+        // Interoperability IFD
+        IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
+        if (interIfd != null) {
+            ExifTag interOffsetTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
+            if (interOffsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_INTEROPERABILITY_IFD);
+            }
+            exifIfd.setTag(interOffsetTag);
+        }
+
+        IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
+
+        // thumbnail
+        if (mExifData.hasCompressedThumbnail()) {
+
+            if (ifd1 == null) {
+                ifd1 = new IfdData(IfdId.TYPE_IFD_1);
+                mExifData.addIfdData(ifd1);
+            }
+
+            ExifTag offsetTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+            if (offsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+            }
+
+            ifd1.setTag(offsetTag);
+            ExifTag lengthTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+            if (lengthTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+            }
+
+            lengthTag.setValue(mExifData.getCompressedThumbnail().length);
+            ifd1.setTag(lengthTag);
+
+            // Get rid of tags for uncompressed if they exist.
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
+        } else if (mExifData.hasUncompressedStrip()) {
+            if (ifd1 == null) {
+                ifd1 = new IfdData(IfdId.TYPE_IFD_1);
+                mExifData.addIfdData(ifd1);
+            }
+            int stripCount = mExifData.getStripCount();
+            ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
+            if (offsetTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_STRIP_OFFSETS);
+            }
+            ExifTag lengthTag = mInterface
+                    .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+            if (lengthTag == null) {
+                throw new IOException("No definition for crucial exif tag: "
+                        + ExifInterface.TAG_STRIP_BYTE_COUNTS);
+            }
+            long[] lengths = new long[stripCount];
+            for (int i = 0; i < mExifData.getStripCount(); i++) {
+                lengths[i] = mExifData.getStrip(i).length;
+            }
+            lengthTag.setValue(lengths);
+            ifd1.setTag(offsetTag);
+            ifd1.setTag(lengthTag);
+            // Get rid of tags for compressed if they exist.
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
+            ifd1.removeTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+        } else if (ifd1 != null) {
+            // Get rid of offset and length tags if there is no thumbnail.
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
+            ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
+            ifd1.removeTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+        }
+    }
+
+    private int calculateAllOffset() {
+        int offset = TIFF_HEADER_SIZE;
+        IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
+        offset = calculateOffsetOfIfd(ifd0, offset);
+        ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
+
+        IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
+        offset = calculateOffsetOfIfd(exifIfd, offset);
+
+        IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
+        if (interIfd != null) {
+            exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
+                    .setValue(offset);
+            offset = calculateOffsetOfIfd(interIfd, offset);
+        }
+
+        IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
+        if (gpsIfd != null) {
+            ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
+            offset = calculateOffsetOfIfd(gpsIfd, offset);
+        }
+
+        IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
+        if (ifd1 != null) {
+            ifd0.setOffsetToNextIfd(offset);
+            offset = calculateOffsetOfIfd(ifd1, offset);
+        }
+
+        // thumbnail
+        if (mExifData.hasCompressedThumbnail()) {
+            ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
+                    .setValue(offset);
+            offset += mExifData.getCompressedThumbnail().length;
+        } else if (mExifData.hasUncompressedStrip()) {
+            int stripCount = mExifData.getStripCount();
+            long[] offsets = new long[stripCount];
+            for (int i = 0; i < mExifData.getStripCount(); i++) {
+                offsets[i] = offset;
+                offset += mExifData.getStrip(i).length;
+            }
+            ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
+                    offsets);
+        }
+        return offset;
+    }
+
+    static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
+            throws IOException {
+        switch (tag.getDataType()) {
+            case ExifTag.TYPE_ASCII:
+                byte buf[] = tag.getStringByte();
+                if (buf.length == tag.getComponentCount()) {
+                    buf[buf.length - 1] = 0;
+                    dataOutputStream.write(buf);
+                } else {
+                    dataOutputStream.write(buf);
+                    dataOutputStream.write(0);
+                }
+                break;
+            case ExifTag.TYPE_LONG:
+            case ExifTag.TYPE_UNSIGNED_LONG:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    dataOutputStream.writeInt((int) tag.getValueAt(i));
+                }
+                break;
+            case ExifTag.TYPE_RATIONAL:
+            case ExifTag.TYPE_UNSIGNED_RATIONAL:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    dataOutputStream.writeRational(tag.getRational(i));
+                }
+                break;
+            case ExifTag.TYPE_UNDEFINED:
+            case ExifTag.TYPE_UNSIGNED_BYTE:
+                buf = new byte[tag.getComponentCount()];
+                tag.getBytes(buf);
+                dataOutputStream.write(buf);
+                break;
+            case ExifTag.TYPE_UNSIGNED_SHORT:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    dataOutputStream.writeShort((short) tag.getValueAt(i));
+                }
+                break;
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java b/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java
new file mode 100644
index 0000000..5467d42
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java
@@ -0,0 +1,916 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * This class provides a low-level EXIF parsing API. Given a JPEG format
+ * InputStream, the caller can request which IFD's to read via
+ * {@link #parse(InputStream, int)} with given options.
+ * <p>
+ * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the
+ * parser.
+ *
+ * <pre>
+ * void parse() {
+ *     ExifParser parser = ExifParser.parse(mImageInputStream,
+ *             ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
+ *     int event = parser.next();
+ *     while (event != ExifParser.EVENT_END) {
+ *         switch (event) {
+ *             case ExifParser.EVENT_START_OF_IFD:
+ *                 break;
+ *             case ExifParser.EVENT_NEW_TAG:
+ *                 ExifTag tag = parser.getTag();
+ *                 if (!tag.hasValue()) {
+ *                     parser.registerForTagValue(tag);
+ *                 } else {
+ *                     processTag(tag);
+ *                 }
+ *                 break;
+ *             case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ *                 tag = parser.getTag();
+ *                 if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ *                     processTag(tag);
+ *                 }
+ *                 break;
+ *         }
+ *         event = parser.next();
+ *     }
+ * }
+ *
+ * void processTag(ExifTag tag) {
+ *     // process the tag as you like.
+ * }
+ * </pre>
+ */
+class ExifParser {
+    private static final boolean LOGV = false;
+    private static final String TAG = "ExifParser";
+    /**
+     * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to
+     * know which IFD we are in.
+     */
+    public static final int EVENT_START_OF_IFD = 0;
+    /**
+     * When the parser reaches a new tag. Call {@link #getTag()}to get the
+     * corresponding tag.
+     */
+    public static final int EVENT_NEW_TAG = 1;
+    /**
+     * When the parser reaches the value area of tag that is registered by
+     * {@link #registerForTagValue(ExifTag)} previously. Call {@link #getTag()}
+     * to get the corresponding tag.
+     */
+    public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
+
+    /**
+     * When the parser reaches the compressed image area.
+     */
+    public static final int EVENT_COMPRESSED_IMAGE = 3;
+    /**
+     * When the parser reaches the uncompressed image strip. Call
+     * {@link #getStripIndex()} to get the index of the strip.
+     *
+     * @see #getStripIndex()
+     * @see #getStripCount()
+     */
+    public static final int EVENT_UNCOMPRESSED_STRIP = 4;
+    /**
+     * When there is nothing more to parse.
+     */
+    public static final int EVENT_END = 5;
+
+    /**
+     * Option bit to request to parse IFD0.
+     */
+    public static final int OPTION_IFD_0 = 1 << 0;
+    /**
+     * Option bit to request to parse IFD1.
+     */
+    public static final int OPTION_IFD_1 = 1 << 1;
+    /**
+     * Option bit to request to parse Exif-IFD.
+     */
+    public static final int OPTION_IFD_EXIF = 1 << 2;
+    /**
+     * Option bit to request to parse GPS-IFD.
+     */
+    public static final int OPTION_IFD_GPS = 1 << 3;
+    /**
+     * Option bit to request to parse Interoperability-IFD.
+     */
+    public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
+    /**
+     * Option bit to request to parse thumbnail.
+     */
+    public static final int OPTION_THUMBNAIL = 1 << 5;
+
+    protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
+    protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
+
+    // TIFF header
+    protected static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
+    protected static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
+    protected static final short TIFF_HEADER_TAIL = 0x002A;
+
+    protected static final int TAG_SIZE = 12;
+    protected static final int OFFSET_SIZE = 2;
+
+    private static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+    protected static final int DEFAULT_IFD0_OFFSET = 8;
+
+    private final CountedDataInputStream mTiffStream;
+    private final int mOptions;
+    private int mIfdStartOffset = 0;
+    private int mNumOfTagInIfd = 0;
+    private int mIfdType;
+    private ExifTag mTag;
+    private ImageEvent mImageEvent;
+    private int mStripCount;
+    private ExifTag mStripSizeTag;
+    private ExifTag mJpegSizeTag;
+    private boolean mNeedToParseOffsetsInCurrentIfd;
+    private boolean mContainExifData = false;
+    private int mApp1End;
+    private int mOffsetToApp1EndFromSOF = 0;
+    private byte[] mDataAboveIfd0;
+    private int mIfd0Position;
+    private int mTiffStartPosition;
+    private final ExifInterface mInterface;
+
+    private static final short TAG_EXIF_IFD = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
+    private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
+    private static final short TAG_INTEROPERABILITY_IFD = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
+    private static final short TAG_JPEG_INTERCHANGE_FORMAT = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+    private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+    private static final short TAG_STRIP_OFFSETS = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
+    private static final short TAG_STRIP_BYTE_COUNTS = ExifInterface
+            .getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+
+    private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>();
+
+    private boolean isIfdRequested(int ifdType) {
+        switch (ifdType) {
+            case IfdId.TYPE_IFD_0:
+                return (mOptions & OPTION_IFD_0) != 0;
+            case IfdId.TYPE_IFD_1:
+                return (mOptions & OPTION_IFD_1) != 0;
+            case IfdId.TYPE_IFD_EXIF:
+                return (mOptions & OPTION_IFD_EXIF) != 0;
+            case IfdId.TYPE_IFD_GPS:
+                return (mOptions & OPTION_IFD_GPS) != 0;
+            case IfdId.TYPE_IFD_INTEROPERABILITY:
+                return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
+        }
+        return false;
+    }
+
+    private boolean isThumbnailRequested() {
+        return (mOptions & OPTION_THUMBNAIL) != 0;
+    }
+
+    private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
+            throws IOException, ExifInvalidFormatException {
+        if (inputStream == null) {
+            throw new IOException("Null argument inputStream to ExifParser");
+        }
+        if (LOGV) {
+            Log.v(TAG, "Reading exif...");
+        }
+        mInterface = iRef;
+        mContainExifData = seekTiffData(inputStream);
+        mTiffStream = new CountedDataInputStream(inputStream);
+        mOptions = options;
+        if (!mContainExifData) {
+            return;
+        }
+
+        parseTiffHeader();
+        long offset = mTiffStream.readUnsignedInt();
+        if (offset > Integer.MAX_VALUE) {
+            throw new ExifInvalidFormatException("Invalid offset " + offset);
+        }
+        mIfd0Position = (int) offset;
+        mIfdType = IfdId.TYPE_IFD_0;
+        if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
+            registerIfd(IfdId.TYPE_IFD_0, offset);
+            if (offset != DEFAULT_IFD0_OFFSET) {
+                mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
+                read(mDataAboveIfd0);
+            }
+        }
+    }
+
+    /**
+     * Parses the the given InputStream with the given options
+     *
+     * @exception IOException
+     * @exception ExifInvalidFormatException
+     */
+    protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
+            throws IOException, ExifInvalidFormatException {
+        return new ExifParser(inputStream, options, iRef);
+    }
+
+    /**
+     * Parses the the given InputStream with default options; that is, every IFD
+     * and thumbnaill will be parsed.
+     *
+     * @exception IOException
+     * @exception ExifInvalidFormatException
+     * @see #parse(InputStream, int)
+     */
+    protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
+            throws IOException, ExifInvalidFormatException {
+        return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1
+                | OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY
+                | OPTION_THUMBNAIL, iRef);
+    }
+
+    /**
+     * Moves the parser forward and returns the next parsing event
+     *
+     * @exception IOException
+     * @exception ExifInvalidFormatException
+     * @see #EVENT_START_OF_IFD
+     * @see #EVENT_NEW_TAG
+     * @see #EVENT_VALUE_OF_REGISTERED_TAG
+     * @see #EVENT_COMPRESSED_IMAGE
+     * @see #EVENT_UNCOMPRESSED_STRIP
+     * @see #EVENT_END
+     */
+    protected int next() throws IOException, ExifInvalidFormatException {
+        if (!mContainExifData) {
+            return EVENT_END;
+        }
+        int offset = mTiffStream.getReadByteCount();
+        int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+        if (offset < endOfTags) {
+            mTag = readTag();
+            if (mTag == null) {
+                return next();
+            }
+            if (mNeedToParseOffsetsInCurrentIfd) {
+                checkOffsetOrImageTag(mTag);
+            }
+            return EVENT_NEW_TAG;
+        } else if (offset == endOfTags) {
+            // There is a link to ifd1 at the end of ifd0
+            if (mIfdType == IfdId.TYPE_IFD_0) {
+                long ifdOffset = readUnsignedLong();
+                if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
+                    if (ifdOffset != 0) {
+                        registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+                    }
+                }
+            } else {
+                int offsetSize = 4;
+                // Some camera models use invalid length of the offset
+                if (mCorrespondingEvent.size() > 0) {
+                    offsetSize = mCorrespondingEvent.firstEntry().getKey() -
+                            mTiffStream.getReadByteCount();
+                }
+                if (offsetSize < 4) {
+                    Log.w(TAG, "Invalid size of link to next IFD: " + offsetSize);
+                } else {
+                    long ifdOffset = readUnsignedLong();
+                    if (ifdOffset != 0) {
+                        Log.w(TAG, "Invalid link to next IFD: " + ifdOffset);
+                    }
+                }
+            }
+        }
+        while (mCorrespondingEvent.size() != 0) {
+            Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+            Object event = entry.getValue();
+            try {
+                skipTo(entry.getKey());
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to skip to data at: " + entry.getKey() +
+                        " for " + event.getClass().getName() + ", the file may be broken.");
+                continue;
+            }
+            if (event instanceof IfdEvent) {
+                mIfdType = ((IfdEvent) event).ifd;
+                mNumOfTagInIfd = mTiffStream.readUnsignedShort();
+                mIfdStartOffset = entry.getKey();
+
+                if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
+                    Log.w(TAG, "Invalid size of IFD " + mIfdType);
+                    return EVENT_END;
+                }
+
+                mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
+                if (((IfdEvent) event).isRequested) {
+                    return EVENT_START_OF_IFD;
+                } else {
+                    skipRemainingTagsInCurrentIfd();
+                }
+            } else if (event instanceof ImageEvent) {
+                mImageEvent = (ImageEvent) event;
+                return mImageEvent.type;
+            } else {
+                ExifTagEvent tagEvent = (ExifTagEvent) event;
+                mTag = tagEvent.tag;
+                if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+                    readFullTagValue(mTag);
+                    checkOffsetOrImageTag(mTag);
+                }
+                if (tagEvent.isRequested) {
+                    return EVENT_VALUE_OF_REGISTERED_TAG;
+                }
+            }
+        }
+        return EVENT_END;
+    }
+
+    /**
+     * Skips the tags area of current IFD, if the parser is not in the tag area,
+     * nothing will happen.
+     *
+     * @throws IOException
+     * @throws ExifInvalidFormatException
+     */
+    protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
+        int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+        int offset = mTiffStream.getReadByteCount();
+        if (offset > endOfTags) {
+            return;
+        }
+        if (mNeedToParseOffsetsInCurrentIfd) {
+            while (offset < endOfTags) {
+                mTag = readTag();
+                offset += TAG_SIZE;
+                if (mTag == null) {
+                    continue;
+                }
+                checkOffsetOrImageTag(mTag);
+            }
+        } else {
+            skipTo(endOfTags);
+        }
+        long ifdOffset = readUnsignedLong();
+        // For ifd0, there is a link to ifd1 in the end of all tags
+        if (mIfdType == IfdId.TYPE_IFD_0
+                && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
+            if (ifdOffset > 0) {
+                registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+            }
+        }
+    }
+
+    private boolean needToParseOffsetsInCurrentIfd() {
+        switch (mIfdType) {
+            case IfdId.TYPE_IFD_0:
+                return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS)
+                        || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
+                        || isIfdRequested(IfdId.TYPE_IFD_1);
+            case IfdId.TYPE_IFD_1:
+                return isThumbnailRequested();
+            case IfdId.TYPE_IFD_EXIF:
+                // The offset to interoperability IFD is located in Exif IFD
+                return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * If {@link #next()} return {@link #EVENT_NEW_TAG} or
+     * {@link #EVENT_VALUE_OF_REGISTERED_TAG}, call this function to get the
+     * corresponding tag.
+     * <p>
+     * For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size
+     * of the value is greater than 4 bytes. One should call
+     * {@link ExifTag#hasValue()} to check if the tag contains value. If there
+     * is no value,call {@link #registerForTagValue(ExifTag)} to have the parser
+     * emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
+     * pointed by the offset.
+     * <p>
+     * When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the
+     * tag will have already been read except for tags of undefined type. For
+     * tags of undefined type, call one of the read methods to get the value.
+     *
+     * @see #registerForTagValue(ExifTag)
+     * @see #read(byte[])
+     * @see #read(byte[], int, int)
+     * @see #readLong()
+     * @see #readRational()
+     * @see #readString(int)
+     * @see #readString(int, Charset)
+     */
+    protected ExifTag getTag() {
+        return mTag;
+    }
+
+    /**
+     * Gets number of tags in the current IFD area.
+     */
+    protected int getTagCountInCurrentIfd() {
+        return mNumOfTagInIfd;
+    }
+
+    /**
+     * Gets the ID of current IFD.
+     *
+     * @see IfdId#TYPE_IFD_0
+     * @see IfdId#TYPE_IFD_1
+     * @see IfdId#TYPE_IFD_GPS
+     * @see IfdId#TYPE_IFD_INTEROPERABILITY
+     * @see IfdId#TYPE_IFD_EXIF
+     */
+    protected int getCurrentIfd() {
+        return mIfdType;
+    }
+
+    /**
+     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+     * get the index of this strip.
+     *
+     * @see #getStripCount()
+     */
+    protected int getStripIndex() {
+        return mImageEvent.stripIndex;
+    }
+
+    /**
+     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+     * get the number of strip data.
+     *
+     * @see #getStripIndex()
+     */
+    protected int getStripCount() {
+        return mStripCount;
+    }
+
+    /**
+     * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+     * get the strip size.
+     */
+    protected int getStripSize() {
+        if (mStripSizeTag == null)
+            return 0;
+        return (int) mStripSizeTag.getValueAt(0);
+    }
+
+    /**
+     * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get
+     * the image data size.
+     */
+    protected int getCompressedImageSize() {
+        if (mJpegSizeTag == null) {
+            return 0;
+        }
+        return (int) mJpegSizeTag.getValueAt(0);
+    }
+
+    private void skipTo(int offset) throws IOException {
+        mTiffStream.skipTo(offset);
+        while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
+            mCorrespondingEvent.pollFirstEntry();
+        }
+    }
+
+    /**
+     * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may
+     * not contain the value if the size of the value is greater than 4 bytes.
+     * When the value is not available here, call this method so that the parser
+     * will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
+     * where the value is located.
+     *
+     * @see #EVENT_VALUE_OF_REGISTERED_TAG
+     */
+    protected void registerForTagValue(ExifTag tag) {
+        if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
+            mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
+        }
+    }
+
+    private void registerIfd(int ifdType, long offset) {
+        // Cast unsigned int to int since the offset is always smaller
+        // than the size of APP1 (65536)
+        mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
+    }
+
+    private void registerCompressedImage(long offset) {
+        mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
+    }
+
+    private void registerUncompressedStrip(int stripIndex, long offset) {
+        mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP
+                , stripIndex));
+    }
+
+    private ExifTag readTag() throws IOException, ExifInvalidFormatException {
+        short tagId = mTiffStream.readShort();
+        short dataFormat = mTiffStream.readShort();
+        long numOfComp = mTiffStream.readUnsignedInt();
+        if (numOfComp > Integer.MAX_VALUE) {
+            throw new ExifInvalidFormatException(
+                    "Number of component is larger then Integer.MAX_VALUE");
+        }
+        // Some invalid image file contains invalid data type. Ignore those tags
+        if (!ExifTag.isValidType(dataFormat)) {
+            Log.w(TAG, String.format("Tag %04x: Invalid data type %d", tagId, dataFormat));
+            mTiffStream.skip(4);
+            return null;
+        }
+        // TODO: handle numOfComp overflow
+        ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType,
+                ((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
+        int dataSize = tag.getDataSize();
+        if (dataSize > 4) {
+            long offset = mTiffStream.readUnsignedInt();
+            if (offset > Integer.MAX_VALUE) {
+                throw new ExifInvalidFormatException(
+                        "offset is larger then Integer.MAX_VALUE");
+            }
+            // Some invalid images put some undefined data before IFD0.
+            // Read the data here.
+            if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
+                byte[] buf = new byte[(int) numOfComp];
+                System.arraycopy(mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET,
+                        buf, 0, (int) numOfComp);
+                tag.setValue(buf);
+            } else {
+                tag.setOffset((int) offset);
+            }
+        } else {
+            boolean defCount = tag.hasDefinedCount();
+            // Set defined count to 0 so we can add \0 to non-terminated strings
+            tag.setHasDefinedCount(false);
+            // Read value
+            readFullTagValue(tag);
+            tag.setHasDefinedCount(defCount);
+            mTiffStream.skip(4 - dataSize);
+            // Set the offset to the position of value.
+            tag.setOffset(mTiffStream.getReadByteCount() - 4);
+        }
+        return tag;
+    }
+
+    /**
+     * Check the tag, if the tag is one of the offset tag that points to the IFD
+     * or image the caller is interested in, register the IFD or image.
+     */
+    private void checkOffsetOrImageTag(ExifTag tag) {
+        // Some invalid formattd image contains tag with 0 size.
+        if (tag.getComponentCount() == 0) {
+            return;
+        }
+        short tid = tag.getTagId();
+        int ifd = tag.getIfd();
+        if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
+            if (isIfdRequested(IfdId.TYPE_IFD_EXIF)
+                    || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+                registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
+            }
+        } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
+            if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
+                registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
+            }
+        } else if (tid == TAG_INTEROPERABILITY_IFD
+                && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
+            if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+                registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
+            }
+        } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
+                && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
+            if (isThumbnailRequested()) {
+                registerCompressedImage(tag.getValueAt(0));
+            }
+        } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
+                && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
+            if (isThumbnailRequested()) {
+                mJpegSizeTag = tag;
+            }
+        } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
+            if (isThumbnailRequested()) {
+                if (tag.hasValue()) {
+                    for (int i = 0; i < tag.getComponentCount(); i++) {
+                        if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
+                            registerUncompressedStrip(i, tag.getValueAt(i));
+                        } else {
+                            registerUncompressedStrip(i, tag.getValueAt(i));
+                        }
+                    }
+                } else {
+                    mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
+                }
+            }
+        } else if (tid == TAG_STRIP_BYTE_COUNTS
+                && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
+                &&isThumbnailRequested() && tag.hasValue()) {
+            mStripSizeTag = tag;
+        }
+    }
+
+    private boolean checkAllowed(int ifd, int tagId) {
+        int info = mInterface.getTagInfo().get(tagId);
+        if (info == ExifInterface.DEFINITION_NULL) {
+            return false;
+        }
+        return ExifInterface.isIfdAllowed(info, ifd);
+    }
+
+    protected void readFullTagValue(ExifTag tag) throws IOException {
+        // Some invalid images contains tags with wrong size, check it here
+        short type = tag.getDataType();
+        if (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED ||
+                type == ExifTag.TYPE_UNSIGNED_BYTE) {
+            int size = tag.getComponentCount();
+            if (mCorrespondingEvent.size() > 0) {
+                if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount()
+                        + size) {
+                    Object event = mCorrespondingEvent.firstEntry().getValue();
+                    if (event instanceof ImageEvent) {
+                        // Tag value overlaps thumbnail, ignore thumbnail.
+                        Log.w(TAG, "Thumbnail overlaps value for tag: \n" + tag.toString());
+                        Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+                        Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey());
+                    } else {
+                        // Tag value overlaps another tag, shorten count
+                        if (event instanceof IfdEvent) {
+                            Log.w(TAG, "Ifd " + ((IfdEvent) event).ifd
+                                    + " overlaps value for tag: \n" + tag.toString());
+                        } else if (event instanceof ExifTagEvent) {
+                            Log.w(TAG, "Tag value for tag: \n"
+                                    + ((ExifTagEvent) event).tag.toString()
+                                    + " overlaps value for tag: \n" + tag.toString());
+                        }
+                        size = mCorrespondingEvent.firstEntry().getKey()
+                                - mTiffStream.getReadByteCount();
+                        Log.w(TAG, "Invalid size of tag: \n" + tag.toString()
+                                + " setting count to: " + size);
+                        tag.forceSetComponentCount(size);
+                    }
+                }
+            }
+        }
+        switch (tag.getDataType()) {
+            case ExifTag.TYPE_UNSIGNED_BYTE:
+            case ExifTag.TYPE_UNDEFINED: {
+                byte buf[] = new byte[tag.getComponentCount()];
+                read(buf);
+                tag.setValue(buf);
+            }
+                break;
+            case ExifTag.TYPE_ASCII:
+                tag.setValue(readString(tag.getComponentCount()));
+                break;
+            case ExifTag.TYPE_UNSIGNED_LONG: {
+                long value[] = new long[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readUnsignedLong();
+                }
+                tag.setValue(value);
+            }
+                break;
+            case ExifTag.TYPE_UNSIGNED_RATIONAL: {
+                Rational value[] = new Rational[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readUnsignedRational();
+                }
+                tag.setValue(value);
+            }
+                break;
+            case ExifTag.TYPE_UNSIGNED_SHORT: {
+                int value[] = new int[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readUnsignedShort();
+                }
+                tag.setValue(value);
+            }
+                break;
+            case ExifTag.TYPE_LONG: {
+                int value[] = new int[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readLong();
+                }
+                tag.setValue(value);
+            }
+                break;
+            case ExifTag.TYPE_RATIONAL: {
+                Rational value[] = new Rational[tag.getComponentCount()];
+                for (int i = 0, n = value.length; i < n; i++) {
+                    value[i] = readRational();
+                }
+                tag.setValue(value);
+            }
+                break;
+        }
+        if (LOGV) {
+            Log.v(TAG, "\n" + tag.toString());
+        }
+    }
+
+    private void parseTiffHeader() throws IOException,
+            ExifInvalidFormatException {
+        short byteOrder = mTiffStream.readShort();
+        if (LITTLE_ENDIAN_TAG == byteOrder) {
+            mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+        } else if (BIG_ENDIAN_TAG == byteOrder) {
+            mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+        } else {
+            throw new ExifInvalidFormatException("Invalid TIFF header");
+        }
+
+        if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
+            throw new ExifInvalidFormatException("Invalid TIFF header");
+        }
+    }
+
+    private boolean seekTiffData(InputStream inputStream) throws IOException,
+            ExifInvalidFormatException {
+        CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
+        if (dataStream.readShort() != JpegHeader.SOI) {
+            throw new ExifInvalidFormatException("Invalid JPEG format");
+        }
+
+        short marker = dataStream.readShort();
+        while (marker != JpegHeader.EOI
+                && !JpegHeader.isSofMarker(marker)) {
+            int length = dataStream.readUnsignedShort();
+            // Some invalid formatted image contains multiple APP1,
+            // try to find the one with Exif data.
+            if (marker == JpegHeader.APP1) {
+                int header = 0;
+                short headerTail = 0;
+                if (length >= 8) {
+                    header = dataStream.readInt();
+                    headerTail = dataStream.readShort();
+                    length -= 6;
+                    if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
+                        mTiffStartPosition = dataStream.getReadByteCount();
+                        mApp1End = length;
+                        mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End;
+                        return true;
+                    }
+                }
+            }
+            if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
+                Log.w(TAG, "Invalid JPEG format.");
+                return false;
+            }
+            marker = dataStream.readShort();
+        }
+        return false;
+    }
+
+    protected int getOffsetToExifEndFromSOF() {
+        return mOffsetToApp1EndFromSOF;
+    }
+
+    protected int getTiffStartPosition() {
+        return mTiffStartPosition;
+    }
+
+    /**
+     * Reads bytes from the InputStream.
+     */
+    protected int read(byte[] buffer, int offset, int length) throws IOException {
+        return mTiffStream.read(buffer, offset, length);
+    }
+
+    /**
+     * Equivalent to read(buffer, 0, buffer.length).
+     */
+    protected int read(byte[] buffer) throws IOException {
+        return mTiffStream.read(buffer);
+    }
+
+    /**
+     * Reads a String from the InputStream with US-ASCII charset. The parser
+     * will read n bytes and convert it to ascii string. This is used for
+     * reading values of type {@link ExifTag#TYPE_ASCII}.
+     */
+    protected String readString(int n) throws IOException {
+        return readString(n, US_ASCII);
+    }
+
+    /**
+     * Reads a String from the InputStream with the given charset. The parser
+     * will read n bytes and convert it to string. This is used for reading
+     * values of type {@link ExifTag#TYPE_ASCII}.
+     */
+    protected String readString(int n, Charset charset) throws IOException {
+        if (n > 0) {
+            return mTiffStream.readString(n, charset);
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the
+     * InputStream.
+     */
+    protected int readUnsignedShort() throws IOException {
+        return mTiffStream.readShort() & 0xffff;
+    }
+
+    /**
+     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the
+     * InputStream.
+     */
+    protected long readUnsignedLong() throws IOException {
+        return readLong() & 0xffffffffL;
+    }
+
+    /**
+     * Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the
+     * InputStream.
+     */
+    protected Rational readUnsignedRational() throws IOException {
+        long nomi = readUnsignedLong();
+        long denomi = readUnsignedLong();
+        return new Rational(nomi, denomi);
+    }
+
+    /**
+     * Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream.
+     */
+    protected int readLong() throws IOException {
+        return mTiffStream.readInt();
+    }
+
+    /**
+     * Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream.
+     */
+    protected Rational readRational() throws IOException {
+        int nomi = readLong();
+        int denomi = readLong();
+        return new Rational(nomi, denomi);
+    }
+
+    private static class ImageEvent {
+        int stripIndex;
+        int type;
+
+        ImageEvent(int type) {
+            this.stripIndex = 0;
+            this.type = type;
+        }
+
+        ImageEvent(int type, int stripIndex) {
+            this.type = type;
+            this.stripIndex = stripIndex;
+        }
+    }
+
+    private static class IfdEvent {
+        int ifd;
+        boolean isRequested;
+
+        IfdEvent(int ifd, boolean isInterestedIfd) {
+            this.ifd = ifd;
+            this.isRequested = isInterestedIfd;
+        }
+    }
+
+    private static class ExifTagEvent {
+        ExifTag tag;
+        boolean isRequested;
+
+        ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
+            this.tag = tag;
+            this.isRequested = isRequireByUser;
+        }
+    }
+
+    /**
+     * Gets the byte order of the current InputStream.
+     */
+    protected ByteOrder getByteOrder() {
+        return mTiffStream.getByteOrder();
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java b/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java
new file mode 100644
index 0000000..68e972f
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This class reads the EXIF header of a JPEG file and stores it in
+ * {@link ExifData}.
+ */
+class ExifReader {
+    private static final String TAG = "ExifReader";
+
+    private final ExifInterface mInterface;
+
+    ExifReader(ExifInterface iRef) {
+        mInterface = iRef;
+    }
+
+    /**
+     * Parses the inputStream and and returns the EXIF data in an
+     * {@link ExifData}.
+     *
+     * @throws ExifInvalidFormatException
+     * @throws IOException
+     */
+    protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException,
+            IOException {
+        ExifParser parser = ExifParser.parse(inputStream, mInterface);
+        ExifData exifData = new ExifData(parser.getByteOrder());
+        ExifTag tag = null;
+
+        int event = parser.next();
+        while (event != ExifParser.EVENT_END) {
+            switch (event) {
+                case ExifParser.EVENT_START_OF_IFD:
+                    exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
+                    break;
+                case ExifParser.EVENT_NEW_TAG:
+                    tag = parser.getTag();
+                    if (!tag.hasValue()) {
+                        parser.registerForTagValue(tag);
+                    } else {
+                        exifData.getIfdData(tag.getIfd()).setTag(tag);
+                    }
+                    break;
+                case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+                    tag = parser.getTag();
+                    if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
+                        parser.readFullTagValue(tag);
+                    }
+                    exifData.getIfdData(tag.getIfd()).setTag(tag);
+                    break;
+                case ExifParser.EVENT_COMPRESSED_IMAGE:
+                    byte buf[] = new byte[parser.getCompressedImageSize()];
+                    if (buf.length == parser.read(buf)) {
+                        exifData.setCompressedThumbnail(buf);
+                    } else {
+                        Log.w(TAG, "Failed to read the compressed thumbnail");
+                    }
+                    break;
+                case ExifParser.EVENT_UNCOMPRESSED_STRIP:
+                    buf = new byte[parser.getStripSize()];
+                    if (buf.length == parser.read(buf)) {
+                        exifData.setStripBytes(parser.getStripIndex(), buf);
+                    } else {
+                        Log.w(TAG, "Failed to read the strip bytes");
+                    }
+                    break;
+            }
+            event = parser.next();
+        }
+        return exifData;
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java b/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java
new file mode 100644
index 0000000..b8b3872
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java
@@ -0,0 +1,1008 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * This class stores information of an EXIF tag. For more information about
+ * defined EXIF tags, please read the Jeita EXIF 2.2 standard. Tags should be
+ * instantiated using {@link ExifInterface#buildTag}.
+ *
+ * @see ExifInterface
+ */
+public class ExifTag {
+    /**
+     * The BYTE type in the EXIF standard. An 8-bit unsigned integer.
+     */
+    public static final short TYPE_UNSIGNED_BYTE = 1;
+    /**
+     * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit
+     * ASCII code. The final byte is terminated with NULL.
+     */
+    public static final short TYPE_ASCII = 2;
+    /**
+     * The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer
+     */
+    public static final short TYPE_UNSIGNED_SHORT = 3;
+    /**
+     * The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer
+     */
+    public static final short TYPE_UNSIGNED_LONG = 4;
+    /**
+     * The RATIONAL type of EXIF standard. It consists of two LONGs. The first
+     * one is the numerator and the second one expresses the denominator.
+     */
+    public static final short TYPE_UNSIGNED_RATIONAL = 5;
+    /**
+     * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any
+     * value depending on the field definition.
+     */
+    public static final short TYPE_UNDEFINED = 7;
+    /**
+     * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer
+     * (2's complement notation).
+     */
+    public static final short TYPE_LONG = 9;
+    /**
+     * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first
+     * one is the numerator and the second one is the denominator.
+     */
+    public static final short TYPE_RATIONAL = 10;
+
+    private static Charset US_ASCII = Charset.forName("US-ASCII");
+    private static final int TYPE_TO_SIZE_MAP[] = new int[11];
+    private static final int UNSIGNED_SHORT_MAX = 65535;
+    private static final long UNSIGNED_LONG_MAX = 4294967295L;
+    private static final long LONG_MAX = Integer.MAX_VALUE;
+    private static final long LONG_MIN = Integer.MIN_VALUE;
+
+    static {
+        TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1;
+        TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1;
+        TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2;
+        TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4;
+        TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8;
+        TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1;
+        TYPE_TO_SIZE_MAP[TYPE_LONG] = 4;
+        TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8;
+    }
+
+    static final int SIZE_UNDEFINED = 0;
+
+    // Exif TagId
+    private final short mTagId;
+    // Exif Tag Type
+    private final short mDataType;
+    // If tag has defined count
+    private boolean mHasDefinedDefaultComponentCount;
+    // Actual data count in tag (should be number of elements in value array)
+    private int mComponentCountActual;
+    // The ifd that this tag should be put in
+    private int mIfd;
+    // The value (array of elements of type Tag Type)
+    private Object mValue;
+    // Value offset in exif header.
+    private int mOffset;
+
+    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd kk:mm:ss");
+
+    /**
+     * Returns true if the given IFD is a valid IFD.
+     */
+    public static boolean isValidIfd(int ifdId) {
+        return ifdId == IfdId.TYPE_IFD_0 || ifdId == IfdId.TYPE_IFD_1
+                || ifdId == IfdId.TYPE_IFD_EXIF || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY
+                || ifdId == IfdId.TYPE_IFD_GPS;
+    }
+
+    /**
+     * Returns true if a given type is a valid tag type.
+     */
+    public static boolean isValidType(short type) {
+        return type == TYPE_UNSIGNED_BYTE || type == TYPE_ASCII ||
+                type == TYPE_UNSIGNED_SHORT || type == TYPE_UNSIGNED_LONG ||
+                type == TYPE_UNSIGNED_RATIONAL || type == TYPE_UNDEFINED ||
+                type == TYPE_LONG || type == TYPE_RATIONAL;
+    }
+
+    // Use builtTag in ExifInterface instead of constructor.
+    ExifTag(short tagId, short type, int componentCount, int ifd,
+            boolean hasDefinedComponentCount) {
+        mTagId = tagId;
+        mDataType = type;
+        mComponentCountActual = componentCount;
+        mHasDefinedDefaultComponentCount = hasDefinedComponentCount;
+        mIfd = ifd;
+        mValue = null;
+    }
+
+    /**
+     * Gets the element size of the given data type in bytes.
+     *
+     * @see #TYPE_ASCII
+     * @see #TYPE_LONG
+     * @see #TYPE_RATIONAL
+     * @see #TYPE_UNDEFINED
+     * @see #TYPE_UNSIGNED_BYTE
+     * @see #TYPE_UNSIGNED_LONG
+     * @see #TYPE_UNSIGNED_RATIONAL
+     * @see #TYPE_UNSIGNED_SHORT
+     */
+    public static int getElementSize(short type) {
+        return TYPE_TO_SIZE_MAP[type];
+    }
+
+    /**
+     * Returns the ID of the IFD this tag belongs to.
+     *
+     * @see IfdId#TYPE_IFD_0
+     * @see IfdId#TYPE_IFD_1
+     * @see IfdId#TYPE_IFD_EXIF
+     * @see IfdId#TYPE_IFD_GPS
+     * @see IfdId#TYPE_IFD_INTEROPERABILITY
+     */
+    public int getIfd() {
+        return mIfd;
+    }
+
+    protected void setIfd(int ifdId) {
+        mIfd = ifdId;
+    }
+
+    /**
+     * Gets the TID of this tag.
+     */
+    public short getTagId() {
+        return mTagId;
+    }
+
+    /**
+     * Gets the data type of this tag
+     *
+     * @see #TYPE_ASCII
+     * @see #TYPE_LONG
+     * @see #TYPE_RATIONAL
+     * @see #TYPE_UNDEFINED
+     * @see #TYPE_UNSIGNED_BYTE
+     * @see #TYPE_UNSIGNED_LONG
+     * @see #TYPE_UNSIGNED_RATIONAL
+     * @see #TYPE_UNSIGNED_SHORT
+     */
+    public short getDataType() {
+        return mDataType;
+    }
+
+    /**
+     * Gets the total data size in bytes of the value of this tag.
+     */
+    public int getDataSize() {
+        return getComponentCount() * getElementSize(getDataType());
+    }
+
+    /**
+     * Gets the component count of this tag.
+     */
+
+    // TODO: fix integer overflows with this
+    public int getComponentCount() {
+        return mComponentCountActual;
+    }
+
+    /**
+     * Sets the component count of this tag. Call this function before
+     * setValue() if the length of value does not match the component count.
+     */
+    protected void forceSetComponentCount(int count) {
+        mComponentCountActual = count;
+    }
+
+    /**
+     * Returns true if this ExifTag contains value; otherwise, this tag will
+     * contain an offset value that is determined when the tag is written.
+     */
+    public boolean hasValue() {
+        return mValue != null;
+    }
+
+    /**
+     * Sets integer values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_SHORT}. This method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
+     * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The value.length does NOT match the component count in the definition
+     * for this tag.</li>
+     * </ul>
+     */
+    public boolean setValue(int[] value) {
+        if (checkBadComponentCount(value.length)) {
+            return false;
+        }
+        if (mDataType != TYPE_UNSIGNED_SHORT && mDataType != TYPE_LONG &&
+                mDataType != TYPE_UNSIGNED_LONG) {
+            return false;
+        }
+        if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
+            return false;
+        } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
+            return false;
+        }
+
+        long[] data = new long[value.length];
+        for (int i = 0; i < value.length; i++) {
+            data[i] = value[i];
+        }
+        mValue = data;
+        mComponentCountActual = value.length;
+        return true;
+    }
+
+    /**
+     * Sets integer value into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_SHORT}, or {@link #TYPE_LONG}. This method
+     * will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
+     * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The component count in the definition of this tag is not 1.</li>
+     * </ul>
+     */
+    public boolean setValue(int value) {
+        return setValue(new int[] {
+                value
+        });
+    }
+
+    /**
+     * Sets long values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The value.length does NOT match the component count in the definition
+     * for this tag.</li>
+     * </ul>
+     */
+    public boolean setValue(long[] value) {
+        if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) {
+            return false;
+        }
+        if (checkOverflowForUnsignedLong(value)) {
+            return false;
+        }
+        mValue = value;
+        mComponentCountActual = value.length;
+        return true;
+    }
+
+    /**
+     * Sets long values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
+     * <li>The value overflows.</li>
+     * <li>The component count in the definition for this tag is not 1.</li>
+     * </ul>
+     */
+    public boolean setValue(long value) {
+        return setValue(new long[] {
+                value
+        });
+    }
+
+    /**
+     * Sets a string value into this tag. This method should be used for tags of
+     * type {@link #TYPE_ASCII}. The string is converted to an ASCII string.
+     * Characters that cannot be converted are replaced with '?'. The length of
+     * the string must be equal to either (component count -1) or (component
+     * count). The final byte will be set to the string null terminator '\0',
+     * overwriting the last character in the string if the value.length is equal
+     * to the component count. This method will fail if:
+     * <ul>
+     * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.</li>
+     * <li>The length of the string is not equal to (component count -1) or
+     * (component count) in the definition for this tag.</li>
+     * </ul>
+     */
+    public boolean setValue(String value) {
+        if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) {
+            return false;
+        }
+
+        byte[] buf = value.getBytes(US_ASCII);
+        byte[] finalBuf = buf;
+        if (buf.length > 0) {
+            finalBuf = (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED) ? buf : Arrays
+                .copyOf(buf, buf.length + 1);
+        } else if (mDataType == TYPE_ASCII && mComponentCountActual == 1) {
+            finalBuf = new byte[] { 0 };
+        }
+        int count = finalBuf.length;
+        if (checkBadComponentCount(count)) {
+            return false;
+        }
+        mComponentCountActual = count;
+        mValue = finalBuf;
+        return true;
+    }
+
+    /**
+     * Sets Rational values into this tag. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This
+     * method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL}
+     * or {@link #TYPE_RATIONAL}.</li>
+     * <li>The value overflows.</li>
+     * <li>The value.length does NOT match the component count in the definition
+     * for this tag.</li>
+     * </ul>
+     *
+     * @see Rational
+     */
+    public boolean setValue(Rational[] value) {
+        if (checkBadComponentCount(value.length)) {
+            return false;
+        }
+        if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) {
+            return false;
+        }
+        if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
+            return false;
+        } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
+            return false;
+        }
+
+        mValue = value;
+        mComponentCountActual = value.length;
+        return true;
+    }
+
+    /**
+     * Sets a Rational value into this tag. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This
+     * method will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL}
+     * or {@link #TYPE_RATIONAL}.</li>
+     * <li>The value overflows.</li>
+     * <li>The component count in the definition for this tag is not 1.</li>
+     * </ul>
+     *
+     * @see Rational
+     */
+    public boolean setValue(Rational value) {
+        return setValue(new Rational[] {
+                value
+        });
+    }
+
+    /**
+     * Sets byte values into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method
+     * will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or
+     * {@link #TYPE_UNDEFINED} .</li>
+     * <li>The length does NOT match the component count in the definition for
+     * this tag.</li>
+     * </ul>
+     */
+    public boolean setValue(byte[] value, int offset, int length) {
+        if (checkBadComponentCount(length)) {
+            return false;
+        }
+        if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) {
+            return false;
+        }
+        mValue = new byte[length];
+        System.arraycopy(value, offset, mValue, 0, length);
+        mComponentCountActual = length;
+        return true;
+    }
+
+    /**
+     * Equivalent to setValue(value, 0, value.length).
+     */
+    public boolean setValue(byte[] value) {
+        return setValue(value, 0, value.length);
+    }
+
+    /**
+     * Sets byte value into this tag. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method
+     * will fail if:
+     * <ul>
+     * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or
+     * {@link #TYPE_UNDEFINED} .</li>
+     * <li>The component count in the definition for this tag is not 1.</li>
+     * </ul>
+     */
+    public boolean setValue(byte value) {
+        return setValue(new byte[] {
+                value
+        });
+    }
+
+    /**
+     * Sets the value for this tag using an appropriate setValue method for the
+     * given object. This method will fail if:
+     * <ul>
+     * <li>The corresponding setValue method for the class of the object passed
+     * in would fail.</li>
+     * <li>There is no obvious way to cast the object passed in into an EXIF tag
+     * type.</li>
+     * </ul>
+     */
+    public boolean setValue(Object obj) {
+        if (obj == null) {
+            return false;
+        } else if (obj instanceof Short) {
+            return setValue(((Short) obj).shortValue() & 0x0ffff);
+        } else if (obj instanceof String) {
+            return setValue((String) obj);
+        } else if (obj instanceof int[]) {
+            return setValue((int[]) obj);
+        } else if (obj instanceof long[]) {
+            return setValue((long[]) obj);
+        } else if (obj instanceof Rational) {
+            return setValue((Rational) obj);
+        } else if (obj instanceof Rational[]) {
+            return setValue((Rational[]) obj);
+        } else if (obj instanceof byte[]) {
+            return setValue((byte[]) obj);
+        } else if (obj instanceof Integer) {
+            return setValue(((Integer) obj).intValue());
+        } else if (obj instanceof Long) {
+            return setValue(((Long) obj).longValue());
+        } else if (obj instanceof Byte) {
+            return setValue(((Byte) obj).byteValue());
+        } else if (obj instanceof Short[]) {
+            // Nulls in this array are treated as zeroes.
+            Short[] arr = (Short[]) obj;
+            int[] fin = new int[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].shortValue() & 0x0ffff;
+            }
+            return setValue(fin);
+        } else if (obj instanceof Integer[]) {
+            // Nulls in this array are treated as zeroes.
+            Integer[] arr = (Integer[]) obj;
+            int[] fin = new int[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].intValue();
+            }
+            return setValue(fin);
+        } else if (obj instanceof Long[]) {
+            // Nulls in this array are treated as zeroes.
+            Long[] arr = (Long[]) obj;
+            long[] fin = new long[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].longValue();
+            }
+            return setValue(fin);
+        } else if (obj instanceof Byte[]) {
+            // Nulls in this array are treated as zeroes.
+            Byte[] arr = (Byte[]) obj;
+            byte[] fin = new byte[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                fin[i] = (arr[i] == null) ? 0 : arr[i].byteValue();
+            }
+            return setValue(fin);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Sets a timestamp to this tag. The method converts the timestamp with the
+     * format of "yyyy:MM:dd kk:mm:ss" and calls {@link #setValue(String)}. This
+     * method will fail if the data type is not {@link #TYPE_ASCII} or the
+     * component count of this tag is not 20 or undefined.
+     *
+     * @param time the number of milliseconds since Jan. 1, 1970 GMT
+     * @return true on success
+     */
+    public boolean setTimeValue(long time) {
+        // synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe
+        synchronized (TIME_FORMAT) {
+            return setValue(TIME_FORMAT.format(new Date(time)));
+        }
+    }
+
+    /**
+     * Gets the value as a String. This method should be used for tags of type
+     * {@link #TYPE_ASCII}.
+     *
+     * @return the value as a String, or null if the tag's value does not exist
+     *         or cannot be converted to a String.
+     */
+    public String getValueAsString() {
+        if (mValue == null) {
+            return null;
+        } else if (mValue instanceof String) {
+            return (String) mValue;
+        } else if (mValue instanceof byte[]) {
+            return new String((byte[]) mValue, US_ASCII);
+        }
+        return null;
+    }
+
+    /**
+     * Gets the value as a String. This method should be used for tags of type
+     * {@link #TYPE_ASCII}.
+     *
+     * @param defaultValue the String to return if the tag's value does not
+     *            exist or cannot be converted to a String.
+     * @return the tag's value as a String, or the defaultValue.
+     */
+    public String getValueAsString(String defaultValue) {
+        String s = getValueAsString();
+        if (s == null) {
+            return defaultValue;
+        }
+        return s;
+    }
+
+    /**
+     * Gets the value as a byte array. This method should be used for tags of
+     * type {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+     *
+     * @return the value as a byte array, or null if the tag's value does not
+     *         exist or cannot be converted to a byte array.
+     */
+    public byte[] getValueAsBytes() {
+        if (mValue instanceof byte[]) {
+            return (byte[]) mValue;
+        }
+        return null;
+    }
+
+    /**
+     * Gets the value as a byte. If there are more than 1 bytes in this value,
+     * gets the first byte. This method should be used for tags of type
+     * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+     *
+     * @param defaultValue the byte to return if tag's value does not exist or
+     *            cannot be converted to a byte.
+     * @return the tag's value as a byte, or the defaultValue.
+     */
+    public byte getValueAsByte(byte defaultValue) {
+        byte[] b = getValueAsBytes();
+        if (b == null || b.length < 1) {
+            return defaultValue;
+        }
+        return b[0];
+    }
+
+    /**
+     * Gets the value as an array of Rationals. This method should be used for
+     * tags of type {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     *
+     * @return the value as as an array of Rationals, or null if the tag's value
+     *         does not exist or cannot be converted to an array of Rationals.
+     */
+    public Rational[] getValueAsRationals() {
+        if (mValue instanceof Rational[]) {
+            return (Rational[]) mValue;
+        }
+        return null;
+    }
+
+    /**
+     * Gets the value as a Rational. If there are more than 1 Rationals in this
+     * value, gets the first one. This method should be used for tags of type
+     * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     *
+     * @param defaultValue the Rational to return if tag's value does not exist
+     *            or cannot be converted to a Rational.
+     * @return the tag's value as a Rational, or the defaultValue.
+     */
+    public Rational getValueAsRational(Rational defaultValue) {
+        Rational[] r = getValueAsRationals();
+        if (r == null || r.length < 1) {
+            return defaultValue;
+        }
+        return r[0];
+    }
+
+    /**
+     * Gets the value as a Rational. If there are more than 1 Rationals in this
+     * value, gets the first one. This method should be used for tags of type
+     * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     *
+     * @param defaultValue the numerator of the Rational to return if tag's
+     *            value does not exist or cannot be converted to a Rational (the
+     *            denominator will be 1).
+     * @return the tag's value as a Rational, or the defaultValue.
+     */
+    public Rational getValueAsRational(long defaultValue) {
+        Rational defaultVal = new Rational(defaultValue, 1);
+        return getValueAsRational(defaultVal);
+    }
+
+    /**
+     * Gets the value as an array of ints. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @return the value as as an array of ints, or null if the tag's value does
+     *         not exist or cannot be converted to an array of ints.
+     */
+    public int[] getValueAsInts() {
+        if (mValue == null) {
+            return null;
+        } else if (mValue instanceof long[]) {
+            long[] val = (long[]) mValue;
+            int[] arr = new int[val.length];
+            for (int i = 0; i < val.length; i++) {
+                arr[i] = (int) val[i]; // Truncates
+            }
+            return arr;
+        }
+        return null;
+    }
+
+    /**
+     * Gets the value as an int. If there are more than 1 ints in this value,
+     * gets the first one. This method should be used for tags of type
+     * {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @param defaultValue the int to return if tag's value does not exist or
+     *            cannot be converted to an int.
+     * @return the tag's value as a int, or the defaultValue.
+     */
+    public int getValueAsInt(int defaultValue) {
+        int[] i = getValueAsInts();
+        if (i == null || i.length < 1) {
+            return defaultValue;
+        }
+        return i[0];
+    }
+
+    /**
+     * Gets the value as an array of longs. This method should be used for tags
+     * of type {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @return the value as as an array of longs, or null if the tag's value
+     *         does not exist or cannot be converted to an array of longs.
+     */
+    public long[] getValueAsLongs() {
+        if (mValue instanceof long[]) {
+            return (long[]) mValue;
+        }
+        return null;
+    }
+
+    /**
+     * Gets the value or null if none exists. If there are more than 1 longs in
+     * this value, gets the first one. This method should be used for tags of
+     * type {@link #TYPE_UNSIGNED_LONG}.
+     *
+     * @param defaultValue the long to return if tag's value does not exist or
+     *            cannot be converted to a long.
+     * @return the tag's value as a long, or the defaultValue.
+     */
+    public long getValueAsLong(long defaultValue) {
+        long[] l = getValueAsLongs();
+        if (l == null || l.length < 1) {
+            return defaultValue;
+        }
+        return l[0];
+    }
+
+    /**
+     * Gets the tag's value or null if none exists.
+     */
+    public Object getValue() {
+        return mValue;
+    }
+
+    /**
+     * Gets a long representation of the value.
+     *
+     * @param defaultValue value to return if there is no value or value is a
+     *            rational with a denominator of 0.
+     * @return the tag's value as a long, or defaultValue if no representation
+     *         exists.
+     */
+    public long forceGetValueAsLong(long defaultValue) {
+        long[] l = getValueAsLongs();
+        if (l != null && l.length >= 1) {
+            return l[0];
+        }
+        byte[] b = getValueAsBytes();
+        if (b != null && b.length >= 1) {
+            return b[0];
+        }
+        Rational[] r = getValueAsRationals();
+        if (r != null && r.length >= 1 && r[0].getDenominator() != 0) {
+            return (long) r[0].toDouble();
+        }
+        return defaultValue;
+    }
+
+    /**
+     * Gets a string representation of the value.
+     */
+    public String forceGetValueAsString() {
+        if (mValue == null) {
+            return "";
+        } else if (mValue instanceof byte[]) {
+            if (mDataType == TYPE_ASCII) {
+                return new String((byte[]) mValue, US_ASCII);
+            } else {
+                return Arrays.toString((byte[]) mValue);
+            }
+        } else if (mValue instanceof long[]) {
+            if (((long[]) mValue).length == 1) {
+                return String.valueOf(((long[]) mValue)[0]);
+            } else {
+                return Arrays.toString((long[]) mValue);
+            }
+        } else if (mValue instanceof Object[]) {
+            if (((Object[]) mValue).length == 1) {
+                Object val = ((Object[]) mValue)[0];
+                if (val == null) {
+                    return "";
+                } else {
+                    return val.toString();
+                }
+            } else {
+                return Arrays.toString((Object[]) mValue);
+            }
+        } else {
+            return mValue.toString();
+        }
+    }
+
+    /**
+     * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG},
+     * {@link #TYPE_UNDEFINED}, {@link #TYPE_UNSIGNED_BYTE},
+     * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}. For
+     * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}, call
+     * {@link #getRational(int)} instead.
+     *
+     * @exception IllegalArgumentException if the data type is
+     *                {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     */
+    protected long getValueAt(int index) {
+        if (mValue instanceof long[]) {
+            return ((long[]) mValue)[index];
+        } else if (mValue instanceof byte[]) {
+            return ((byte[]) mValue)[index];
+        }
+        throw new IllegalArgumentException("Cannot get integer value from "
+                + convertTypeToString(mDataType));
+    }
+
+    /**
+     * Gets the {@link #TYPE_ASCII} data.
+     *
+     * @exception IllegalArgumentException If the type is NOT
+     *                {@link #TYPE_ASCII}.
+     */
+    protected String getString() {
+        if (mDataType != TYPE_ASCII) {
+            throw new IllegalArgumentException("Cannot get ASCII value from "
+                    + convertTypeToString(mDataType));
+        }
+        return new String((byte[]) mValue, US_ASCII);
+    }
+
+    /*
+     * Get the converted ascii byte. Used by ExifOutputStream.
+     */
+    protected byte[] getStringByte() {
+        return (byte[]) mValue;
+    }
+
+    /**
+     * Gets the {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL} data.
+     *
+     * @exception IllegalArgumentException If the type is NOT
+     *                {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+     */
+    protected Rational getRational(int index) {
+        if ((mDataType != TYPE_RATIONAL) && (mDataType != TYPE_UNSIGNED_RATIONAL)) {
+            throw new IllegalArgumentException("Cannot get RATIONAL value from "
+                    + convertTypeToString(mDataType));
+        }
+        return ((Rational[]) mValue)[index];
+    }
+
+    /**
+     * Equivalent to getBytes(buffer, 0, buffer.length).
+     */
+    protected void getBytes(byte[] buf) {
+        getBytes(buf, 0, buf.length);
+    }
+
+    /**
+     * Gets the {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE} data.
+     *
+     * @param buf the byte array in which to store the bytes read.
+     * @param offset the initial position in buffer to store the bytes.
+     * @param length the maximum number of bytes to store in buffer. If length >
+     *            component count, only the valid bytes will be stored.
+     * @exception IllegalArgumentException If the type is NOT
+     *                {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+     */
+    protected void getBytes(byte[] buf, int offset, int length) {
+        if ((mDataType != TYPE_UNDEFINED) && (mDataType != TYPE_UNSIGNED_BYTE)) {
+            throw new IllegalArgumentException("Cannot get BYTE value from "
+                    + convertTypeToString(mDataType));
+        }
+        System.arraycopy(mValue, 0, buf, offset,
+                (length > mComponentCountActual) ? mComponentCountActual : length);
+    }
+
+    /**
+     * Gets the offset of this tag. This is only valid if this data size > 4 and
+     * contains an offset to the location of the actual value.
+     */
+    protected int getOffset() {
+        return mOffset;
+    }
+
+    /**
+     * Sets the offset of this tag.
+     */
+    protected void setOffset(int offset) {
+        mOffset = offset;
+    }
+
+    protected void setHasDefinedCount(boolean d) {
+        mHasDefinedDefaultComponentCount = d;
+    }
+
+    protected boolean hasDefinedCount() {
+        return mHasDefinedDefaultComponentCount;
+    }
+
+    private boolean checkBadComponentCount(int count) {
+        if (mHasDefinedDefaultComponentCount && (mComponentCountActual != count)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static String convertTypeToString(short type) {
+        switch (type) {
+            case TYPE_UNSIGNED_BYTE:
+                return "UNSIGNED_BYTE";
+            case TYPE_ASCII:
+                return "ASCII";
+            case TYPE_UNSIGNED_SHORT:
+                return "UNSIGNED_SHORT";
+            case TYPE_UNSIGNED_LONG:
+                return "UNSIGNED_LONG";
+            case TYPE_UNSIGNED_RATIONAL:
+                return "UNSIGNED_RATIONAL";
+            case TYPE_UNDEFINED:
+                return "UNDEFINED";
+            case TYPE_LONG:
+                return "LONG";
+            case TYPE_RATIONAL:
+                return "RATIONAL";
+            default:
+                return "";
+        }
+    }
+
+    private boolean checkOverflowForUnsignedShort(int[] value) {
+        for (int v : value) {
+            if (v > UNSIGNED_SHORT_MAX || v < 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkOverflowForUnsignedLong(long[] value) {
+        for (long v : value) {
+            if (v < 0 || v > UNSIGNED_LONG_MAX) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkOverflowForUnsignedLong(int[] value) {
+        for (int v : value) {
+            if (v < 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkOverflowForUnsignedRational(Rational[] value) {
+        for (Rational v : value) {
+            if (v.getNumerator() < 0 || v.getDenominator() < 0
+                    || v.getNumerator() > UNSIGNED_LONG_MAX
+                    || v.getDenominator() > UNSIGNED_LONG_MAX) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkOverflowForRational(Rational[] value) {
+        for (Rational v : value) {
+            if (v.getNumerator() < LONG_MIN || v.getDenominator() < LONG_MIN
+                    || v.getNumerator() > LONG_MAX
+                    || v.getDenominator() > LONG_MAX) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof ExifTag) {
+            ExifTag tag = (ExifTag) obj;
+            if (tag.mTagId != this.mTagId
+                    || tag.mComponentCountActual != this.mComponentCountActual
+                    || tag.mDataType != this.mDataType) {
+                return false;
+            }
+            if (mValue != null) {
+                if (tag.mValue == null) {
+                    return false;
+                } else if (mValue instanceof long[]) {
+                    if (!(tag.mValue instanceof long[])) {
+                        return false;
+                    }
+                    return Arrays.equals((long[]) mValue, (long[]) tag.mValue);
+                } else if (mValue instanceof Rational[]) {
+                    if (!(tag.mValue instanceof Rational[])) {
+                        return false;
+                    }
+                    return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue);
+                } else if (mValue instanceof byte[]) {
+                    if (!(tag.mValue instanceof byte[])) {
+                        return false;
+                    }
+                    return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue);
+                } else {
+                    return mValue.equals(tag.mValue);
+                }
+            } else {
+                return tag.mValue == null;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("tag id: %04X\n", mTagId) + "ifd id: " + mIfd + "\ntype: "
+                + convertTypeToString(mDataType) + "\ncount: " + mComponentCountActual
+                + "\noffset: " + mOffset + "\nvalue: " + forceGetValueAsString() + "\n";
+    }
+
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/IfdData.java b/gallerycommon/src/com/android/gallery3d/exif/IfdData.java
new file mode 100644
index 0000000..093944a
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/IfdData.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class stores all the tags in an IFD.
+ *
+ * @see ExifData
+ * @see ExifTag
+ */
+class IfdData {
+
+    private final int mIfdId;
+    private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>();
+    private int mOffsetToNextIfd = 0;
+    private static final int[] sIfds = {
+            IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF,
+            IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS
+    };
+    /**
+     * Creates an IfdData with given IFD ID.
+     *
+     * @see IfdId#TYPE_IFD_0
+     * @see IfdId#TYPE_IFD_1
+     * @see IfdId#TYPE_IFD_EXIF
+     * @see IfdId#TYPE_IFD_GPS
+     * @see IfdId#TYPE_IFD_INTEROPERABILITY
+     */
+    IfdData(int ifdId) {
+        mIfdId = ifdId;
+    }
+
+    static protected int[] getIfds() {
+        return sIfds;
+    }
+
+    /**
+     * Get a array the contains all {@link ExifTag} in this IFD.
+     */
+    protected ExifTag[] getAllTags() {
+        return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
+    }
+
+    /**
+     * Gets the ID of this IFD.
+     *
+     * @see IfdId#TYPE_IFD_0
+     * @see IfdId#TYPE_IFD_1
+     * @see IfdId#TYPE_IFD_EXIF
+     * @see IfdId#TYPE_IFD_GPS
+     * @see IfdId#TYPE_IFD_INTEROPERABILITY
+     */
+    protected int getId() {
+        return mIfdId;
+    }
+
+    /**
+     * Gets the {@link ExifTag} with given tag id. Return null if there is no
+     * such tag.
+     */
+    protected ExifTag getTag(short tagId) {
+        return mExifTags.get(tagId);
+    }
+
+    /**
+     * Adds or replaces a {@link ExifTag}.
+     */
+    protected ExifTag setTag(ExifTag tag) {
+        tag.setIfd(mIfdId);
+        return mExifTags.put(tag.getTagId(), tag);
+    }
+
+    protected boolean checkCollision(short tagId) {
+        return mExifTags.get(tagId) != null;
+    }
+
+    /**
+     * Removes the tag of the given ID
+     */
+    protected void removeTag(short tagId) {
+        mExifTags.remove(tagId);
+    }
+
+    /**
+     * Gets the tags count in the IFD.
+     */
+    protected int getTagCount() {
+        return mExifTags.size();
+    }
+
+    /**
+     * Sets the offset of next IFD.
+     */
+    protected void setOffsetToNextIfd(int offset) {
+        mOffsetToNextIfd = offset;
+    }
+
+    /**
+     * Gets the offset of next IFD.
+     */
+    protected int getOffsetToNextIfd() {
+        return mOffsetToNextIfd;
+    }
+
+    /**
+     * Returns true if all tags in this two IFDs are equal. Note that tags of
+     * IFDs offset or thumbnail offset will be ignored.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof IfdData) {
+            IfdData data = (IfdData) obj;
+            if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
+                ExifTag[] tags = data.getAllTags();
+                for (ExifTag tag : tags) {
+                    if (ExifInterface.isOffsetTag(tag.getTagId())) {
+                        continue;
+                    }
+                    ExifTag tag2 = mExifTags.get(tag.getTagId());
+                    if (!tag.equals(tag2)) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/IfdId.java b/gallerycommon/src/com/android/gallery3d/exif/IfdId.java
new file mode 100644
index 0000000..7842edb
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/IfdId.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+/**
+ * The constants of the IFD ID defined in EXIF spec.
+ */
+public interface IfdId {
+    public static final int TYPE_IFD_0 = 0;
+    public static final int TYPE_IFD_1 = 1;
+    public static final int TYPE_IFD_EXIF = 2;
+    public static final int TYPE_IFD_INTEROPERABILITY = 3;
+    public static final int TYPE_IFD_GPS = 4;
+    /* This is used in ExifData to allocate enough IfdData */
+    static final int TYPE_IFD_COUNT = 5;
+
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java b/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java
new file mode 100644
index 0000000..e3e787e
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+class JpegHeader {
+    public static final short SOI =  (short) 0xFFD8;
+    public static final short APP1 = (short) 0xFFE1;
+    public static final short APP0 = (short) 0xFFE0;
+    public static final short EOI = (short) 0xFFD9;
+
+    /**
+     *  SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG,
+     *  and DAC marker.
+     */
+    public static final short SOF0 = (short) 0xFFC0;
+    public static final short SOF15 = (short) 0xFFCF;
+    public static final short DHT = (short) 0xFFC4;
+    public static final short JPG = (short) 0xFFC8;
+    public static final short DAC = (short) 0xFFCC;
+
+    public static final boolean isSofMarker(short marker) {
+        return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG
+                && marker != DAC;
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java b/gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java
new file mode 100644
index 0000000..428e6b9
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+class OrderedDataOutputStream extends FilterOutputStream {
+    private final ByteBuffer mByteBuffer = ByteBuffer.allocate(4);
+
+    public OrderedDataOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    public OrderedDataOutputStream setByteOrder(ByteOrder order) {
+        mByteBuffer.order(order);
+        return this;
+    }
+
+    public OrderedDataOutputStream writeShort(short value) throws IOException {
+        mByteBuffer.rewind();
+        mByteBuffer.putShort(value);
+        out.write(mByteBuffer.array(), 0, 2);
+        return this;
+    }
+
+    public OrderedDataOutputStream writeInt(int value) throws IOException {
+        mByteBuffer.rewind();
+        mByteBuffer.putInt(value);
+        out.write(mByteBuffer.array());
+        return this;
+    }
+
+    public OrderedDataOutputStream writeRational(Rational rational) throws IOException {
+        writeInt((int) rational.getNumerator());
+        writeInt((int) rational.getDenominator());
+        return this;
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/exif/Rational.java b/gallerycommon/src/com/android/gallery3d/exif/Rational.java
new file mode 100644
index 0000000..591d63f
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/exif/Rational.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+/**
+ * The rational data type of EXIF tag. Contains a pair of longs representing the
+ * numerator and denominator of a Rational number.
+ */
+public class Rational {
+
+    private final long mNumerator;
+    private final long mDenominator;
+
+    /**
+     * Create a Rational with a given numerator and denominator.
+     *
+     * @param nominator
+     * @param denominator
+     */
+    public Rational(long nominator, long denominator) {
+        mNumerator = nominator;
+        mDenominator = denominator;
+    }
+
+    /**
+     * Create a copy of a Rational.
+     */
+    public Rational(Rational r) {
+        mNumerator = r.mNumerator;
+        mDenominator = r.mDenominator;
+    }
+
+    /**
+     * Gets the numerator of the rational.
+     */
+    public long getNumerator() {
+        return mNumerator;
+    }
+
+    /**
+     * Gets the denominator of the rational
+     */
+    public long getDenominator() {
+        return mDenominator;
+    }
+
+    /**
+     * Gets the rational value as type double. Will cause a divide-by-zero error
+     * if the denominator is 0.
+     */
+    public double toDouble() {
+        return mNumerator / (double) mDenominator;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof Rational) {
+            Rational data = (Rational) obj;
+            return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return mNumerator + "/" + mDenominator;
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java
new file mode 100644
index 0000000..44ccd4c
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+import android.graphics.Point;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class JPEGInputStream extends FilterInputStream {
+    private long JNIPointer = 0; // Used by JNI code. Don't touch.
+
+    private boolean mValidConfig = false;
+    private boolean mConfigChanged = false;
+    private int mFormat = -1;
+    private byte[] mTmpBuffer = new byte[1];
+    private int mWidth = 0;
+    private int mHeight = 0;
+
+    public JPEGInputStream(InputStream in) {
+        super(in);
+    }
+
+    public JPEGInputStream(InputStream in, int format) {
+        super(in);
+        setConfig(format);
+    }
+
+    public boolean setConfig(int format) {
+        // Make sure format is valid
+        switch (format) {
+            case JpegConfig.FORMAT_GRAYSCALE:
+            case JpegConfig.FORMAT_RGB:
+            case JpegConfig.FORMAT_ABGR:
+            case JpegConfig.FORMAT_RGBA:
+                break;
+            default:
+                return false;
+        }
+        mFormat = format;
+        mValidConfig = true;
+        mConfigChanged = true;
+        return true;
+    }
+
+    public Point getDimensions() throws IOException {
+        if (mValidConfig) {
+            applyConfigChange();
+            return new Point(mWidth, mHeight);
+        }
+        return null;
+    }
+
+    @Override
+    public int available() {
+        return 0; // TODO
+    }
+
+    @Override
+    public void close() throws IOException {
+        cleanup();
+        super.close();
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+        // Do nothing
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    @Override
+    public int read() throws IOException {
+        read(mTmpBuffer, 0, 1);
+        return 0xFF & mTmpBuffer[0];
+    }
+
+    @Override
+    public int read(byte[] buffer) throws IOException {
+        return read(buffer, 0, buffer.length);
+    }
+
+    @Override
+    public int read(byte[] buffer, int offset, int count) throws IOException {
+        if (offset < 0 || count < 0 || (offset + count) > buffer.length) {
+            throw new ArrayIndexOutOfBoundsException(String.format(
+                    " buffer length %d, offset %d, length %d",
+                    buffer.length, offset, count));
+        }
+        if (!mValidConfig) {
+            return 0;
+        }
+        applyConfigChange();
+        int flag = JpegConfig.J_ERROR_FATAL;
+        try {
+            flag = readDecodedBytes(buffer, offset, count);
+        } finally {
+            if (flag < 0) {
+                cleanup();
+            }
+        }
+        if (flag < 0) {
+            switch (flag) {
+                case JpegConfig.J_DONE:
+                    return -1; // Returns -1 after reading EOS.
+                default:
+                    throw new IOException("Error reading jpeg stream");
+            }
+        }
+        return flag;
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        throw new IOException("Reset not supported.");
+    }
+
+    @Override
+    public long skip(long byteCount) throws IOException {
+        if (byteCount <= 0) {
+            return 0;
+        }
+        // Shorten skip to a reasonable amount
+        int flag = skipDecodedBytes((int) (0x7FFFFFFF & byteCount));
+        if (flag < 0) {
+            switch (flag) {
+                case JpegConfig.J_DONE:
+                    return 0; // Returns 0 after reading EOS.
+                default:
+                    throw new IOException("Error skipping jpeg stream");
+            }
+        }
+        return flag;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            cleanup();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    private void applyConfigChange() throws IOException {
+        if (mConfigChanged) {
+            cleanup();
+            Point dimens = new Point(0, 0);
+            int flag = setup(dimens, in, mFormat);
+            switch(flag) {
+                case JpegConfig.J_SUCCESS:
+                    break; // allow setup to continue
+                case JpegConfig.J_ERROR_BAD_ARGS:
+                    throw new IllegalArgumentException("Bad arguments to read");
+                default:
+                    throw new IOException("Error to reading jpeg headers.");
+            }
+            mWidth = dimens.x;
+            mHeight = dimens.y;
+            mConfigChanged = false;
+        }
+    }
+
+    native private int setup(Point dimens, InputStream in, int format);
+
+    native private void cleanup();
+
+    native private int readDecodedBytes( byte[] inBuffer, int offset, int inCount);
+
+    native private int skipDecodedBytes(int bytes);
+
+    static {
+        System.loadLibrary("jni_jpegstream");
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java
new file mode 100644
index 0000000..c49d375
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+public class JPEGOutputStream extends FilterOutputStream {
+    private long JNIPointer = 0; // Used by JNI code. Don't touch.
+
+    private byte[] mTmpBuffer = new byte[1];
+    private int mWidth = 0;
+    private int mHeight = 0;
+    private int mQuality = 0;
+    private int mFormat = -1;
+    private boolean mValidConfig = false;
+    private boolean mConfigChanged = false;
+
+    public JPEGOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    public JPEGOutputStream(OutputStream out, int width, int height, int quality,
+            int format) {
+        super(out);
+        setConfig(width, height, quality, format);
+    }
+
+    public boolean setConfig(int width, int height, int quality, int format) {
+        // Clamp quality to range (0, 100]
+        quality = Math.max(Math.min(quality, 100), 1);
+
+        // Make sure format is valid
+        switch (format) {
+            case JpegConfig.FORMAT_GRAYSCALE:
+            case JpegConfig.FORMAT_RGB:
+            case JpegConfig.FORMAT_ABGR:
+            case JpegConfig.FORMAT_RGBA:
+                break;
+            default:
+                return false;
+        }
+
+        // If valid, set configuration
+        if (width > 0 && height > 0) {
+            mWidth = width;
+            mHeight = height;
+            mFormat = format;
+            mQuality = quality;
+            mValidConfig = true;
+            mConfigChanged = true;
+        } else {
+            return false;
+        }
+
+        return mValidConfig;
+    }
+
+    @Override
+    public void close() throws IOException {
+        cleanup();
+        super.close();
+    }
+
+    @Override
+    public void write(byte[] buffer, int offset, int length) throws IOException {
+        if (offset < 0 || length < 0 || (offset + length) > buffer.length) {
+            throw new ArrayIndexOutOfBoundsException(String.format(
+                    " buffer length %d, offset %d, length %d",
+                    buffer.length, offset, length));
+        }
+        if (!mValidConfig) {
+            return;
+        }
+        if (mConfigChanged) {
+            cleanup();
+            int flag = setup(out, mWidth, mHeight, mFormat, mQuality);
+            switch(flag) {
+                case JpegConfig.J_SUCCESS:
+                    break; // allow setup to continue
+                case JpegConfig.J_ERROR_BAD_ARGS:
+                    throw new IllegalArgumentException("Bad arguments to write");
+                default:
+                    throw new IOException("Error to writing jpeg headers.");
+            }
+            mConfigChanged = false;
+        }
+        int returnCode = JpegConfig.J_ERROR_FATAL;
+        try {
+            returnCode = writeInputBytes(buffer, offset, length);
+        } finally {
+            if (returnCode < 0) {
+                cleanup();
+            }
+        }
+        if (returnCode < 0) {
+            throw new IOException("Error writing jpeg stream");
+        }
+    }
+
+    @Override
+    public void write(byte[] buffer) throws IOException {
+        write(buffer, 0, buffer.length);
+    }
+
+    @Override
+    public void write(int oneByte) throws IOException {
+        mTmpBuffer[0] = (byte) oneByte;
+        write(mTmpBuffer);
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            cleanup();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    native private int setup(OutputStream out, int width, int height, int format, int quality);
+
+    native private void cleanup();
+
+    native private int writeInputBytes(byte[] inBuffer, int offset, int inCount);
+
+    static {
+        System.loadLibrary("jni_jpegstream");
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java
new file mode 100644
index 0000000..e514e3b
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+public interface JpegConfig {
+    // Pixel formats
+    public static final int FORMAT_GRAYSCALE = 0x001; // 1 byte/pixel
+    public static final int FORMAT_RGB = 0x003; // 3 bytes/pixel RGBRGBRGBRGB...
+    public static final int FORMAT_RGBA = 0x004; // 4 bytes/pixel RGBARGBARGBARGBA...
+    public static final int FORMAT_ABGR = 0x104; // 4 bytes/pixel ABGRABGRABGR...
+
+    // Jni error codes
+    static final int J_SUCCESS = 0;
+    static final int J_ERROR_FATAL = -1;
+    static final int J_ERROR_BAD_ARGS = -2;
+    static final int J_EXCEPTION = -3;
+    static final int J_DONE = -4;
+}
diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java b/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java
new file mode 100644
index 0000000..abd8f68
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+import java.nio.ByteOrder;
+
+public class StreamUtils {
+
+    private StreamUtils() {
+    }
+
+    /**
+     * Copies the input byte array into the output int array with the given
+     * endianness. If input is not a multiple of 4, ignores the last 1-3 bytes
+     * and returns true.
+     */
+    public static boolean byteToIntArray(int[] output, byte[] input, ByteOrder endianness) {
+        int length = input.length - (input.length % 4);
+        if (output.length * 4 < length) {
+            throw new ArrayIndexOutOfBoundsException("Output array is too short to hold input");
+        }
+        if (endianness == ByteOrder.BIG_ENDIAN) {
+            for (int i = 0, j = 0; i < output.length; i++, j += 4) {
+                output[i] = ((input[j] & 0xFF) << 24) | ((input[j + 1] & 0xFF) << 16)
+                        | ((input[j + 2] & 0xFF) << 8) | ((input[j + 3] & 0xFF));
+            }
+        } else {
+            for (int i = 0, j = 0; i < output.length; i++, j += 4) {
+                output[i] = ((input[j + 3] & 0xFF) << 24) | ((input[j + 2] & 0xFF) << 16)
+                        | ((input[j + 1] & 0xFF) << 8) | ((input[j] & 0xFF));
+            }
+        }
+        return input.length % 4 != 0;
+    }
+
+    public static int[] byteToIntArray(byte[] input, ByteOrder endianness) {
+        int[] output = new int[input.length / 4];
+        byteToIntArray(output, input, endianness);
+        return output;
+    }
+
+    /**
+     * Uses native endianness.
+     */
+    public static int[] byteToIntArray(byte[] input) {
+        return byteToIntArray(input, ByteOrder.nativeOrder());
+    }
+
+    /**
+     * Returns the number of bytes in a pixel for a given format defined in
+     * JpegConfig.
+     */
+    public static int pixelSize(int format) {
+        switch (format) {
+            case JpegConfig.FORMAT_ABGR:
+            case JpegConfig.FORMAT_RGBA:
+                return 4;
+            case JpegConfig.FORMAT_RGB:
+                return 3;
+            case JpegConfig.FORMAT_GRAYSCALE:
+                return 1;
+            default:
+                return -1;
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/util/Future.java b/gallerycommon/src/com/android/gallery3d/util/Future.java
new file mode 100644
index 0000000..580a2a1
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/util/Future.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+// This Future differs from the java.util.concurrent.Future in these aspects:
+//
+// - Once cancel() is called, isCancelled() always returns true. It is a sticky
+//   flag used to communicate to the implementation. The implmentation may
+//   ignore that flag. Regardless whether the Future is cancelled, a return
+//   value will be provided to get(). The implementation may choose to return
+//   null if it finds the Future is cancelled.
+//
+// - get() does not throw exceptions.
+//
+public interface Future<T> {
+    public void cancel();
+    public boolean isCancelled();
+    public boolean isDone();
+    public T get();
+    public void waitDone();
+}
diff --git a/gallerycommon/src/com/android/gallery3d/util/FutureListener.java b/gallerycommon/src/com/android/gallery3d/util/FutureListener.java
new file mode 100644
index 0000000..ed1f820
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/util/FutureListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+public interface FutureListener<T> {
+    public void onFutureDone(Future<T> future);
+}
diff --git a/gallerycommon/src/com/android/gallery3d/util/PriorityThreadFactory.java b/gallerycommon/src/com/android/gallery3d/util/PriorityThreadFactory.java
new file mode 100644
index 0000000..30d8e4a
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/util/PriorityThreadFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+
+import android.os.Process;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A thread factory that creates threads with a given thread priority.
+ */
+public class PriorityThreadFactory implements ThreadFactory {
+
+    private final int mPriority;
+    private final AtomicInteger mNumber = new AtomicInteger();
+    private final String mName;
+
+    public PriorityThreadFactory(String name, int priority) {
+        mName = name;
+        mPriority = priority;
+    }
+
+    @Override
+    public Thread newThread(Runnable r) {
+        return new Thread(r, mName + '-' + mNumber.getAndIncrement()) {
+            @Override
+            public void run() {
+                Process.setThreadPriority(mPriority);
+                super.run();
+            }
+        };
+    }
+
+}
diff --git a/gallerycommon/src/com/android/gallery3d/util/ThreadPool.java b/gallerycommon/src/com/android/gallery3d/util/ThreadPool.java
new file mode 100644
index 0000000..115dc66
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/util/ThreadPool.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import android.util.Log;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadPool {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ThreadPool";
+    private static final int CORE_POOL_SIZE = 4;
+    private static final int MAX_POOL_SIZE = 8;
+    private static final int KEEP_ALIVE_TIME = 10; // 10 seconds
+
+    // Resource type
+    public static final int MODE_NONE = 0;
+    public static final int MODE_CPU = 1;
+    public static final int MODE_NETWORK = 2;
+
+    public static final JobContext JOB_CONTEXT_STUB = new JobContextStub();
+
+    ResourceCounter mCpuCounter = new ResourceCounter(2);
+    ResourceCounter mNetworkCounter = new ResourceCounter(2);
+
+    // A Job is like a Callable, but it has an addition JobContext parameter.
+    public interface Job<T> {
+        public T run(JobContext jc);
+    }
+
+    public interface JobContext {
+        boolean isCancelled();
+        void setCancelListener(CancelListener listener);
+        boolean setMode(int mode);
+    }
+
+    private static class JobContextStub implements JobContext {
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public void setCancelListener(CancelListener listener) {
+        }
+
+        @Override
+        public boolean setMode(int mode) {
+            return true;
+        }
+    }
+
+    public interface CancelListener {
+        public void onCancel();
+    }
+
+    private static class ResourceCounter {
+        public int value;
+        public ResourceCounter(int v) {
+            value = v;
+        }
+    }
+
+    private final Executor mExecutor;
+
+    public ThreadPool() {
+        this(CORE_POOL_SIZE, MAX_POOL_SIZE);
+    }
+
+    public ThreadPool(int initPoolSize, int maxPoolSize) {
+        mExecutor = new ThreadPoolExecutor(
+                initPoolSize, maxPoolSize, KEEP_ALIVE_TIME,
+                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
+                new PriorityThreadFactory("thread-pool",
+                android.os.Process.THREAD_PRIORITY_BACKGROUND));
+    }
+
+    // Submit a job to the thread pool. The listener will be called when the
+    // job is finished (or cancelled).
+    public <T> Future<T> submit(Job<T> job, FutureListener<T> listener) {
+        Worker<T> w = new Worker<T>(job, listener);
+        mExecutor.execute(w);
+        return w;
+    }
+
+    public <T> Future<T> submit(Job<T> job) {
+        return submit(job, null);
+    }
+
+    private class Worker<T> implements Runnable, Future<T>, JobContext {
+        @SuppressWarnings("hiding")
+        private static final String TAG = "Worker";
+        private Job<T> mJob;
+        private FutureListener<T> mListener;
+        private CancelListener mCancelListener;
+        private ResourceCounter mWaitOnResource;
+        private volatile boolean mIsCancelled;
+        private boolean mIsDone;
+        private T mResult;
+        private int mMode;
+
+        public Worker(Job<T> job, FutureListener<T> listener) {
+            mJob = job;
+            mListener = listener;
+        }
+
+        // This is called by a thread in the thread pool.
+        @Override
+        public void run() {
+            T result = null;
+
+            // A job is in CPU mode by default. setMode returns false
+            // if the job is cancelled.
+            if (setMode(MODE_CPU)) {
+                try {
+                    result = mJob.run(this);
+                } catch (Throwable ex) {
+                    Log.w(TAG, "Exception in running a job", ex);
+                }
+            }
+
+            synchronized(this) {
+                setMode(MODE_NONE);
+                mResult = result;
+                mIsDone = true;
+                notifyAll();
+            }
+            if (mListener != null) mListener.onFutureDone(this);
+        }
+
+        // Below are the methods for Future.
+        @Override
+        public synchronized void cancel() {
+            if (mIsCancelled) return;
+            mIsCancelled = true;
+            if (mWaitOnResource != null) {
+                synchronized (mWaitOnResource) {
+                    mWaitOnResource.notifyAll();
+                }
+            }
+            if (mCancelListener != null) {
+                mCancelListener.onCancel();
+            }
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return mIsCancelled;
+        }
+
+        @Override
+        public synchronized boolean isDone() {
+            return mIsDone;
+        }
+
+        @Override
+        public synchronized T get() {
+            while (!mIsDone) {
+                try {
+                    wait();
+                } catch (Exception ex) {
+                    Log.w(TAG, "ingore exception", ex);
+                    // ignore.
+                }
+            }
+            return mResult;
+        }
+
+        @Override
+        public void waitDone() {
+            get();
+        }
+
+        // Below are the methods for JobContext (only called from the
+        // thread running the job)
+        @Override
+        public synchronized void setCancelListener(CancelListener listener) {
+            mCancelListener = listener;
+            if (mIsCancelled && mCancelListener != null) {
+                mCancelListener.onCancel();
+            }
+        }
+
+        @Override
+        public boolean setMode(int mode) {
+            // Release old resource
+            ResourceCounter rc = modeToCounter(mMode);
+            if (rc != null) releaseResource(rc);
+            mMode = MODE_NONE;
+
+            // Acquire new resource
+            rc = modeToCounter(mode);
+            if (rc != null) {
+                if (!acquireResource(rc)) {
+                    return false;
+                }
+                mMode = mode;
+            }
+
+            return true;
+        }
+
+        private ResourceCounter modeToCounter(int mode) {
+            if (mode == MODE_CPU) {
+                return mCpuCounter;
+            } else if (mode == MODE_NETWORK) {
+                return mNetworkCounter;
+            } else {
+                return null;
+            }
+        }
+
+        private boolean acquireResource(ResourceCounter counter) {
+            while (true) {
+                synchronized (this) {
+                    if (mIsCancelled) {
+                        mWaitOnResource = null;
+                        return false;
+                    }
+                    mWaitOnResource = counter;
+                }
+
+                synchronized (counter) {
+                    if (counter.value > 0) {
+                        counter.value--;
+                        break;
+                    } else {
+                        try {
+                            counter.wait();
+                        } catch (InterruptedException ex) {
+                            // ignore.
+                        }
+                    }
+                }
+            }
+
+            synchronized (this) {
+                mWaitOnResource = null;
+            }
+
+            return true;
+        }
+
+        private void releaseResource(ResourceCounter counter) {
+            synchronized (counter) {
+                counter.value++;
+                counter.notifyAll();
+            }
+        }
+    }
+}
diff --git a/jni/Android.mk b/jni/Android.mk
new file mode 100644
index 0000000..e612486
--- /dev/null
+++ b/jni/Android.mk
@@ -0,0 +1,52 @@
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_CFLAGS += -DEGL_EGLEXT_PROTOTYPES
+
+LOCAL_SRC_FILES := jni_egl_fence.cpp
+
+LOCAL_SDK_VERSION := 9
+
+LOCAL_LDFLAGS :=  -llog -lEGL
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := libjni_eglfence
+
+
+include $(BUILD_SHARED_LIBRARY)
+
+# Filtershow
+
+include $(CLEAR_VARS)
+
+LOCAL_CPP_EXTENSION := .cc
+LOCAL_LDFLAGS	:= -llog -ljnigraphics
+LOCAL_SDK_VERSION := 9
+LOCAL_MODULE    := libjni_filtershow_filters
+LOCAL_SRC_FILES := filters/gradient.c \
+                   filters/saturated.c \
+                   filters/exposure.c \
+                   filters/edge.c \
+                   filters/contrast.c \
+                   filters/hue.c \
+                   filters/shadows.c \
+                   filters/highlight.c \
+                   filters/hsv.c \
+                   filters/vibrance.c \
+                   filters/geometry.c \
+                   filters/negative.c \
+                   filters/vignette.c \
+                   filters/redEyeMath.c \
+                   filters/fx.c \
+                   filters/wbalance.c \
+                   filters/redeye.c \
+                   filters/bwfilter.c \
+                   filters/tinyplanet.cc \
+                   filters/kmeans.cc
+
+LOCAL_CFLAGS    += -ffast-math -O3 -funroll-loops
+LOCAL_ARM_MODE := arm
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/jni/Application.mk b/jni/Application.mk
new file mode 100644
index 0000000..22d188e
--- /dev/null
+++ b/jni/Application.mk
@@ -0,0 +1 @@
+APP_PLATFORM := android-9
diff --git a/jni/filters/bwfilter.c b/jni/filters/bwfilter.c
new file mode 100644
index 0000000..f7fb31a
--- /dev/null
+++ b/jni/filters/bwfilter.c
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterBwFilter, nativeApplyFilter, jobject bitmap, jint width, jint height, jint rw, jint gw, jint bw)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    unsigned char * rgb = (unsigned char * )destination;
+    float sr = rw;
+    float sg = gw;
+    float sb = bw;
+
+    float min = MIN(sg,sb);
+    min = MIN(sr,min);
+    float max =  MAX(sg,sb);
+    max = MAX(sr,max);
+    float avg = (min+max)/2;
+    sb /= avg;
+    sg /= avg;
+    sr /= avg;
+    int i;
+    int len = width * height * 4;
+
+    for (i = 0; i < len; i+=4)
+    {
+        float r = sr *rgb[RED];
+        float g = sg *rgb[GREEN];
+        float b = sb *rgb[BLUE];
+        min = MIN(g,b);
+        min = MIN(r,min);
+        max = MAX(g,b);
+        max = MAX(r,max);
+        avg =(min+max)/2;
+        rgb[RED]   = CLAMP(avg);
+        rgb[GREEN] = rgb[RED];
+        rgb[BLUE]  = rgb[RED];
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/contrast.c b/jni/filters/contrast.c
new file mode 100644
index 0000000..b04e936
--- /dev/null
+++ b/jni/filters/contrast.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+unsigned char clamp(int c)
+{
+    int N = 255;
+    c &= ~(c >> 31);
+    c -= N;
+    c &= (c >> 31);
+    c += N;
+    return  (unsigned char) c;
+}
+
+int clampMax(int c,int max)
+{
+    c &= ~(c >> 31);
+    c -= max;
+    c &= (c >> 31);
+    c += max;
+    return  c;
+}
+
+void JNIFUNCF(ImageFilterContrast, nativeApplyFilter, jobject bitmap, jint width, jint height, jfloat bright)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    unsigned char * rgb = (unsigned char * )destination;
+    int i;
+    int len = width * height * 4;
+    float m =  (float)pow(2, bright/100.);
+    float c =  127-m*127;
+
+    for (i = 0; i < len; i+=4) {
+        rgb[RED]   = clamp((int)(m*rgb[RED]+c));
+        rgb[GREEN] = clamp((int)(m*rgb[GREEN]+c));
+        rgb[BLUE]  = clamp((int)(m*rgb[BLUE]+c));
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
+
diff --git a/jni/filters/edge.c b/jni/filters/edge.c
new file mode 100644
index 0000000..9f5d88f
--- /dev/null
+++ b/jni/filters/edge.c
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterEdge, nativeApplyFilter, jobject bitmap, jint width, jint height, jfloat p)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+
+    // using contrast function:
+    // f(v) = exp(-alpha * v^beta)
+    // use beta ~ 1
+
+    float const alpha = 5.0f;
+    float const beta = p;
+    float const c_min = 100.0f;
+    float const c_max = 500.0f;
+
+    // pixels must be 4 bytes
+    char * dst = destination;
+
+    int j, k;
+    char * ptr = destination;
+    int row_stride = 4 * width;
+
+    // set 2 row buffer (avoids bitmap copy)
+    int buf_len = 2 * row_stride;
+    char buf[buf_len];
+    int buf_row_ring = 0;
+
+    // set initial buffer to black
+    memset(buf, 0, buf_len * sizeof(char));
+    for (j = 3; j < buf_len; j+=4) {
+        *(buf + j) = 255;  // set initial alphas
+    }
+
+    // apply sobel filter
+    for (j = 1; j < height - 1; j++) {
+
+        for (k = 1; k < width - 1; k++){
+            int loc = j * row_stride + k * 4;
+
+            float bestx = 0.0f;
+            int l;
+            for (l = 0; l < 3; l++) {
+                float tmp = 0.0f;
+                tmp += *(ptr + (loc - row_stride + 4 + l));
+                tmp += *(ptr + (loc + 4 + l)) * 2.0f;
+                tmp += *(ptr + (loc + row_stride + 4 + l));
+                tmp -= *(ptr + (loc - row_stride - 4 + l));
+                tmp -= *(ptr + (loc - 4 + l)) * 2.0f;
+                tmp -= *(ptr + (loc + row_stride - 4 + l));
+                if (fabs(tmp) > fabs(bestx)) {
+                    bestx = tmp;
+                }
+            }
+
+            float besty = 0.0f;
+            for (l = 0; l < 3; l++) {
+                float tmp = 0.0f;
+                tmp -= *(ptr + (loc - row_stride - 4 + l));
+                tmp -= *(ptr + (loc - row_stride + l)) * 2.0f;
+                tmp -= *(ptr + (loc - row_stride + 4 + l));
+                tmp += *(ptr + (loc + row_stride - 4 + l));
+                tmp += *(ptr + (loc + row_stride + l)) * 2.0f;
+                tmp += *(ptr + (loc + row_stride + 4 + l));
+                if (fabs(tmp) > fabs(besty)) {
+                    besty = tmp;
+                }
+            }
+
+            // compute gradient magnitude
+            float mag = sqrt(bestx * bestx + besty * besty);
+
+            // clamp
+            mag = MIN(MAX(c_min, mag), c_max);
+
+            // scale to [0, 1]
+            mag = (mag - c_min) / (c_max - c_min);
+
+            float ret = 1.0f - exp (- alpha * pow(mag, beta));
+            ret = 255 * ret;
+
+            int off = k * 4;
+            *(buf + buf_row_ring + off) = ret;
+            *(buf + buf_row_ring + off + 1) = ret;
+            *(buf + buf_row_ring + off + 2) = ret;
+            *(buf + buf_row_ring + off + 3) = *(ptr + loc + 3);
+        }
+
+        buf_row_ring += row_stride;
+        buf_row_ring %= buf_len;
+
+        if (j - 1 >= 0) {
+            memcpy((dst + row_stride * (j - 1)), (buf + buf_row_ring), row_stride * sizeof(char));
+        }
+
+    }
+    buf_row_ring += row_stride;
+    buf_row_ring %= buf_len;
+    int second_last_row = row_stride * (height - 2);
+    memcpy((dst + second_last_row), (buf + buf_row_ring), row_stride * sizeof(char));
+
+    // set last row to black
+    int last_row = row_stride * (height - 1);
+    memset((dst + last_row), 0, row_stride * sizeof(char));
+    for (j = 3; j < row_stride; j+=4) {
+        *(dst + last_row + j) = 255;  // set alphas
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/exposure.c b/jni/filters/exposure.c
new file mode 100644
index 0000000..6b32798
--- /dev/null
+++ b/jni/filters/exposure.c
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterExposure, nativeApplyFilter, jobject bitmap, jint width, jint height, jfloat bright)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    unsigned char * rgb = (unsigned char * )destination;
+    int i;
+    int len = width * height * 4;
+
+    int m =   (255-bright);
+
+    for (i = 0; i < len; i+=4)
+    {
+        rgb[RED]   = clamp((255*(rgb[RED]))/m);
+        rgb[GREEN] = clamp((255*(rgb[GREEN]))/m);
+        rgb[BLUE]  = clamp((255*(rgb[BLUE]))/m);
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
+
diff --git a/jni/filters/filters.h b/jni/filters/filters.h
new file mode 100644
index 0000000..14b69cd
--- /dev/null
+++ b/jni/filters/filters.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#ifndef FILTERS_H
+#define FILTERS_H
+
+#include <jni.h>
+#include <string.h>
+#include <android/log.h>
+#include <android/bitmap.h>
+
+typedef unsigned int Color;
+
+#define SetColor(a, r, g, b) ((a << 24) | (b << 16) | (g << 8) | (r << 0));
+#define GetA(color) (((color) >> 24) & 0xFF)
+#define GetB(color) (((color) >> 16) & 0xFF)
+#define GetG(color) (((color) >> 8) & 0xFF)
+#define GetR(color) (((color) >> 0) & 0xFF)
+
+#define MIN(a, b) (a < b ? a : b)
+#define MAX(a, b) (a > b ? a : b)
+
+#define LOG(msg...) __android_log_print(ANDROID_LOG_VERBOSE, "NativeFilters", msg)
+
+#define JNIFUNCF(cls, name, vars...) Java_com_android_gallery3d_filtershow_filters_ ## cls ## _ ## name(JNIEnv* env, jobject obj, vars)
+
+#define RED i
+#define GREEN i+1
+#define BLUE i+2
+#define ALPHA i+3
+#define CLAMP(c) (MAX(0, MIN(255, c)))
+
+__inline__ unsigned char  clamp(int c);
+__inline__ int clampMax(int c,int max);
+
+extern void rgb2hsv( unsigned char *rgb,int rgbOff,unsigned short *hsv,int hsvOff);
+extern void hsv2rgb(unsigned short *hsv,int hsvOff,unsigned char  *rgb,int rgbOff);
+extern void filterRedEye(unsigned char *src, unsigned char *dest, int iw, int ih, short *rect);
+extern double fastevalPoly(double *poly,int n, double x);
+#endif // FILTERS_H
diff --git a/jni/filters/fx.c b/jni/filters/fx.c
new file mode 100644
index 0000000..c3c9cbd
--- /dev/null
+++ b/jni/filters/fx.c
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+
+__inline__ int  interp(unsigned char  *src, int p , int *off ,float dr,float dg, float db){
+
+    float fr00 = (src[p+off[0]])*(1-dr)+(src[p+off[1]])*dr;
+    float fr01 = (src[p+off[2]])*(1-dr)+(src[p+off[3]])*dr;
+    float fr10 = (src[p+off[4]])*(1-dr)+(src[p+off[5]])*dr;
+    float fr11 = (src[p+off[6]])*(1-dr)+(src[p+off[7]])*dr;
+    float frb0 = fr00 * (1-db)+fr01*db;
+    float frb1 = fr10 * (1-db)+fr11*db;
+    float frbg = frb0 * (1-dg)+frb1*dg;
+
+    return (int)frbg ;
+}
+
+void JNIFUNCF(ImageFilterFx, nativeApplyFilter, jobject bitmap, jint width, jint height,
+        jobject lutbitmap, jint lutwidth, jint lutheight,
+        jint start, jint end)
+{
+    char* destination = 0;
+    char* lut = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    AndroidBitmap_lockPixels(env, lutbitmap, (void**) &lut);
+    unsigned char * rgb = (unsigned char * )destination;
+    unsigned char * lutrgb = (unsigned char * )lut;
+    int lutdim_r   = lutheight;
+    int lutdim_g   = lutheight;;
+    int lutdim_b   = lutwidth/lutheight;;
+    int STEP = 4;
+
+    int off[8] =  {
+            0,
+            STEP*1,
+            STEP*lutdim_r,
+            STEP*(lutdim_r + 1),
+            STEP*(lutdim_r*lutdim_b),
+            STEP*(lutdim_r*lutdim_b+1),
+            STEP*(lutdim_r*lutdim_b+lutdim_r),
+            STEP*(lutdim_r*lutdim_b+lutdim_r + 1)
+    };
+
+    float scale_R = (lutdim_r-1.f)/256.f;
+    float scale_G = (lutdim_g-1.f)/256.f;
+    float scale_B = (lutdim_b-1.f)/256.f;
+
+    int i;
+    for (i = start; i < end; i+= STEP)
+    {
+        int r = rgb[RED];
+        int g = rgb[GREEN];
+        int b = rgb[BLUE];
+
+        float fb = b*scale_B;
+        float fg = g*scale_G;
+        float fr = r*scale_R;
+        int lut_b = (int)fb;
+        int lut_g = (int)fg;
+        int lut_r = (int)fr;
+        int p = lut_r+lut_b*lutdim_r+lut_g*lutdim_r*lutdim_b;
+        p*=STEP;
+        float dr = fr-lut_r;
+        float dg = fg-lut_g;
+        float db = fb-lut_b;
+        rgb[RED]   = clamp(interp(lutrgb,p  ,off,dr,dg,db));
+        rgb[GREEN] = clamp(interp(lutrgb,p+1,off,dr,dg,db));
+        rgb[BLUE]  = clamp(interp(lutrgb,p+2,off,dr,dg,db));
+
+    }
+
+    AndroidBitmap_unlockPixels(env, bitmap);
+    AndroidBitmap_unlockPixels(env, lutbitmap);
+}
diff --git a/jni/filters/geometry.c b/jni/filters/geometry.c
new file mode 100644
index 0000000..a0b5aaa
--- /dev/null
+++ b/jni/filters/geometry.c
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+#include <stdio.h>
+
+__inline__ void flipVertical(char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight){
+    //Vertical
+    size_t cpy_bytes = sizeof(char) * 4;
+    int width = cpy_bytes * srcWidth;
+    int length = srcHeight;
+    int total = length * width;
+    size_t bytes_to_copy = sizeof(char) * width;
+    int i = 0;
+    int temp = total - width;
+    for (i = 0; i < total; i += width) {
+        memcpy(destination + temp - i, source + i, bytes_to_copy);
+    }
+}
+
+__inline__ void flipHorizontal(char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight){
+    //Horizontal
+    size_t cpy_bytes = sizeof(char) * 4;
+    int width = cpy_bytes * srcWidth;
+    int length = srcHeight;
+    int total = length * width;
+    int i = 0;
+    int j = 0;
+    int temp = 0;
+    for (i = 0; i < total; i+= width) {
+        temp = width + i - cpy_bytes;
+        for (j = 0; j < width; j+=cpy_bytes) {
+            memcpy(destination + temp - j, source + i + j, cpy_bytes);
+        }
+    }
+}
+
+__inline__ void flip_fun(int flip, char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight){
+    int horiz = (flip & 1) != 0;
+    int vert = (flip & 2) != 0;
+    if (horiz && vert){
+        int arr_len = dstWidth * dstHeight * sizeof(char) * 4;
+        char* temp = (char *) malloc(arr_len);
+        flipHorizontal(source, srcWidth, srcHeight, temp, dstWidth, dstHeight);
+        flipVertical(temp, dstWidth, dstHeight, destination, dstWidth, dstHeight);
+        free(temp);
+        return;
+    }
+    if (horiz){
+        flipHorizontal(source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+        return;
+    }
+    if (vert){
+        flipVertical(source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+        return;
+    }
+}
+
+//90 CCW (opposite of what's used in UI?)
+__inline__ void rotate90(char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight){
+    size_t cpy_bytes = sizeof(char) * 4;
+    int width = cpy_bytes * srcWidth;
+    int length = srcHeight;
+    int total = length * width;
+    int i = 0;
+    int j = 0;
+    for (j = 0; j < length * cpy_bytes; j+= cpy_bytes){
+        for (i = 0; i < width; i+=cpy_bytes){
+            int column_disp = (width - cpy_bytes - i) * length;
+            int row_disp = j;
+            memcpy(destination + column_disp + row_disp , source + j * srcWidth + i, cpy_bytes);
+        }
+    }
+}
+
+__inline__ void rotate180(char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight){
+    flip_fun(3, source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+}
+
+__inline__ void rotate270(char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight){
+    rotate90(source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+    flip_fun(3, destination, dstWidth, dstHeight, destination, dstWidth, dstHeight);
+}
+
+// rotate == 1 is 90 degrees, 2 is 180, 3 is 270 (positive is CCW).
+__inline__ void rotate_fun(int rotate, char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight){
+    switch( rotate )
+    {
+        case 1:
+            rotate90(source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+            break;
+        case 2:
+            rotate180(source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+            break;
+        case 3:
+            rotate270(source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+            break;
+        default:
+            break;
+    }
+}
+
+__inline__ void crop(char * source, int srcWidth, int srcHeight, char * destination, int dstWidth, int dstHeight, int offsetWidth, int offsetHeight){
+    size_t cpy_bytes = sizeof(char) * 4;
+    int row_width = cpy_bytes * srcWidth;
+    int new_row_width = cpy_bytes * dstWidth;
+    if ((srcWidth > dstWidth + offsetWidth) || (srcHeight > dstHeight + offsetHeight)){
+        return;
+    }
+    int i = 0;
+    int j = 0;
+    for (j = offsetHeight; j < offsetHeight + dstHeight; j++){
+        memcpy(destination + (j - offsetHeight) * new_row_width, source + j * row_width + offsetWidth * cpy_bytes, cpy_bytes * dstWidth );
+    }
+}
+
+void JNIFUNCF(ImageFilterGeometry, nativeApplyFilterFlip, jobject src, jint srcWidth, jint srcHeight, jobject dst, jint dstWidth, jint dstHeight, jint flip) {
+    char* destination = 0;
+    char* source = 0;
+    if (srcWidth != dstWidth || srcHeight != dstHeight) {
+        return;
+    }
+    AndroidBitmap_lockPixels(env, src, (void**) &source);
+    AndroidBitmap_lockPixels(env, dst, (void**) &destination);
+    flip_fun(flip, source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+    AndroidBitmap_unlockPixels(env, dst);
+    AndroidBitmap_unlockPixels(env, src);
+}
+
+void JNIFUNCF(ImageFilterGeometry, nativeApplyFilterRotate, jobject src, jint srcWidth, jint srcHeight, jobject dst, jint dstWidth, jint dstHeight, jint rotate) {
+    char* destination = 0;
+    char* source = 0;
+    int len = dstWidth * dstHeight * 4;
+    AndroidBitmap_lockPixels(env, src, (void**) &source);
+    AndroidBitmap_lockPixels(env, dst, (void**) &destination);
+    rotate_fun(rotate, source, srcWidth, srcHeight, destination, dstWidth, dstHeight);
+    AndroidBitmap_unlockPixels(env, dst);
+    AndroidBitmap_unlockPixels(env, src);
+}
+
+void JNIFUNCF(ImageFilterGeometry, nativeApplyFilterCrop, jobject src, jint srcWidth, jint srcHeight, jobject dst, jint dstWidth, jint dstHeight, jint offsetWidth, jint offsetHeight) {
+    char* destination = 0;
+    char* source = 0;
+    int len = dstWidth * dstHeight * 4;
+    AndroidBitmap_lockPixels(env, src, (void**) &source);
+    AndroidBitmap_lockPixels(env, dst, (void**) &destination);
+    crop(source, srcWidth, srcHeight, destination, dstWidth, dstHeight, offsetWidth, offsetHeight);
+    AndroidBitmap_unlockPixels(env, dst);
+    AndroidBitmap_unlockPixels(env, src);
+}
+
+void JNIFUNCF(ImageFilterGeometry, nativeApplyFilterStraighten, jobject src, jint srcWidth, jint srcHeight, jobject dst, jint dstWidth, jint dstHeight, jfloat straightenAngle) {
+    char* destination = 0;
+    char* source = 0;
+    int len = dstWidth * dstHeight * 4;
+    AndroidBitmap_lockPixels(env, src, (void**) &source);
+    AndroidBitmap_lockPixels(env, dst, (void**) &destination);
+    // TODO: implement straighten
+    int i = 0;
+    for (; i < len; i += 4) {
+        int r = source[RED];
+        int g = source[GREEN];
+        int b = source[BLUE];
+        destination[RED] = 128;
+        destination[GREEN] = g;
+        destination[BLUE] = 128;
+    }
+    AndroidBitmap_unlockPixels(env, dst);
+    AndroidBitmap_unlockPixels(env, src);
+}
+
diff --git a/jni/filters/gradient.c b/jni/filters/gradient.c
new file mode 100644
index 0000000..1a85697
--- /dev/null
+++ b/jni/filters/gradient.c
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+
+void JNIFUNCF(ImageFilter, nativeApplyGradientFilter, jobject bitmap, jint width, jint height,
+        jintArray redGradient, jintArray greenGradient, jintArray blueGradient)
+{
+    char* destination = 0;
+    jint* redGradientArray = 0;
+    jint* greenGradientArray = 0;
+    jint* blueGradientArray = 0;
+    if (redGradient)
+        redGradientArray = (*env)->GetIntArrayElements(env, redGradient, NULL);
+    if (greenGradient)
+        greenGradientArray = (*env)->GetIntArrayElements(env, greenGradient, NULL);
+    if (blueGradient)
+        blueGradientArray = (*env)->GetIntArrayElements(env, blueGradient, NULL);
+
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    int i;
+    int len = width * height * 4;
+    for (i = 0; i < len; i+=4)
+    {
+        if (redGradient)
+        {
+            int r = destination[RED];
+            r = redGradientArray[r];
+            destination[RED] = r;
+        }
+        if (greenGradient)
+        {
+            int g = destination[GREEN];
+            g = greenGradientArray[g];
+            destination[GREEN] = g;
+        }
+        if (blueGradient)
+        {
+            int b = destination[BLUE];
+            b = blueGradientArray[b];
+            destination[BLUE] = b;
+        }
+    }
+    if (redGradient)
+        (*env)->ReleaseIntArrayElements(env, redGradient, redGradientArray, 0);
+    if (greenGradient)
+        (*env)->ReleaseIntArrayElements(env, greenGradient, greenGradientArray, 0);
+    if (blueGradient)
+        (*env)->ReleaseIntArrayElements(env, blueGradient, blueGradientArray, 0);
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
+
diff --git a/jni/filters/highlight.c b/jni/filters/highlight.c
new file mode 100644
index 0000000..fe9b88f
--- /dev/null
+++ b/jni/filters/highlight.c
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterHighlights, nativeApplyFilter, jobject bitmap,
+              jint width, jint height, jfloatArray luminanceMap){
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    unsigned char * rgb = (unsigned char * )destination;
+    int i;
+    int len = width * height * 4;
+    jfloat* lum = (*env)->GetFloatArrayElements(env, luminanceMap,0);
+    unsigned short * hsv = (unsigned short *)malloc(3*sizeof(short));
+
+    for (i = 0; i < len; i+=4)
+    {
+        rgb2hsv(rgb,i,hsv,0);
+        int v = clampMax(hsv[0],4080);
+        hsv[0] = (unsigned short) clampMax(lum[((255*v)/4080)]*4080,4080);
+        hsv2rgb(hsv,0, rgb,i);
+    }
+
+    free(hsv);
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/hsv.c b/jni/filters/hsv.c
new file mode 100644
index 0000000..aabd053
--- /dev/null
+++ b/jni/filters/hsv.c
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+double fastevalPoly(double *poly,int n, double x){
+
+    double f =x;
+    double sum = poly[0]+poly[1]*f;
+    int i;
+    for (i = 2; i < n; i++) {
+        f*=x;
+        sum += poly[i]*f;
+    }
+    return sum;
+}
+
+void rgb2hsv( unsigned char *rgb,int rgbOff,unsigned short *hsv,int hsvOff)
+{
+    int iMin,iMax,chroma;
+    int ABITS = 4;
+    int HSCALE = 256;
+
+    int k1=255 << ABITS;
+    int k2=HSCALE << ABITS;
+
+    int ri = rgb[rgbOff+0];
+    int gi = rgb[rgbOff+1];
+    int bi = rgb[rgbOff+2];
+    short rv,rs,rh;
+
+    if (ri > gi) {
+        iMax = MAX (ri, bi);
+        iMin = MIN (gi, bi);
+    } else {
+        iMax = MAX (gi, bi);
+        iMin = MIN (ri, bi);
+    }
+
+    chroma = iMax - iMin;
+    // set value
+    rv = (short)( iMax << ABITS);
+
+    // set saturation
+    if (rv == 0)
+        rs = 0;
+    else
+        rs = (short)((k1*chroma)/iMax);
+
+    // set hue
+    if (rs == 0)
+        rh = 0;
+    else {
+        if ( ri == iMax ) {
+            rh  = (short)( (k2*(6*chroma+gi - bi))/(6*chroma));
+            if (rh >= k2) rh -= k2;
+        } else if (gi  == iMax)
+            rh  = (short)( (k2*(2*chroma+bi - ri ))/(6*chroma));
+        else // (bi == iMax )
+                    rh  = (short)( (k2*(4*chroma+ri - gi ))/(6*chroma));
+    }
+    hsv[hsvOff+0] = rv;
+    hsv[hsvOff+1] = rs;
+    hsv[hsvOff+2] = rh;
+}
+
+void hsv2rgb(unsigned short *hsv,int hsvOff, unsigned char *rgb,int rgbOff)
+{
+    int ABITS = 4;
+    int HSCALE = 256;
+    int m;
+    int H,X,ih,is,iv;
+    int k1=255<<ABITS;
+    int k2=HSCALE<<ABITS;
+    int k3=1<<(ABITS-1);
+    int rr=0;
+    int rg=0;
+    int rb=0;
+    short cv = hsv[hsvOff+0];
+    short cs = hsv[hsvOff+1];
+    short ch = hsv[hsvOff+2];
+
+    // set chroma and min component value m
+    //chroma = ( cv * cs )/k1;
+    //m = cv - chroma;
+    m = ((int)cv*(k1 - (int)cs ))/k1;
+
+    // chroma  == 0 <-> cs == 0 --> m=cv
+    if (cs == 0) {
+        rb = ( rg = ( rr =( cv >> ABITS) ));
+    } else {
+        ih=(int)ch;
+        is=(int)cs;
+        iv=(int)cv;
+
+        H = (6*ih)/k2;
+        X = ((iv*is)/k2)*(k2- abs(6*ih- 2*(H>>1)*k2 - k2)) ;
+
+        // removing additional bits --> unit8
+        X=( (X+iv*(k1 - is ))/k1 + k3 ) >> ABITS;
+        m=m >> ABITS;
+
+        // ( chroma + m ) --> cv ;
+        cv=(short) (cv >> ABITS);
+        switch (H) {
+        case 0:
+            rr = cv;
+            rg = X;
+            rb = m;
+            break;
+        case 1:
+            rr = X;
+            rg = cv;
+            rb = m;
+            break;
+        case 2:
+            rr = m;
+            rg = cv;
+            rb = X;
+            break;
+        case 3:
+            rr = m;
+            rg = X;
+            rb = cv;
+            break;
+        case 4:
+            rr = X;
+            rg = m;
+            rb = cv;
+            break;
+        case 5:
+            rr = cv;
+            rg = m ;
+            rb = X;
+            break;
+        }
+    }
+    rgb[rgbOff+0] =  rr;
+    rgb[rgbOff+1] =  rg;
+    rgb[rgbOff+2] =  rb;
+}
+
diff --git a/jni/filters/hue.c b/jni/filters/hue.c
new file mode 100644
index 0000000..a4aef93
--- /dev/null
+++ b/jni/filters/hue.c
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterHue, nativeApplyFilter, jobject bitmap, jint width, jint height, jfloatArray matrix)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    unsigned char * rgb = (unsigned char * )destination;
+    int i;
+    int len = width * height * 4;
+    jfloat* mat = (*env)->GetFloatArrayElements(env, matrix,0);
+
+    for (i = 0; i < len; i+=4)
+    {
+      int r = rgb[RED];
+      int g = rgb[GREEN];
+      int b = rgb[BLUE];
+
+      float rf = r*mat[0] + g*mat[4] +  b*mat[8] + mat[12];
+      float gf = r*mat[1] + g*mat[5] +  b*mat[9] + mat[13];
+      float bf = r*mat[2] + g*mat[6] +  b*mat[10] + mat[14];
+
+      rgb[RED]   = clamp((int)rf);
+      rgb[GREEN] = clamp((int)gf);
+      rgb[BLUE]  = clamp((int)bf);
+    }
+
+    (*env)->ReleaseFloatArrayElements(env, matrix, mat, 0);
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
+
diff --git a/jni/filters/kmeans.cc b/jni/filters/kmeans.cc
new file mode 100644
index 0000000..97cead7
--- /dev/null
+++ b/jni/filters/kmeans.cc
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+#include "kmeans.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * For reasonable speeds:
+ * k < 30
+ * small_ds_bitmap width/height < 64 pixels.
+ * large_ds_bitmap width/height < 512 pixels
+ *
+ * bad for high-frequency image noise
+ */
+
+void JNIFUNCF(ImageFilterKMeans, nativeApplyFilter, jobject bitmap, jint width, jint height,
+        jobject large_ds_bitmap, jint lwidth, jint lheight, jobject small_ds_bitmap,
+        jint swidth, jint sheight, jint p, jint seed)
+{
+    char* destination = 0;
+    char* larger_ds_dst = 0;
+    char* smaller_ds_dst = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    AndroidBitmap_lockPixels(env, large_ds_bitmap, (void**) &larger_ds_dst);
+    AndroidBitmap_lockPixels(env, small_ds_bitmap, (void**) &smaller_ds_dst);
+    unsigned char * dst = (unsigned char *) destination;
+
+    unsigned char * small_ds = (unsigned char *) smaller_ds_dst;
+    unsigned char * large_ds = (unsigned char *) larger_ds_dst;
+
+    // setting for small bitmap
+    int len = swidth * sheight * 4;
+    int dimension = 3;
+    int stride = 4;
+    int iterations = 20;
+    int k = p;
+    unsigned int s = seed;
+    unsigned char finalCentroids[k * stride];
+
+    // get initial picks from small downsampled image
+    runKMeans<unsigned char, int>(k, finalCentroids, small_ds, len, dimension,
+            stride, iterations, s);
+
+
+    len = lwidth * lheight * 4;
+    iterations = 8;
+    unsigned char nextCentroids[k * stride];
+
+    // run kmeans on large downsampled image
+    runKMeansWithPicks<unsigned char, int>(k, nextCentroids, large_ds, len,
+            dimension, stride, iterations, finalCentroids);
+
+    len = width * height * 4;
+
+    // apply to final image
+    applyCentroids<unsigned char, int>(k, nextCentroids, dst, len, dimension, stride);
+
+    AndroidBitmap_unlockPixels(env, small_ds_bitmap);
+    AndroidBitmap_unlockPixels(env, large_ds_bitmap);
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
+#ifdef __cplusplus
+}
+#endif
diff --git a/jni/filters/kmeans.h b/jni/filters/kmeans.h
new file mode 100644
index 0000000..2450605
--- /dev/null
+++ b/jni/filters/kmeans.h
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#ifndef KMEANS_H
+#define KMEANS_H
+
+#include <cstdlib>
+#include <math.h>
+
+// Helper functions
+
+template <typename T, typename N>
+inline void sum(T values[], int len, int dimension, int stride, N dst[]) {
+    int x, y;
+    // zero out dst vector
+    for (x = 0; x < dimension; x++) {
+        dst[x] = 0;
+    }
+    for (x = 0; x < len; x+= stride) {
+        for (y = 0; y < dimension; y++) {
+            dst[y] += values[x + y];
+        }
+    }
+}
+
+template <typename T, typename N>
+inline void set(T val1[], N val2[], int dimension) {
+    int x;
+    for (x = 0; x < dimension; x++) {
+        val1[x] = val2[x];
+    }
+}
+
+template <typename T, typename N>
+inline void add(T val[], N dst[], int dimension) {
+    int x;
+    for (x = 0; x < dimension; x++) {
+        dst[x] += val[x];
+    }
+}
+
+template <typename T, typename N>
+inline void divide(T dst[], N divisor, int dimension) {
+   int x;
+   if (divisor == 0) {
+       return;
+   }
+   for (x = 0; x < dimension; x++) {
+       dst[x] /= divisor;
+   }
+}
+
+/**
+ * Calculates euclidean distance.
+ */
+
+template <typename T, typename N>
+inline N euclideanDist(T val1[], T val2[], int dimension) {
+    int x;
+    N sum = 0;
+    for (x = 0; x < dimension; x++) {
+        N diff = (N) val1[x] - (N) val2[x];
+        sum += diff * diff;
+    }
+    return sqrt(sum);
+}
+
+// K-Means
+
+
+/**
+ * Picks k random starting points from the data set.
+ */
+template <typename T>
+void initialPickHeuristicRandom(int k, T values[], int len, int dimension, int stride, T dst[],
+        unsigned int seed) {
+    int x, z, num_vals, cntr;
+    num_vals = len / stride;
+    cntr = 0;
+    srand(seed);
+    unsigned int r_vals[k];
+    unsigned int r;
+
+    for (x = 0; x < k; x++) {
+
+        // ensure randomly chosen value is unique
+        int r_check = 0;
+        while (r_check == 0) {
+            r = (unsigned int) rand() % num_vals;
+            r_check = 1;
+            for (z = 0; z < x; z++) {
+                if (r == r_vals[z]) {
+                    r_check = 0;
+                }
+            }
+        }
+        r_vals[x] = r;
+        r *= stride;
+
+        // set dst to be randomly chosen value
+        set<T,T>(dst + cntr, values + r, dimension);
+        cntr += stride;
+    }
+}
+
+/**
+ * Finds index of closet centroid to a value
+ */
+template <typename T, typename N>
+inline int findClosest(T values[], T oldCenters[], int dimension, int stride, int pop_size) {
+    int best_ind = 0;
+    N best_len = euclideanDist <T, N>(values, oldCenters, dimension);
+    int y;
+    for (y = stride; y < pop_size; y+=stride) {
+        N l = euclideanDist <T, N>(values, oldCenters + y, dimension);
+        if (l < best_len) {
+            best_len = l;
+            best_ind = y;
+        }
+    }
+    return best_ind;
+}
+
+/**
+ * Calculates new centroids by averaging value clusters for old centroids.
+ */
+template <typename T, typename N>
+int calculateNewCentroids(int k, T values[], int len, int dimension, int stride, T oldCenters[],
+        T dst[]) {
+    int x, pop_size;
+    pop_size = k * stride;
+    int popularities[k];
+    N tmp[pop_size];
+
+    //zero popularities
+    memset(popularities, 0, sizeof(int) * k);
+    // zero dst, and tmp
+    for (x = 0; x < pop_size; x++) {
+        tmp[x] = 0;
+    }
+
+    // put summation for each k in tmp
+    for (x = 0; x < len; x+=stride) {
+        int best = findClosest<T, N>(values + x, oldCenters, dimension, stride, pop_size);
+        add<T, N>(values + x, tmp + best, dimension);
+        popularities[best / stride]++;
+
+    }
+
+    int ret = 0;
+    int y;
+    // divide to get centroid and set dst to result
+    for (x = 0; x < pop_size; x+=stride) {
+        divide<N, int>(tmp + x, popularities[x / stride], dimension);
+        for (y = 0; y < dimension; y++) {
+            if ((dst + x)[y] != (T) ((tmp + x)[y])) {
+                ret = 1;
+            }
+        }
+        set(dst + x, tmp + x, dimension);
+    }
+    return ret;
+}
+
+template <typename T, typename N>
+void runKMeansWithPicks(int k, T finalCentroids[], T values[], int len, int dimension, int stride,
+        int iterations, T initialPicks[]){
+        int k_len = k * stride;
+        int x;
+
+        // zero newCenters
+        for (x = 0; x < k_len; x++) {
+            finalCentroids[x] = 0;
+        }
+
+        T * c1 = initialPicks;
+        T * c2 = finalCentroids;
+        T * temp;
+        int ret = 1;
+        for (x = 0; x < iterations; x++) {
+            ret = calculateNewCentroids<T, N>(k, values, len, dimension, stride, c1, c2);
+            temp = c1;
+            c1 = c2;
+            c2 = temp;
+            if (ret == 0) {
+                x = iterations;
+            }
+        }
+        set<T, T>(finalCentroids, c1, dimension);
+}
+
+/**
+ * Runs the k-means algorithm on dataset values with some initial centroids.
+ */
+template <typename T, typename N>
+void runKMeans(int k, T finalCentroids[], T values[], int len, int dimension, int stride,
+        int iterations, unsigned int seed){
+    int k_len = k * stride;
+    T initialPicks [k_len];
+    initialPickHeuristicRandom<T>(k, values, len, dimension, stride, initialPicks, seed);
+
+    runKMeansWithPicks<T, N>(k, finalCentroids, values, len, dimension, stride,
+        iterations, initialPicks);
+}
+
+/**
+ * Sets each value in values to the closest centroid.
+ */
+template <typename T, typename N>
+void applyCentroids(int k, T centroids[], T values[], int len, int dimension, int stride) {
+    int x, pop_size;
+    pop_size = k * stride;
+    for (x = 0; x < len; x+= stride) {
+        int best = findClosest<T, N>(values + x, centroids, dimension, stride, pop_size);
+        set<T, T>(values + x, centroids + best, dimension);
+    }
+}
+
+#endif // KMEANS_H
diff --git a/jni/filters/negative.c b/jni/filters/negative.c
new file mode 100644
index 0000000..735e583
--- /dev/null
+++ b/jni/filters/negative.c
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterNegative, nativeApplyFilter, jobject bitmap, jint width, jint height)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+
+    int tot_len = height * width * 4;
+    int i;
+    char * dst = destination;
+    for (i = 0; i < tot_len; i+=4) {
+        dst[RED] = 255 - dst[RED];
+        dst[GREEN] = 255 - dst[GREEN];
+        dst[BLUE] = 255 - dst[BLUE];
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/redEyeMath.c b/jni/filters/redEyeMath.c
new file mode 100644
index 0000000..26f3f76
--- /dev/null
+++ b/jni/filters/redEyeMath.c
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+int value(int r, int g, int b) {
+    return MAX(r, MAX(g, b));
+}
+
+int isRed(unsigned char *src, int p) {
+    int b = src[p + 2];
+    int g = src[p + 1];
+    int r = src[p];
+    int max = MAX(g, b);
+
+    return ((r * 100 / (max + 2) > 160) & (max < 80));
+}
+
+void findPossible(unsigned char *src, unsigned char *mask, int iw, int ih,
+        short *rect) {
+    int recX = rect[0], recY = rect[1], recW = rect[2], recH = rect[3];
+    int y, x;
+
+    for (y = 0; y < recH; y++) {
+        int sy = (recY + y) * iw;
+        for (x = 0; x < recW; x++) {
+            int p = (recX + x + sy) * 4;
+
+            int b = src[p + 2];
+            int g = src[p + 1];
+            int r = src[p];
+            mask[x + y * recW] = (
+                    mask[x + y * recW] > 0 && (value(r, g, b) > 240) ? 1 : 0);
+
+        }
+
+    }
+}
+
+void findReds(unsigned char *src, unsigned char *mask, int iw, int ih,
+        short *rect) {
+    int recX = rect[0], recY = rect[1], recW = rect[2], recH = rect[3];
+    int y, x;
+
+    for (y = 0; y < recH; y++) {
+        int sy = (recY + y) * iw;
+        for (x = 0; x < recW; x++) {
+            int p = (recX + x + sy) * 4;
+
+            mask[x + y * recW] = ((isRed(src, p)) ? 1 : 0);
+
+        }
+
+    }
+}
+
+void dialateMaskIfRed(unsigned char *src, int iw, int ih, unsigned char *mask,
+        unsigned char *out, short *rect) {
+    int recX = rect[0], recY = rect[1], recW = rect[2], recH = rect[3];
+    int y, x;
+
+    for (y = 1; y < recH - 1; y++) {
+        int row = recW * y;
+        int sy = (recY + y) * iw;
+        for (x = 1; x < recW - 1; x++) {
+            int p = (recX + x + sy) * 4;
+
+            char b = (mask[row + x] | mask[row + x + 1] | mask[row + x - 1]
+                    | mask[row + x - recW] | mask[row + x + recW]);
+            if (b != 0 && isRed(src, p))
+                out[row + x] = 1;
+            else
+                out[row + x] = mask[row + x];
+        }
+    }
+}
+
+void dialateMask(unsigned char *mask, unsigned char *out, int mw, int mh) {
+    int y, x;
+    for (y = 1; y < mh - 1; y++) {
+        int row = mw * y;
+        for (x = 1; x < mw - 1; x++) {
+            out[row + x] = (mask[row + x] | mask[row + x + 1]
+                    | mask[row + x - 1] | mask[row + x - mw]
+                    | mask[row + x + mw]);
+        }
+    }
+}
+
+void stuff(int r, int g, int b, unsigned char *img, int off) {
+    img[off + 2] = b;
+    img[off + 1] = g;
+    img[off] = r;
+}
+
+void filterRedEye(unsigned char *src, unsigned char *dest, int iw, int ih, short *rect) {
+    int recX = rect[0], recY = rect[1], recW = rect[2], recH = rect[3];
+    unsigned char *mask1 = (unsigned char *) malloc(recW * recH);
+    unsigned char *mask2 = (unsigned char *)malloc(recW*recH);
+    int QUE_LEN = 100;
+    int y, x, i;
+
+    rect[0] = MAX(rect[0],0);
+    rect[1] = MAX(rect[1],0);
+    rect[2] = MIN(rect[2]+rect[0],iw)-rect[0];
+    rect[3] = MIN(rect[3]+rect[1],ih)-rect[1];
+
+    findReds(src, mask2, iw, ih, rect);
+    dialateMask(mask2, mask1, recW, recH);
+    dialateMask(mask1, mask2, recW, recH);
+    dialateMask(mask2, mask1, recW, recH);
+    dialateMask(mask1, mask2, recW, recH);
+    findPossible(src, mask2, iw, ih, rect);
+    dialateMask(mask2, mask1, recW, recH);
+
+    for (i = 0; i < 12; i++) {
+        dialateMaskIfRed(src, iw, ih, mask1, mask2, rect);
+        dialateMaskIfRed(src, iw, ih, mask2, mask1, rect);
+    }
+    dialateMask(mask1, mask2, recW, recH);
+    dialateMask(mask2, mask1, recW, recH);
+
+    for (y = 3; y < recH-3; y++) {
+        int sy = (recY + y) * iw;
+        for (x = 3; x < recW-3; x++) {
+            int p = (recX + x + sy) * 4;
+
+            int b = src[p + 2];
+            int g = src[p + 1];
+            int r = src[p];
+
+            if (mask1[x + y * recW] != 0) {
+                int m = MAX(g,b);
+                float rr = (r - m) / (float) m;
+                if (rr > .7f && g < 60 && b < 60) {
+                    dest[p + 2] = (0);
+                    dest[p + 1] = (0);
+                    dest[p] = (0);
+                } else {
+                    if (mask2[x + y * recW] != 0) {
+                        stuff(r / 2, g / 2, b / 2, dest, p);
+                    } else
+                        stuff((2 * r) / 3, (2 * g) / 3, (2 * b) / 3, dest, p);
+                }
+
+            } else
+                stuff(r, g, b, dest, p);
+
+            //dest[p + 2] = dest[p + 1] =dest[p]=src[p];
+        }
+
+    }
+
+    free(mask1);
+    free(mask2);
+}
+
+
diff --git a/jni/filters/redeye.c b/jni/filters/redeye.c
new file mode 100644
index 0000000..9a358dd
--- /dev/null
+++ b/jni/filters/redeye.c
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+ void JNIFUNCF(ImageFilterRedEye, nativeApplyFilter, jobject bitmap, jint width, jint height, jshortArray vrect)
+ {
+     char* destination = 0;
+     AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+     unsigned char * rgb = (unsigned char * )destination;
+     short* rect = (*env)->GetShortArrayElements(env, vrect,0);
+
+     filterRedEye(rgb,rgb,width,height,rect);
+
+     (*env)->ReleaseShortArrayElements(env, vrect, rect, 0);
+     AndroidBitmap_unlockPixels(env, bitmap);
+ }
diff --git a/jni/filters/saturated.c b/jni/filters/saturated.c
new file mode 100644
index 0000000..1bc0cc5
--- /dev/null
+++ b/jni/filters/saturated.c
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterSaturated, nativeApplyFilter, jobject bitmap, jint width, jint height, jfloat saturation)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    int i;
+    int len = width * height * 4;
+    float Rf = 0.2999f;
+    float Gf = 0.587f;
+    float Bf = 0.114f;
+    float S = saturation;;
+    float MS = 1.0f - S;
+    float Rt = Rf * MS;
+    float Gt = Gf * MS;
+    float Bt = Bf * MS;
+    float R, G, B;
+    for (i = 0; i < len; i+=4)
+    {
+        int r = destination[RED];
+        int g = destination[GREEN];
+        int b = destination[BLUE];
+        int t = (r + g) / 2;
+        R = r;
+        G = g;
+        B = b;
+
+        float Rc = R * (Rt + S) + G * Gt + B * Bt;
+        float Gc = R * Rt + G * (Gt + S) + B * Bt;
+        float Bc = R * Rt + G * Gt + B * (Bt + S);
+
+        destination[RED] = CLAMP(Rc);
+        destination[GREEN] = CLAMP(Gc);
+        destination[BLUE] = CLAMP(Bc);
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/shadows.c b/jni/filters/shadows.c
new file mode 100644
index 0000000..38d64c8
--- /dev/null
+++ b/jni/filters/shadows.c
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterShadows, nativeApplyFilter, jobject bitmap, jint width, jint height, float scale){
+    double shadowFilterMap[] = {
+            -0.00591,  0.0001,
+             1.16488,  0.01668,
+            -0.18027, -0.06791,
+            -0.12625,  0.09001,
+             0.15065, -0.03897
+    };
+
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    unsigned char * rgb = (unsigned char * )destination;
+    int i;
+    double s = (scale>=0)?scale:scale/5;
+    int len = width * height * 4;
+
+    double *poly = (double *) malloc(5*sizeof(double));
+    for (i = 0; i < 5; i++) {
+        poly[i] = fastevalPoly(shadowFilterMap+i*2,2 , s);
+    }
+
+    unsigned short * hsv = (unsigned short *)malloc(3*sizeof(short));
+
+    for (i = 0; i < len; i+=4)
+    {
+        rgb2hsv(rgb,i,hsv,0);
+
+        double v = (fastevalPoly(poly,5,hsv[0]/4080.)*4080);
+        if (v>4080) v = 4080;
+        hsv[0] = (unsigned short) ((v>0)?v:0);
+
+        hsv2rgb(hsv,0, rgb,i);
+    }
+
+    free(poly);
+    free(hsv);
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/tinyplanet.cc b/jni/filters/tinyplanet.cc
new file mode 100644
index 0000000..beac086
--- /dev/null
+++ b/jni/filters/tinyplanet.cc
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+#include <math.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+#define PI_F 3.141592653589f
+
+class ImageRGBA {
+ public:
+  ImageRGBA(unsigned char* image, int width, int height)
+   : image_(image), width_(width), height_(height) {
+    width_step_ = width * 4;
+  }
+
+  int Width() const {
+    return width_;
+  }
+
+  int Height() const {
+    return height_;
+  }
+
+  // Pixel accessor.
+  unsigned char* operator()(int x, int y) {
+    return image_ + y * width_step_ + x * 4;
+  }
+  const unsigned char* operator()(int x, int y) const {
+    return image_ + y * width_step_ + x * 4;
+  }
+
+ private:
+  unsigned char* image_;
+  int width_;
+  int height_;
+  int width_step_;
+};
+
+// Interpolate a pixel in a 3 channel image.
+inline void InterpolatePixel(const ImageRGBA &image, float x, float y,
+                             unsigned char* dest) {
+  // Get pointers and scale factors for the source pixels.
+  float ax = x - floor(x);
+  float ay = y - floor(y);
+  float axn = 1.0f - ax;
+  float ayn = 1.0f - ay;
+  const unsigned char *p = image(x, y);
+  const unsigned char *p2 = image(x, y + 1);
+
+  // Interpolate each image color plane.
+  dest[0] = static_cast<unsigned char>(axn * ayn * p[0] + ax * ayn * p[4] +
+             ax * ay * p2[4] + axn * ay * p2[0] + 0.5f);
+  p++;
+  p2++;
+
+  dest[1] = static_cast<unsigned char>(axn * ayn * p[0] + ax * ayn * p[4] +
+             ax * ay * p2[4] + axn * ay * p2[0] + 0.5f);
+  p++;
+  p2++;
+
+  dest[2] = static_cast<unsigned char>(axn * ayn * p[0] + ax * ayn * p[4] +
+             ax * ay * p2[4] + axn * ay * p2[0] + 0.5f);
+  p++;
+  p2++;
+  dest[3] = 0xFF;
+}
+
+// Wrap circular coordinates around the globe
+inline float wrap(float value, float dimension) {
+  return value - (dimension * floor(value/dimension));
+}
+
+void StereographicProjection(float scale, float angle, unsigned char* input_image,
+                             int input_width, int input_height,
+                             unsigned char* output_image, int output_width,
+                             int output_height) {
+  ImageRGBA input(input_image, input_width, input_height);
+  ImageRGBA output(output_image, output_width, output_height);
+
+  const float image_scale = output_width * scale;
+
+  for (int x = 0; x < output_width; x++) {
+    // Center and scale x
+    float xf = (x - output_width / 2.0f) / image_scale;
+
+    for (int y = 0; y < output_height; y++) {
+      // Center and scale y
+      float yf = (y - output_height / 2.0f) / image_scale;
+
+      // Convert to polar
+      float r = hypotf(xf, yf);
+      float theta = angle+atan2(yf, xf);
+      if (theta>PI_F) theta-=2*PI_F;
+
+      // Project onto plane
+      float phi = 2 * atan(1 / r);
+      // (theta stays the same)
+
+      // Map to panorama image
+      float px = (theta / (2 * PI_F)) * input_width;
+      float py = (phi / PI_F) * input_height;
+
+      // Wrap around the globe
+      px = wrap(px, input_width);
+      py = wrap(py, input_height);
+
+      // Write the interpolated pixel
+      InterpolatePixel(input, px, py, output(x, y));
+    }
+  }
+}
+
+
+void JNIFUNCF(ImageFilterTinyPlanet, nativeApplyFilter, jobject bitmap_in, jint width, jint height, jobject bitmap_out, jint output_size, jfloat scale,jfloat angle)
+{
+    char* source = 0;
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap_in, (void**) &source);
+    AndroidBitmap_lockPixels(env, bitmap_out, (void**) &destination);
+    unsigned char * rgb_in = (unsigned char * )source;
+    unsigned char * rgb_out = (unsigned char * )destination;
+
+    StereographicProjection(scale,angle, rgb_in, width, height, rgb_out, output_size, output_size);
+    AndroidBitmap_unlockPixels(env, bitmap_in);
+    AndroidBitmap_unlockPixels(env, bitmap_out);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+
diff --git a/jni/filters/vibrance.c b/jni/filters/vibrance.c
new file mode 100644
index 0000000..cb5c536
--- /dev/null
+++ b/jni/filters/vibrance.c
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include <math.h>
+#include "filters.h"
+
+void JNIFUNCF(ImageFilterVibrance, nativeApplyFilter, jobject bitmap, jint width, jint height,  jfloat vibrance)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    int i;
+    int len = width * height * 4;
+    float Rf = 0.2999f;
+    float Gf = 0.587f;
+    float Bf = 0.114f;
+    float Vib = vibrance/100.f;
+    float S  = Vib+1;
+    float MS = 1.0f - S;
+    float Rt = Rf * MS;
+    float Gt = Gf * MS;
+    float Bt = Bf * MS;
+    float R, G, B;
+    for (i = 0; i < len; i+=4)
+    {
+        int r = destination[RED];
+        int g = destination[GREEN];
+        int b = destination[BLUE];
+        float red = (r-MAX(g, b))/256.f;
+        float sx = (float)(Vib/(1+exp(-red*3)));
+        S = sx+1;
+        MS = 1.0f - S;
+        Rt = Rf * MS;
+        Gt = Gf * MS;
+        Bt = Bf * MS;
+        int t = (r + g) / 2;
+        R = r;
+        G = g;
+        B = b;
+
+        float Rc = R * (Rt + S) + G * Gt + B * Bt;
+        float Gc = R * Rt + G * (Gt + S) + B * Bt;
+        float Bc = R * Rt + G * Gt + B * (Bt + S);
+
+        destination[RED] = CLAMP(Rc);
+        destination[GREEN] = CLAMP(Gc);
+        destination[BLUE] = CLAMP(Bc);
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/vignette.c b/jni/filters/vignette.c
new file mode 100644
index 0000000..b9ee3ff
--- /dev/null
+++ b/jni/filters/vignette.c
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+#include <math.h>
+
+static int* gVignetteMap = 0;
+static int gVignetteWidth = 0;
+static int gVignetteHeight = 0;
+
+void JNIFUNCF(ImageFilterVignette, nativeApplyFilter, jobject bitmap, jint width, jint height, jint centerx, jint centery, jfloat radiusx, jfloat radiusy, jfloat strength)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    int i;
+    int len = width * height * 4;
+    int vignette = 0;
+    float d = centerx;
+    if (radiusx == 0) radiusx = 10;
+    if (radiusy == 0) radiusy = 10;
+    float scalex = 1/radiusx;
+    float scaley = 1/radiusy;
+
+    for (i = 0; i < len; i += 4)
+    {
+        int p = i/4;
+        float x = ((p%width)-centerx)*scalex;
+        float y = ((p/width)-centery)*scaley;
+        float dist = sqrt(x*x+y*y)-1;
+        vignette = (int) (strength*256*MAX(dist,0));
+        destination[RED] = CLAMP(destination[RED] - vignette);
+        destination[GREEN] = CLAMP(destination[GREEN] - vignette);
+        destination[BLUE] = CLAMP(destination[BLUE] - vignette);
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/filters/wbalance.c b/jni/filters/wbalance.c
new file mode 100644
index 0000000..2b92b99
--- /dev/null
+++ b/jni/filters/wbalance.c
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "filters.h"
+
+void estmateWhite(unsigned char *src, int len, int *wr, int *wb, int *wg){
+
+    int STEP = 4;
+    int RANGE = 256;
+    int *histR = (int *) malloc(256*sizeof(int));
+    int *histG = (int *) malloc(256*sizeof(int));
+    int *histB = (int *) malloc(256*sizeof(int));
+    int i;
+    for (i = 0; i < 255; i++) {
+        histR[i] = histG[i] = histB[i] =0;
+    }
+
+    for (i = 0; i < len; i+=STEP) {
+        histR[(src[RED])]++;
+        histG[(src[GREEN])]++;
+        histB[(src[BLUE])]++;
+    }
+    int min_r = -1, min_g = -1,min_b = -1;
+    int max_r = 0, max_g = 0,max_b = 0;
+    int sum_r = 0,sum_g=0,sum_b=0;
+
+    for (i = 1; i < RANGE-1; i++) {
+        int r = histR[i];
+        int g = histG[i];
+        int b = histB[i];
+        sum_r += r;
+        sum_g += g;
+        sum_b += b;
+
+        if (r>0){
+            if (min_r < 0) min_r = i;
+            max_r = i;
+        }
+        if (g>0){
+            if (min_g < 0) min_g = i;
+            max_g = i;
+        }
+        if (b>0){
+            if (min_b < 0) min_b = i;
+            max_b = i;
+        }
+    }
+
+    int sum15r = 0,sum15g=0,sum15b=0;
+    int count15r = 0,count15g=0,count15b=0;
+    int tmp_r = 0,tmp_g=0,tmp_b=0;
+
+    for (i = RANGE-2; i >0; i--) {
+        int r = histR[i];
+        int g = histG[i];
+        int b = histB[i];
+        tmp_r += r;
+        tmp_g += g;
+        tmp_b += b;
+
+        if ((tmp_r > sum_r/20) && (tmp_r < sum_r/5)) {
+            sum15r += r*i;
+            count15r += r;
+        }
+        if ((tmp_g > sum_g/20) && (tmp_g < sum_g/5)) {
+            sum15g += g*i;
+            count15g += g;
+        }
+        if ((tmp_b > sum_b/20) && (tmp_b < sum_b/5)) {
+            sum15b += b*i;
+            count15b += b;
+        }
+
+    }
+    free(histR);
+    free(histG);
+    free(histB);
+
+    if ((count15r>0) && (count15g>0) && (count15b>0) ){
+        *wr = sum15r/count15r;
+        *wb = sum15g/count15g;
+        *wg = sum15b/count15b;
+    }else {
+        *wg  = *wb = *wr=255;
+    }
+}
+
+void estmateWhiteBox(unsigned char *src, int iw, int ih, int x,int y, int *wr, int *wb, int *wg){
+    int r;
+    int g;
+    int b;
+    int sum;
+    int xp,yp;
+    int bounds = 5;
+    if (x<0) x = bounds;
+    if (y<0) y = bounds;
+    if (x>=(iw-bounds)) x = (iw-bounds-1);
+    if (y>=(ih-bounds)) y = (ih-bounds-1);
+    int startx = x - bounds;
+    int starty = y - bounds;
+    int endx = x + bounds;
+    int endy = y + bounds;
+
+    for(yp= starty;yp<endy;yp++) {
+        for(xp= startx;xp<endx;xp++) {
+            int i = 4*(xp+yp*iw);
+            r += src[RED];
+            g += src[GREEN];
+            b += src[BLUE];
+            sum++;
+        }
+    }
+    *wr = r/sum;
+    *wg = g/sum;
+    *wb = b/sum;
+}
+
+void JNIFUNCF(ImageFilterWBalance, nativeApplyFilter, jobject bitmap, jint width, jint height, int locX,int locY)
+{
+    char* destination = 0;
+    AndroidBitmap_lockPixels(env, bitmap, (void**) &destination);
+    int i;
+    int len = width * height * 4;
+    unsigned char * rgb = (unsigned char * )destination;
+    int wr;
+    int wg;
+    int wb;
+
+    if (locX==-1)
+        estmateWhite(rgb,len,&wr,&wg,&wb);
+    else
+        estmateWhiteBox(rgb, width, height,locX,locY,&wr,&wg,&wb);
+
+    int min = MIN(wr, MIN(wg, wb));
+    int max = MAX(wr, MAX(wg, wb));
+    float avg = (min+max)/2.f;
+    float scaleR =  avg/wr;
+    float scaleG =  avg/wg;
+    float scaleB =  avg/wb;
+
+    for (i = 0; i < len; i+=4)
+    {
+        int r = rgb[RED];
+        int g = rgb[GREEN];
+        int b = rgb[BLUE];
+
+        float Rc =  r*scaleR;
+        float Gc =  g*scaleG;
+        float Bc =  b*scaleB;
+
+        rgb[RED]   = clamp(Rc);
+        rgb[GREEN] = clamp(Gc);
+        rgb[BLUE]  = clamp(Bc);
+    }
+    AndroidBitmap_unlockPixels(env, bitmap);
+}
diff --git a/jni/jni_egl_fence.cpp b/jni/jni_egl_fence.cpp
new file mode 100644
index 0000000..cf15e2f
--- /dev/null
+++ b/jni/jni_egl_fence.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#include "jni_egl_fence.h"
+
+#include <android/log.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <string.h>
+
+#define  ALOGE(...)  __android_log_print(ANDROID_LOG_ERROR,"egl_fence",__VA_ARGS__)
+
+typedef EGLSyncKHR EGLAPIENTRY (*TypeEglCreateSyncKHR)(EGLDisplay dpy,
+    EGLenum type, const EGLint *attrib_list);
+typedef EGLBoolean EGLAPIENTRY (*TypeEglDestroySyncKHR)(EGLDisplay dpy,
+    EGLSyncKHR sync);
+typedef EGLint EGLAPIENTRY (*TypeEglClientWaitSyncKHR)(EGLDisplay dpy,
+    EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
+static TypeEglCreateSyncKHR FuncEglCreateSyncKHR = NULL;
+static TypeEglClientWaitSyncKHR FuncEglClientWaitSyncKHR = NULL;
+static TypeEglDestroySyncKHR FuncEglDestroySyncKHR = NULL;
+static bool initialized = false;
+static bool egl_khr_fence_sync_supported = false;
+
+bool IsEglKHRFenceSyncSupported() {
+  if (!initialized) {
+    EGLDisplay display = eglGetCurrentDisplay();
+    const char* eglExtensions = eglQueryString(eglGetCurrentDisplay(), EGL_EXTENSIONS);
+    if (eglExtensions && strstr(eglExtensions, "EGL_KHR_fence_sync")) {
+      FuncEglCreateSyncKHR = (TypeEglCreateSyncKHR) eglGetProcAddress("eglCreateSyncKHR");
+      FuncEglClientWaitSyncKHR = (TypeEglClientWaitSyncKHR) eglGetProcAddress("eglClientWaitSyncKHR");
+      FuncEglDestroySyncKHR = (TypeEglDestroySyncKHR) eglGetProcAddress("eglDestroySyncKHR");
+      if (FuncEglCreateSyncKHR != NULL && FuncEglClientWaitSyncKHR != NULL
+          && FuncEglDestroySyncKHR != NULL) {
+        egl_khr_fence_sync_supported = true;
+      }
+    }
+    initialized = true;
+  }
+  return egl_khr_fence_sync_supported;
+}
+
+void
+Java_com_android_gallery3d_photoeditor_FilterStack_nativeEglSetFenceAndWait(JNIEnv* env,
+                                                                          jobject thiz) {
+  if (!IsEglKHRFenceSyncSupported()) return;
+  EGLDisplay display = eglGetCurrentDisplay();
+
+  // Create a egl fence and wait for egl to return it.
+  // Additional reference on egl fence sync can be found in:
+  // http://www.khronos.org/registry/vg/extensions/KHR/EGL_KHR_fence_sync.txt
+  EGLSyncKHR fence = FuncEglCreateSyncKHR(display, EGL_SYNC_FENCE_KHR, NULL);
+  if (fence == EGL_NO_SYNC_KHR) {
+    return;
+  }
+
+  EGLint result = FuncEglClientWaitSyncKHR(display,
+                                       fence,
+                                       EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
+                                       EGL_FOREVER_KHR);
+  if (result == EGL_FALSE) {
+    ALOGE("EGL FENCE: error waiting for fence: %#x", eglGetError());
+  }
+  FuncEglDestroySyncKHR(display, fence);
+}
diff --git a/jni/jni_egl_fence.h b/jni/jni_egl_fence.h
new file mode 100644
index 0000000..6b2c20a
--- /dev/null
+++ b/jni/jni_egl_fence.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#ifndef COM_ANDROID_GALLERY3D_PHOTOEDITOR_JNI_EGL_FENSE_H
+#define COM_ANDROID_GALLERY3D_PHOTOEDITOR_JNI_EGL_FENSE_H
+
+#include <jni.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT void JNICALL
+Java_com_android_gallery3d_photoeditor_FilterStack_nativeEglSetFenceAndWait(JNIEnv* env,
+                                                                            jobject thiz);
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* COM_ANDROID_GALLERY3D_PHOTOEDITOR_JNI_EGL_FENSE_H */
diff --git a/jni_jpegstream/Android.mk b/jni_jpegstream/Android.mk
new file mode 100644
index 0000000..de11733
--- /dev/null
+++ b/jni_jpegstream/Android.mk
@@ -0,0 +1,41 @@
+LOCAL_PATH:= $(call my-dir)
+
+# Jpeg Streaming native
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE        := libjni_jpegstream
+
+LOCAL_NDK_STL_VARIANT := stlport_static
+
+LOCAL_C_INCLUDES := $(LOCAL_PATH) \
+                    $(LOCAL_PATH)/src \
+                    external/jpeg
+
+LOCAL_SHARED_LIBRARIES := libjpeg
+ifeq (,$(TARGET_BUILD_APPS))
+   # platform build
+   LOCAL_SHARED_LIBRARIES := libcutils
+endif
+
+LOCAL_LDFLAGS        := -llog
+LOCAL_SDK_VERSION   := 9
+LOCAL_ARM_MODE := arm
+
+LOCAL_CFLAGS    += -ffast-math -O3 -funroll-loops
+LOCAL_CPPFLAGS += $(JNI_CFLAGS)
+
+
+LOCAL_CPP_EXTENSION := .cpp
+LOCAL_SRC_FILES     := \
+    src/inputstream_wrapper.cpp \
+    src/jpegstream.cpp \
+    src/jerr_hook.cpp \
+    src/jpeg_hook.cpp \
+    src/jpeg_writer.cpp \
+    src/jpeg_reader.cpp \
+    src/outputstream_wrapper.cpp \
+    src/stream_wrapper.cpp
+
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/jni_jpegstream/src/error_codes.h b/jni_jpegstream/src/error_codes.h
new file mode 100644
index 0000000..be55a00
--- /dev/null
+++ b/jni_jpegstream/src/error_codes.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef JPEG_ERROR_CODES_H_
+#define JPEG_ERROR_CODES_H_
+
+#define J_DONE                    -4
+#define J_EXCEPTION               -3
+#define J_ERROR_BAD_ARGS          -2
+#define J_ERROR_FATAL             -1
+#define J_SUCCESS                 0
+
+#endif // JPEG_ERROR_CODES_H_
diff --git a/jni_jpegstream/src/inputstream_wrapper.cpp b/jni_jpegstream/src/inputstream_wrapper.cpp
new file mode 100644
index 0000000..98721b0
--- /dev/null
+++ b/jni_jpegstream/src/inputstream_wrapper.cpp
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include "inputstream_wrapper.h"
+#include "error_codes.h"
+
+jmethodID InputStreamWrapper::sReadID = NULL;
+jmethodID InputStreamWrapper::sSkipID = NULL;
+
+int32_t InputStreamWrapper::read(int32_t length, int32_t offset) {
+    if (offset < 0 || length < 0 || (offset + length) > getBufferSize()) {
+        return J_ERROR_BAD_ARGS;
+    }
+    int32_t bytesRead = 0;
+    mEnv->ReleaseByteArrayElements(mByteArray, mBytes, JNI_COMMIT);
+    mBytes = NULL;
+    if (mEnv->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    bytesRead = static_cast<int32_t>(mEnv->CallIntMethod(mStream, sReadID,
+            mByteArray, offset, length));
+    if (mEnv->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    mBytes = mEnv->GetByteArrayElements(mByteArray, NULL);
+    if (mBytes == NULL || mEnv->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    if (bytesRead == END_OF_STREAM) {
+        return J_DONE;
+    }
+    return bytesRead;
+}
+
+int64_t InputStreamWrapper::skip(int64_t count) {
+    int64_t bytesSkipped = 0;
+    bytesSkipped = static_cast<int64_t>(mEnv->CallLongMethod(mStream, sSkipID,
+            static_cast<jlong>(count)));
+    if (mEnv->ExceptionCheck() || bytesSkipped < 0) {
+        return J_EXCEPTION;
+    }
+    return bytesSkipped;
+}
+
+// Acts like a read call that returns the End Of Image marker for a JPEG file.
+int32_t InputStreamWrapper::forceReadEOI() {
+    mBytes[0] = (jbyte) 0xFF;
+    mBytes[1] = (jbyte) 0xD9;
+    return 2;
+}
+
+void InputStreamWrapper::setReadSkipMethodIDs(jmethodID readID,
+        jmethodID skipID) {
+    sReadID = readID;
+    sSkipID = skipID;
+}
diff --git a/jni_jpegstream/src/inputstream_wrapper.h b/jni_jpegstream/src/inputstream_wrapper.h
new file mode 100644
index 0000000..ed9942b
--- /dev/null
+++ b/jni_jpegstream/src/inputstream_wrapper.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef INPUTSTREAM_WRAPPER_H_
+#define INPUTSTREAM_WRAPPER_H_
+
+#include "jni_defines.h"
+#include "stream_wrapper.h"
+
+#include <stdint.h>
+
+class InputStreamWrapper : public StreamWrapper {
+public:
+    virtual int32_t read(int32_t length, int32_t offset);
+    virtual int64_t skip(int64_t count);
+    virtual int32_t forceReadEOI();
+
+    // Call this in JNI_OnLoad to cache read/skip method IDs
+    static void setReadSkipMethodIDs(jmethodID readID, jmethodID skipID);
+protected:
+    static jmethodID sReadID;
+    static jmethodID sSkipID;
+};
+
+#endif // INPUTSTREAM_WRAPPER_H_
diff --git a/jni_jpegstream/src/jerr_hook.cpp b/jni_jpegstream/src/jerr_hook.cpp
new file mode 100644
index 0000000..f8f864f
--- /dev/null
+++ b/jni_jpegstream/src/jerr_hook.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+
+#include "jerr_hook.h"
+#include "jni_defines.h"
+
+/**
+ * Replaces libjpeg's error_exit function, returns control to
+ * the point
+ */
+void ErrExit(j_common_ptr cinfo) {
+    ErrManager* mgr = reinterpret_cast<ErrManager*>(cinfo->err);
+    (*cinfo->err->output_message) (cinfo);
+    // Returns control to error handling in jpeg_writer
+    longjmp(mgr->setjmp_buf, 1);
+}
+
+/**
+ * Replaces libjpeg's output_message function, writes message
+ * to logcat's error log.
+ */
+void ErrOutput(j_common_ptr cinfo) {
+    ErrManager* mgr = reinterpret_cast<ErrManager*>(cinfo->err);
+    char buf[JMSG_LENGTH_MAX];
+    (*cinfo->err->format_message) (cinfo, buf);
+    buf[JMSG_LENGTH_MAX - 1] = '\0';  // Force null terminator
+    // Output error message in ndk logcat.
+    LOGE("%s\n", buf);
+}
+
+void SetupErrMgr(j_common_ptr cinfo, ErrManager* errMgr) {
+    jpeg_std_error(&(errMgr->mgr));
+    errMgr->mgr.error_exit = ErrExit;
+    errMgr->mgr.output_message = ErrOutput;
+    cinfo->err = reinterpret_cast<struct jpeg_error_mgr*>(errMgr);
+}
+
+
diff --git a/jni_jpegstream/src/jerr_hook.h b/jni_jpegstream/src/jerr_hook.h
new file mode 100644
index 0000000..f2ba7cd
--- /dev/null
+++ b/jni_jpegstream/src/jerr_hook.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef JERR_HOOK_H_
+#define JERR_HOOK_H_
+
+extern "C" {
+#include "jinclude.h"
+#include "jpeglib.h"
+#include "jerror.h"
+}
+
+#include <setjmp.h>
+
+/**
+ * ErrManager replaces libjpeg's default error handling with
+ * the following behavior:
+ * - libjpeg function calls return to the position set by
+ *   setjmp for error cleanup.
+ * - libjpeg error and warning messages are printed to
+ *   logcat's error output.
+ */
+typedef struct {
+    struct jpeg_error_mgr mgr;
+    jmp_buf setjmp_buf;
+} ErrManager;
+
+void SetupErrMgr(j_common_ptr cinfo, ErrManager* errMgr);
+
+#endif // JERR_HOOK_H_
diff --git a/jni_jpegstream/src/jni_defines.h b/jni_jpegstream/src/jni_defines.h
new file mode 100644
index 0000000..8c9bd04
--- /dev/null
+++ b/jni_jpegstream/src/jni_defines.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef JNIDEFINES_H
+#define JNIDEFINES_H
+
+
+#include <jni.h>
+#include <string.h>
+#include <android/log.h>
+
+#define LOGV(msg...) __android_log_print(ANDROID_LOG_VERBOSE, "Native_JPEGStream", msg)
+#define LOGE(msg...) __android_log_print(ANDROID_LOG_ERROR, "Native_JPEGStream", msg)
+#define LOGW(msg...) __android_log_print(ANDROID_LOG_WARN, "Native_JPEGStream", msg)
+
+#endif // JNIDEFINES_H
diff --git a/jni_jpegstream/src/jpeg_config.h b/jni_jpegstream/src/jpeg_config.h
new file mode 100644
index 0000000..a997552
--- /dev/null
+++ b/jni_jpegstream/src/jpeg_config.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef JPEG_CONFIG_H_
+#define JPEG_CONFIG_H_
+namespace Jpeg_Config {
+
+// Pixel format
+enum Format {
+    FORMAT_GRAYSCALE = 0x001, // 1 byte/pixel
+    FORMAT_RGB = 0x003, // 3 bytes/pixel RGBRGBRGBRGB...
+    FORMAT_RGBA = 0x004, // 4 bytes/pixel RGBARGBARGBARGBA...
+    FORMAT_ABGR = 0x104 // 4 bytes/pixel ABGRABGRABGR...
+};
+
+} // end namespace Jpeg_Config
+
+#endif // JPEG_CONFIG_H_
diff --git a/jni_jpegstream/src/jpeg_hook.cpp b/jni_jpegstream/src/jpeg_hook.cpp
new file mode 100644
index 0000000..cca54e4
--- /dev/null
+++ b/jni_jpegstream/src/jpeg_hook.cpp
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include "error_codes.h"
+#include "jni_defines.h"
+#include "jpeg_hook.h"
+
+#include <stddef.h>
+#include <string.h>
+
+void Mgr_init_destination_fcn(j_compress_ptr cinfo) {
+    DestManager *dst = reinterpret_cast<DestManager*>(cinfo->dest);
+    dst->mgr.next_output_byte = reinterpret_cast<JOCTET*>(dst->outStream->getBufferPtr());
+    dst->mgr.free_in_buffer = dst->outStream->getBufferSize();
+}
+
+boolean Mgr_empty_output_buffer_fcn(j_compress_ptr cinfo) {
+    DestManager *dst = reinterpret_cast<DestManager*>(cinfo->dest);
+    int32_t len = dst->outStream->getBufferSize();
+    if (dst->outStream->write(len, 0) != J_SUCCESS) {
+        ERREXIT(cinfo, JERR_FILE_WRITE);
+    }
+    dst->mgr.next_output_byte = reinterpret_cast<JOCTET*>(dst->outStream->getBufferPtr());
+    dst->mgr.free_in_buffer = len;
+    return TRUE;
+}
+
+void Mgr_term_destination_fcn(j_compress_ptr cinfo) {
+    DestManager *dst = reinterpret_cast<DestManager*>(cinfo->dest);
+    int32_t remaining = dst->outStream->getBufferSize() - dst->mgr.free_in_buffer;
+    if (dst->outStream->write(remaining, 0) != J_SUCCESS) {
+        ERREXIT(cinfo, JERR_FILE_WRITE);
+    }
+}
+
+int32_t MakeDst(j_compress_ptr cinfo, JNIEnv *env, jobject outStream) {
+    if (cinfo->dest != NULL) {
+        LOGE("DestManager already exists, cannot allocate!");
+        return J_ERROR_FATAL;
+    } else {
+        size_t size = sizeof(DestManager);
+        cinfo->dest = (struct jpeg_destination_mgr *) (*cinfo->mem->alloc_small)
+                ((j_common_ptr) cinfo, JPOOL_PERMANENT, size);
+        if (cinfo->dest == NULL) {
+            LOGE("Could not allocate memory for DestManager.");
+            return J_ERROR_FATAL;
+        }
+        memset(cinfo->dest, '0', size);
+    }
+    DestManager *d = reinterpret_cast<DestManager*>(cinfo->dest);
+    d->mgr.init_destination = Mgr_init_destination_fcn;
+    d->mgr.empty_output_buffer = Mgr_empty_output_buffer_fcn;
+    d->mgr.term_destination = Mgr_term_destination_fcn;
+    d->outStream = new OutputStreamWrapper();
+    if(d->outStream->init(env, outStream)) {
+        return J_SUCCESS;
+    }
+    return J_ERROR_FATAL;
+}
+
+void UpdateDstEnv(j_compress_ptr cinfo, JNIEnv* env) {
+    DestManager* d = reinterpret_cast<DestManager*>(cinfo->dest);
+    d->outStream->updateEnv(env);
+}
+
+void CleanDst(j_compress_ptr cinfo) {
+    if (cinfo != NULL && cinfo->dest != NULL) {
+        DestManager *d = reinterpret_cast<DestManager*>(cinfo->dest);
+        if (d->outStream != NULL) {
+            delete d->outStream;
+            d->outStream = NULL;
+        }
+    }
+}
+
+boolean Mgr_fill_input_buffer_fcn(j_decompress_ptr cinfo) {
+    SourceManager *src = reinterpret_cast<SourceManager*>(cinfo->src);
+    int32_t bytesRead = src->inStream->read(src->inStream->getBufferSize(), 0);
+    if (bytesRead == J_DONE) {
+        if (src->start_of_file == TRUE) {
+            ERREXIT(cinfo, JERR_INPUT_EMPTY);
+        }
+        WARNMS(cinfo, JWRN_JPEG_EOF);
+        bytesRead = src->inStream->forceReadEOI();
+    } else if (bytesRead < 0) {
+        ERREXIT(cinfo, JERR_FILE_READ);
+    } else if (bytesRead == 0) {
+        LOGW("read 0 bytes from InputStream.");
+    }
+    src->mgr.next_input_byte = reinterpret_cast<JOCTET*>(src->inStream->getBufferPtr());
+    src->mgr.bytes_in_buffer = bytesRead;
+    if (bytesRead != 0) {
+        src->start_of_file = FALSE;
+    }
+    return TRUE;
+}
+
+void Mgr_init_source_fcn(j_decompress_ptr cinfo) {
+    SourceManager *s = reinterpret_cast<SourceManager*>(cinfo->src);
+    s->start_of_file = TRUE;
+    Mgr_fill_input_buffer_fcn(cinfo);
+}
+
+void Mgr_skip_input_data_fcn(j_decompress_ptr cinfo, long num_bytes) {
+    // Cannot skip negative or 0 bytes.
+    if (num_bytes <= 0) {
+        LOGW("skipping 0 bytes in InputStream");
+        return;
+    }
+    SourceManager *src = reinterpret_cast<SourceManager*>(cinfo->src);
+    if (src->mgr.bytes_in_buffer >= num_bytes) {
+        src->mgr.bytes_in_buffer -= num_bytes;
+        src->mgr.next_input_byte += num_bytes;
+    } else {
+        // if skipping more bytes than remain in buffer, set skip_bytes
+        int64_t skip = num_bytes - src->mgr.bytes_in_buffer;
+        src->mgr.next_input_byte += src->mgr.bytes_in_buffer;
+        src->mgr.bytes_in_buffer = 0;
+        int64_t actual = src->inStream->skip(skip);
+        if (actual < 0) {
+            ERREXIT(cinfo, JERR_FILE_READ);
+        }
+        skip -= actual;
+        while (skip > 0) {
+            actual = src->inStream->skip(skip);
+            if (actual < 0) {
+                ERREXIT(cinfo, JERR_FILE_READ);
+            }
+            skip -= actual;
+            if (actual == 0) {
+                // Multiple zero byte skips, likely EOF
+                WARNMS(cinfo, JWRN_JPEG_EOF);
+                return;
+            }
+        }
+    }
+}
+
+void Mgr_term_source_fcn(j_decompress_ptr cinfo) {
+    //noop
+}
+
+int32_t MakeSrc(j_decompress_ptr cinfo, JNIEnv *env, jobject inStream){
+    if (cinfo->src != NULL) {
+        LOGE("SourceManager already exists, cannot allocate!");
+        return J_ERROR_FATAL;
+    } else {
+        size_t size = sizeof(SourceManager);
+        cinfo->src = (struct jpeg_source_mgr *) (*cinfo->mem->alloc_small)
+                ((j_common_ptr) cinfo, JPOOL_PERMANENT, size);
+        if (cinfo->src == NULL) {
+            // Could not allocate memory.
+            LOGE("Could not allocate memory for SourceManager.");
+            return J_ERROR_FATAL;
+        }
+        memset(cinfo->src, '0', size);
+    }
+    SourceManager *s = reinterpret_cast<SourceManager*>(cinfo->src);
+    s->start_of_file = TRUE;
+    s->mgr.init_source = Mgr_init_source_fcn;
+    s->mgr.fill_input_buffer = Mgr_fill_input_buffer_fcn;
+    s->mgr.skip_input_data = Mgr_skip_input_data_fcn;
+    s->mgr.resync_to_restart = jpeg_resync_to_restart;  // use default restart
+    s->mgr.term_source = Mgr_term_source_fcn;
+    s->inStream = new InputStreamWrapper();
+    if(s->inStream->init(env, inStream)) {
+        return J_SUCCESS;
+    }
+    return J_ERROR_FATAL;
+}
+
+void UpdateSrcEnv(j_decompress_ptr cinfo, JNIEnv* env) {
+    SourceManager* s = reinterpret_cast<SourceManager*>(cinfo->src);
+    s->inStream->updateEnv(env);
+}
+
+void CleanSrc(j_decompress_ptr cinfo) {
+    if (cinfo != NULL && cinfo->src != NULL) {
+        SourceManager *s = reinterpret_cast<SourceManager*>(cinfo->src);
+        if (s->inStream != NULL) {
+            delete s->inStream;
+            s->inStream = NULL;
+        }
+    }
+}
diff --git a/jni_jpegstream/src/jpeg_hook.h b/jni_jpegstream/src/jpeg_hook.h
new file mode 100644
index 0000000..b02bb34
--- /dev/null
+++ b/jni_jpegstream/src/jpeg_hook.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef LIBJPEG_HOOK_H_
+#define LIBJPEG_HOOK_H_
+
+extern "C" {
+#include "jinclude.h"
+#include "jpeglib.h"
+#include "jerror.h"
+}
+
+#include "inputstream_wrapper.h"
+#include "outputstream_wrapper.h"
+
+#include <stdint.h>
+
+/**
+ * DestManager holds the libjpeg destination manager struct and
+ * a holder with a java OutputStream.
+ */
+typedef struct {
+    struct jpeg_destination_mgr mgr;
+    OutputStreamWrapper *outStream;
+} DestManager;
+
+// Initializes the DestManager struct, sets up the jni refs
+int32_t MakeDst(j_compress_ptr cinfo, JNIEnv *env, jobject outStream);
+
+/**
+ * Updates the jni env pointer. This should be called in the beginning of any
+ * JNI method in jpegstream.cpp before CleanDst or any of the libjpeg functions
+ * that can trigger a call to an OutputStreamWrapper method.
+ */
+void UpdateDstEnv(j_compress_ptr cinfo, JNIEnv* env);
+
+// Cleans the jni refs.  To wipe the compress object call jpeg_destroy_compress
+void CleanDst(j_compress_ptr cinfo);
+
+/**
+ * SourceManager holds the libjpeg source manager struct and a
+ * holder with a java InputStream.
+ */
+typedef struct {
+    struct jpeg_source_mgr mgr;
+    boolean start_of_file;
+    InputStreamWrapper *inStream;
+} SourceManager;
+
+// Initializes the SourceManager struct, sets up the jni refs
+int32_t MakeSrc(j_decompress_ptr cinfo, JNIEnv *env, jobject inStream);
+
+/**
+ * Updates the jni env pointer. This should be called in the beginning of any
+ * JNI method in jpegstream.cpp before CleanSrc or any of the libjpeg functions
+ * that can trigger a call to an InputStreamWrapper method.
+ */
+void UpdateSrcEnv(j_decompress_ptr cinfo, JNIEnv* env);
+
+// Cleans the jni refs.  To wipe the decompress object, call jpeg_destroy_decompress
+void CleanSrc(j_decompress_ptr cinfo);
+
+#endif // LIBJPEG_HOOK_H_
diff --git a/jni_jpegstream/src/jpeg_reader.cpp b/jni_jpegstream/src/jpeg_reader.cpp
new file mode 100644
index 0000000..4726b64
--- /dev/null
+++ b/jni_jpegstream/src/jpeg_reader.cpp
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include "jpeg_reader.h"
+#include "error_codes.h"
+#include "jpeg_hook.h"
+
+#include <setjmp.h>
+
+JpegReader::JpegReader() :
+                mInfo(),
+                mErrorManager(),
+                mScanlineBuf(NULL),
+                mScanlineIter(NULL),
+                mScanlineBuflen(0),
+                mScanlineUnformattedBuflen(0),
+                mScanlineBytesRemaining(0),
+                mFormat(),
+                mFinished(false),
+                mSetup(false) {}
+
+JpegReader::~JpegReader() {
+    if (reset() != J_SUCCESS) {
+        LOGE("Failed to destroy compress object, JpegReader may leak memory.");
+    }
+}
+
+int32_t JpegReader::setup(JNIEnv *env, jobject in, int32_t* width, int32_t* height,
+        Jpeg_Config::Format format) {
+    if (mFinished || mSetup) {
+        return J_ERROR_FATAL;
+    }
+    if (env->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+
+    // Setup error handler
+    SetupErrMgr(reinterpret_cast<j_common_ptr>(&mInfo), &mErrorManager);
+    // Set jump address for error handling
+    if (setjmp(mErrorManager.setjmp_buf)) {
+        return J_ERROR_FATAL;
+    }
+
+    // Call libjpeg setup
+    jpeg_create_decompress(&mInfo);
+
+    // Setup our data source object, this allocates java global references
+    int32_t flags = MakeSrc(&mInfo, env, in);
+    if (flags != J_SUCCESS) {
+        LOGE("Failed to make source with error code: %d ", flags);
+        return flags;
+    }
+
+    // Reads jpeg file header
+    jpeg_read_header(&mInfo, TRUE);
+    jpeg_calc_output_dimensions(&mInfo);
+
+    const int components = (static_cast<int>(format) & 0xff);
+
+    // Do setup for input format
+    switch (components) {
+    case 1:
+        mInfo.out_color_space = JCS_GRAYSCALE;
+        mScanlineUnformattedBuflen = mInfo.output_width;
+        break;
+    case 3:
+    case 4:
+        mScanlineUnformattedBuflen = mInfo.output_width * components;
+        if (mInfo.jpeg_color_space == JCS_CMYK
+                || mInfo.jpeg_color_space == JCS_YCCK) {
+            // Always use cmyk for output in a 4 channel jpeg.
+            // libjpeg has a builtin cmyk->rgb decoder.
+            mScanlineUnformattedBuflen = mInfo.output_width * 4;
+            mInfo.out_color_space = JCS_CMYK;
+        } else {
+            mInfo.out_color_space = JCS_RGB;
+        }
+        break;
+    default:
+        return J_ERROR_BAD_ARGS;
+    }
+
+    mScanlineBuflen = mInfo.output_width * components;
+    mScanlineBytesRemaining = mScanlineBuflen;
+    mScanlineBuf = (JSAMPLE *) (mInfo.mem->alloc_small)(
+            reinterpret_cast<j_common_ptr>(&mInfo), JPOOL_PERMANENT,
+            mScanlineUnformattedBuflen * sizeof(JSAMPLE));
+    mScanlineIter = mScanlineBuf;
+    jpeg_start_decompress(&mInfo);
+
+    // Output image dimensions
+    if (width != NULL) {
+        *width = mInfo.output_width;
+    }
+    if (height != NULL) {
+        *height = mInfo.output_height;
+    }
+
+    mFormat = format;
+    mSetup = true;
+    return J_SUCCESS;
+}
+
+int32_t JpegReader::read(int8_t* bytes, int32_t offset, int32_t count) {
+    if (!mSetup) {
+        return J_ERROR_FATAL;
+    }
+    if (mFinished) {
+        return J_DONE;
+    }
+    // Set jump address for error handling
+    if (setjmp(mErrorManager.setjmp_buf)) {
+        return J_ERROR_FATAL;
+    }
+    if (count <= 0) {
+        return J_ERROR_BAD_ARGS;
+    }
+    int32_t total_length = count;
+    while (mInfo.output_scanline < mInfo.output_height) {
+        if (count < mScanlineBytesRemaining) {
+            // read partial scanline and return
+            if (bytes != NULL) {
+                // Treat NULL bytes as a skip
+                memcpy((void*) (bytes + offset), (void*) mScanlineIter,
+                        count * sizeof(int8_t));
+            }
+            mScanlineBytesRemaining -= count;
+            mScanlineIter += count;
+            return total_length;
+        } else if (count > 0) {
+            // read full scanline
+            if (bytes != NULL) {
+                // Treat NULL bytes as a skip
+                memcpy((void*) (bytes + offset), (void*) mScanlineIter,
+                        mScanlineBytesRemaining * sizeof(int8_t));
+                bytes += mScanlineBytesRemaining;
+            }
+            count -= mScanlineBytesRemaining;
+            mScanlineBytesRemaining = 0;
+        }
+        // Scanline buffer exhausted, read next scanline
+        if (jpeg_read_scanlines(&mInfo, &mScanlineBuf, 1) != 1) {
+            // Always read full scanline, no IO suspension
+            return J_ERROR_FATAL;
+        }
+        // Do in-place pixel formatting
+        formatPixels(static_cast<uint8_t*>(mScanlineBuf),
+                mScanlineUnformattedBuflen);
+
+        // Reset iterators
+        mScanlineIter = mScanlineBuf;
+        mScanlineBytesRemaining = mScanlineBuflen;
+    }
+
+    // Read all of the scanlines
+    jpeg_finish_decompress(&mInfo);
+    mFinished = true;
+    return total_length - count;
+}
+
+void JpegReader::updateEnv(JNIEnv *env) {
+    UpdateSrcEnv(&mInfo, env);
+}
+
+// Does in-place pixel formatting
+void JpegReader::formatPixels(uint8_t* buf, int32_t len) {
+    uint8_t *iter = buf;
+
+    // Do cmyk->rgb conversion if necessary
+    switch (mInfo.out_color_space) {
+    case JCS_CMYK:
+        // Convert CMYK to RGB
+        int r, g, b, c, m, y, k;
+        for (int i = 0; i < len; i += 4) {
+            c = buf[i + 0];
+            m = buf[i + 1];
+            y = buf[i + 2];
+            k = buf[i + 3];
+            // Handle fmt for weird photoshop markers
+            if (mInfo.saw_Adobe_marker) {
+                r = (k * c) / 255;
+                g = (k * m) / 255;
+                b = (k * y) / 255;
+            } else {
+                r = (255 - k) * (255 - c) / 255;
+                g = (255 - k) * (255 - m) / 255;
+                b = (255 - k) * (255 - y) / 255;
+            }
+            *iter++ = r;
+            *iter++ = g;
+            *iter++ = b;
+        }
+        break;
+    case JCS_RGB:
+        iter += (len * 3 / 4);
+        break;
+    case JCS_GRAYSCALE:
+    default:
+        return;
+    }
+
+    // Do endianness and alpha for output format
+    if (mFormat == Jpeg_Config::FORMAT_RGBA) {
+        // Set alphas to 255
+        uint8_t* end = buf + len - 1;
+        for (int i = len - 1; i >= 0; i -= 4) {
+            buf[i] = 255;
+            buf[i - 1] = *--iter;
+            buf[i - 2] = *--iter;
+            buf[i - 3] = *--iter;
+        }
+    } else if (mFormat == Jpeg_Config::FORMAT_ABGR) {
+        // Reverse endianness and set alphas to 255
+        uint8_t* end = buf + len - 1;
+        int r, g, b;
+        for (int i = len - 1; i >= 0; i -= 4) {
+            b = *--iter;
+            g = *--iter;
+            r = *--iter;
+            buf[i] = r;
+            buf[i - 1] = g;
+            buf[i - 2] = b;
+            buf[i - 3] = 255;
+        }
+    }
+}
+
+int32_t JpegReader::reset() {
+    // Set jump address for error handling
+    if (setjmp(mErrorManager.setjmp_buf)) {
+        return J_ERROR_FATAL;
+    }
+    // Clean up global java references
+    CleanSrc(&mInfo);
+    // Wipe decompress struct, free memory pools
+    jpeg_destroy_decompress(&mInfo);
+    mFinished = false;
+    mSetup = false;
+    return J_SUCCESS;
+}
+
diff --git a/jni_jpegstream/src/jpeg_reader.h b/jni_jpegstream/src/jpeg_reader.h
new file mode 100644
index 0000000..afde27b
--- /dev/null
+++ b/jni_jpegstream/src/jpeg_reader.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+#ifndef JPEG_READER_H_
+#define JPEG_READER_H_
+
+#include "jerr_hook.h"
+#include "jni_defines.h"
+#include "jpeg_config.h"
+
+#include <stdint.h>
+
+/**
+ * JpegReader wraps libjpeg's decompression functionality and a
+ * java InputStream object.  Read calls return data from the
+ * InputStream that has been decompressed.
+ */
+class JpegReader {
+public:
+    JpegReader();
+    ~JpegReader();
+
+    /**
+     * Call setup with a valid InputStream reference and pixel format.
+     * If this method is successful, the contents of width and height will
+     * be set to the dimensions of the bitmap to be read.
+     *
+     * ***This method will result in the jpeg file header being read
+     * from the InputStream***
+     *
+     * Returns J_SUCCESS on success or a negative error code.
+     */
+    int32_t setup(JNIEnv *env, jobject in, int32_t* width, int32_t* height,
+            Jpeg_Config::Format format);
+
+    /**
+     * Decompresses bytes from the InputStream and writes at most count
+     * bytes into the buffer, bytes, starting at some offset.  Passing a
+     * NULL as the bytes pointer effectively skips those bytes.
+     *
+     * ***This method will result in bytes being read from the InputStream***
+     *
+     * Returns the number of bytes written into the input buffer or a
+     * negative error code.
+     */
+    int32_t read(int8_t * bytes, int32_t offset, int32_t count);
+
+    /**
+     * Updates the environment pointer.  Call this before read or reset
+     * in any jni function call.
+     */
+    void updateEnv(JNIEnv *env);
+
+    /**
+     * Frees any java global references held by the JpegReader, destroys
+     * the decompress structure, and frees allocations in libjpeg's pools.
+     */
+    int32_t reset();
+
+private:
+    void formatPixels(uint8_t* buf, int32_t len);
+    struct jpeg_decompress_struct mInfo;
+    ErrManager mErrorManager;
+
+    JSAMPLE* mScanlineBuf;
+    JSAMPLE* mScanlineIter;
+    int32_t mScanlineBuflen;
+    int32_t mScanlineUnformattedBuflen;
+    int32_t mScanlineBytesRemaining;
+
+    Jpeg_Config::Format mFormat;
+    bool mFinished;
+    bool mSetup;
+};
+
+#endif // JPEG_READER_H_
diff --git a/jni_jpegstream/src/jpeg_writer.cpp b/jni_jpegstream/src/jpeg_writer.cpp
new file mode 100644
index 0000000..4f78917
--- /dev/null
+++ b/jni_jpegstream/src/jpeg_writer.cpp
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include "jpeg_hook.h"
+#include "jpeg_writer.h"
+#include "error_codes.h"
+
+#include <setjmp.h>
+#include <assert.h>
+
+JpegWriter::JpegWriter() : mInfo(),
+                           mErrorManager(),
+                           mScanlineBuf(NULL),
+                           mScanlineIter(NULL),
+                           mScanlineBuflen(0),
+                           mScanlineBytesRemaining(0),
+                           mFormat(),
+                           mFinished(false),
+                           mSetup(false) {}
+
+JpegWriter::~JpegWriter() {
+    if (reset() != J_SUCCESS) {
+        LOGE("Failed to destroy compress object, may leak memory.");
+    }
+}
+
+const int32_t JpegWriter::DEFAULT_X_DENSITY = 300;
+const int32_t JpegWriter::DEFAULT_Y_DENSITY = 300;
+const int32_t JpegWriter::DEFAULT_DENSITY_UNIT = 1;
+
+int32_t JpegWriter::setup(JNIEnv *env, jobject out, int32_t width, int32_t height,
+        Jpeg_Config::Format format, int32_t quality) {
+    if (mFinished || mSetup) {
+        return J_ERROR_FATAL;
+    }
+    if (env->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    if (height <= 0 || width <= 0 || quality <= 0 || quality > 100) {
+        return J_ERROR_BAD_ARGS;
+    }
+    // Setup error handler
+    SetupErrMgr(reinterpret_cast<j_common_ptr>(&mInfo), &mErrorManager);
+
+    // Set jump address for error handling
+    if (setjmp(mErrorManager.setjmp_buf)) {
+        return J_ERROR_FATAL;
+    }
+
+    // Setup cinfo struct
+    jpeg_create_compress(&mInfo);
+
+    // Setup global java refs
+    int32_t flags = MakeDst(&mInfo, env, out);
+    if (flags != J_SUCCESS) {
+        return flags;
+    }
+
+    // Initialize width, height, and color space
+    mInfo.image_width = width;
+    mInfo.image_height = height;
+    const int components = (static_cast<int>(format) & 0xff);
+    switch (components) {
+    case 1:
+        mInfo.input_components = 1;
+        mInfo.in_color_space = JCS_GRAYSCALE;
+        break;
+    case 3:
+    case 4:
+        mInfo.input_components = 3;
+        mInfo.in_color_space = JCS_RGB;
+        break;
+    default:
+        return J_ERROR_BAD_ARGS;
+    }
+
+    // Set defaults
+    jpeg_set_defaults(&mInfo);
+    mInfo.density_unit = DEFAULT_DENSITY_UNIT; // JFIF code for pixel size units:
+                             // 1 = in, 2 = cm
+    mInfo.X_density = DEFAULT_X_DENSITY; // Horizontal pixel density
+    mInfo.Y_density = DEFAULT_Y_DENSITY; // Vertical pixel density
+
+    // Set compress quality
+    jpeg_set_quality(&mInfo, quality, TRUE);
+
+    mFormat = format;
+
+    // Setup scanline buffer
+    mScanlineBuflen = width * components;
+    mScanlineBytesRemaining = mScanlineBuflen;
+    mScanlineBuf = (JSAMPLE *) (mInfo.mem->alloc_small)(
+            reinterpret_cast<j_common_ptr>(&mInfo), JPOOL_PERMANENT,
+            mScanlineBuflen * sizeof(JSAMPLE));
+    mScanlineIter = mScanlineBuf;
+
+    // Start compression
+    jpeg_start_compress(&mInfo, TRUE);
+    mSetup = true;
+    return J_SUCCESS;
+}
+
+int32_t JpegWriter::write(int8_t* bytes, int32_t length) {
+    if (!mSetup) {
+        return J_ERROR_FATAL;
+    }
+    if (mFinished) {
+        return 0;
+    }
+    // Set jump address for error handling
+    if (setjmp(mErrorManager.setjmp_buf)) {
+        return J_ERROR_FATAL;
+    }
+    if (length < 0 || bytes == NULL) {
+        return J_ERROR_BAD_ARGS;
+    }
+
+    int32_t total_length = length;
+    JSAMPROW row_pointer[1];
+    while (mInfo.next_scanline < mInfo.image_height) {
+        if (length < mScanlineBytesRemaining) {
+            // read partial scanline and return
+            memcpy((void*) mScanlineIter, (void*) bytes,
+                    length * sizeof(int8_t));
+            mScanlineBytesRemaining -= length;
+            mScanlineIter += length;
+            return total_length;
+        } else if (length > 0) {
+            // read full scanline
+            memcpy((void*) mScanlineIter, (void*) bytes,
+                    mScanlineBytesRemaining * sizeof(int8_t));
+            bytes += mScanlineBytesRemaining;
+            length -= mScanlineBytesRemaining;
+            mScanlineBytesRemaining = 0;
+        }
+        // Do in-place pixel formatting
+        formatPixels(static_cast<uint8_t*>(mScanlineBuf), mScanlineBuflen);
+        row_pointer[0] = mScanlineBuf;
+        // Do compression
+        if (jpeg_write_scanlines(&mInfo, row_pointer, 1) != 1) {
+            return J_ERROR_FATAL;
+        }
+        // Reset scanline buffer
+        mScanlineBytesRemaining = mScanlineBuflen;
+        mScanlineIter = mScanlineBuf;
+    }
+    jpeg_finish_compress(&mInfo);
+    mFinished = true;
+    return total_length - length;
+}
+
+// Does in-place pixel formatting
+void JpegWriter::formatPixels(uint8_t* buf, int32_t len) {
+    //  Assumes len is a multiple of 4 for RGBA and ABGR pixels.
+    assert((len % 4) == 0);
+    uint8_t* d = buf;
+    switch (mFormat) {
+    case Jpeg_Config::FORMAT_RGBA: {
+        // Strips alphas
+        for (int i = 0; i < len / 4; ++i, buf += 4) {
+            *d++ = buf[0];
+            *d++ = buf[1];
+            *d++ = buf[2];
+        }
+        break;
+    }
+    case Jpeg_Config::FORMAT_ABGR: {
+        // Strips alphas and flips endianness
+        if (len / 4 >= 1) {
+            *d++ = buf[3];
+            uint8_t tmp = *d;
+            *d++ = buf[2];
+            *d++ = tmp;
+        }
+        for (int i = 1; i < len / 4; ++i, buf += 4) {
+            *d++ = buf[3];
+            *d++ = buf[2];
+            *d++ = buf[1];
+        }
+        break;
+    }
+    default: {
+        // Do nothing
+        break;
+    }
+    }
+}
+
+void JpegWriter::updateEnv(JNIEnv *env) {
+    UpdateDstEnv(&mInfo, env);
+}
+
+int32_t JpegWriter::reset() {
+    // Set jump address for error handling
+    if (setjmp(mErrorManager.setjmp_buf)) {
+        return J_ERROR_FATAL;
+    }
+    // Clean up global java references
+    CleanDst(&mInfo);
+    // Wipe compress struct, free memory pools
+    jpeg_destroy_compress(&mInfo);
+    mFinished = false;
+    mSetup = false;
+    return J_SUCCESS;
+}
diff --git a/jni_jpegstream/src/jpeg_writer.h b/jni_jpegstream/src/jpeg_writer.h
new file mode 100644
index 0000000..bd9a42d
--- /dev/null
+++ b/jni_jpegstream/src/jpeg_writer.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+#ifndef JPEG_WRITER_H_
+#define JPEG_WRITER_H_
+
+#include "jerr_hook.h"
+#include "jni_defines.h"
+#include "jpeg_config.h"
+
+#include <stdint.h>
+
+/**
+ * JpegWriter wraps libjpeg's compression functionality and a
+ * java OutputStream object.  Write calls result in input data
+ * being compressed and written to the OuputStream.
+ */
+class JpegWriter {
+public:
+    JpegWriter();
+    ~JpegWriter();
+
+    /**
+     * Call setup with a valid OutputStream reference, bitmap height and
+     * width, pixel format, and compression quality in range (0, 100].
+     *
+     * Returns J_SUCCESS on success or a negative error code.
+     */
+    int32_t setup(JNIEnv *env, jobject out, int32_t width, int32_t height,
+            Jpeg_Config::Format format, int32_t quality);
+
+    /**
+     * Compresses bytes from the input buffer.
+     *
+     * ***This method will result in bytes being written to the OutputStream***
+     *
+     * Returns J_SUCCESS on success or a negative error code.
+     */
+    int32_t write(int8_t* bytes, int32_t length);
+
+    /**
+     * Updates the environment pointer.  Call this before write or reset
+     * in any jni function call.
+     */
+    void updateEnv(JNIEnv *env);
+
+    /**
+     * Frees any java global references held by the JpegWriter, destroys
+     * the compress structure, and frees allocations in libjpeg's pools.
+     */
+    int32_t reset();
+
+    static const int32_t DEFAULT_X_DENSITY;
+    static const int32_t DEFAULT_Y_DENSITY;
+    static const int32_t DEFAULT_DENSITY_UNIT;
+private:
+    void formatPixels(uint8_t* buf, int32_t len);
+    struct jpeg_compress_struct mInfo;
+    ErrManager mErrorManager;
+
+    JSAMPLE* mScanlineBuf;
+    JSAMPLE* mScanlineIter;
+    int32_t mScanlineBuflen;
+    int32_t mScanlineBytesRemaining;
+
+    Jpeg_Config::Format mFormat;
+    bool mFinished;
+    bool mSetup;
+};
+
+#endif // JPEG_WRITER_H_
diff --git a/jni_jpegstream/src/jpegstream.cpp b/jni_jpegstream/src/jpegstream.cpp
new file mode 100644
index 0000000..3b9a683
--- /dev/null
+++ b/jni_jpegstream/src/jpegstream.cpp
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include "error_codes.h"
+#include "jni_defines.h"
+#include "jpeg_writer.h"
+#include "jpeg_reader.h"
+#include "jpeg_config.h"
+#include "outputstream_wrapper.h"
+#include "inputstream_wrapper.h"
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static jint OutputStream_setup(JNIEnv* env, jobject thiz, jobject out,
+        jint width, jint height, jint format, jint quality) {
+    // Get a reference to this object's class
+    jclass thisClass = env->GetObjectClass(thiz);
+    if (env->ExceptionCheck() || thisClass == NULL) {
+        return J_EXCEPTION;
+    }
+    // Get field for storing C pointer
+    jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J");
+    if (NULL == fidNumber || env->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+
+    // Check size
+    if (width <= 0 || height <= 0) {
+        return J_ERROR_BAD_ARGS;
+    }
+    Jpeg_Config::Format fmt = static_cast<Jpeg_Config::Format>(format);
+    // Check format
+    switch (fmt) {
+    case Jpeg_Config::FORMAT_GRAYSCALE:
+    case Jpeg_Config::FORMAT_RGB:
+    case Jpeg_Config::FORMAT_RGBA:
+    case Jpeg_Config::FORMAT_ABGR:
+        break;
+    default:
+        return J_ERROR_BAD_ARGS;
+    }
+
+    uint32_t w = static_cast<uint32_t>(width);
+    uint32_t h = static_cast<uint32_t>(height);
+    int32_t q = static_cast<int32_t>(quality);
+    // Clamp quality to (0, 100]
+    q = (q > 100) ? 100 : ((q < 1) ? 1 : q);
+
+    JpegWriter* w_ptr = new JpegWriter();
+
+    // Do JpegWriter setup.
+    int32_t errorFlag = w_ptr->setup(env, out, w, h, fmt, q);
+    if (env->ExceptionCheck() || errorFlag != J_SUCCESS) {
+        delete w_ptr;
+        return errorFlag;
+    }
+
+    // Store C pointer for writer
+    env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(w_ptr));
+    if (env->ExceptionCheck()) {
+        delete w_ptr;
+        return J_EXCEPTION;
+    }
+    return J_SUCCESS;
+}
+
+static jint InputStream_setup(JNIEnv* env, jobject thiz, jobject dimens,
+        jobject in, jint format) {
+    // Get a reference to this object's class
+    jclass thisClass = env->GetObjectClass(thiz);
+    if (env->ExceptionCheck() || thisClass == NULL) {
+        return J_EXCEPTION;
+    }
+    jmethodID setMethod = NULL;
+
+    // Get dimensions object setter method
+    if (dimens != NULL) {
+        jclass pointClass = env->GetObjectClass(dimens);
+        if (env->ExceptionCheck() || pointClass == NULL) {
+            return J_EXCEPTION;
+        }
+        setMethod = env->GetMethodID(pointClass, "set", "(II)V");
+        if (env->ExceptionCheck() || setMethod == NULL) {
+            return J_EXCEPTION;
+        }
+    }
+    // Get field for storing C pointer
+    jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J");
+    if (NULL == fidNumber || env->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    Jpeg_Config::Format fmt = static_cast<Jpeg_Config::Format>(format);
+    // Check format
+    switch (fmt) {
+    case Jpeg_Config::FORMAT_GRAYSCALE:
+    case Jpeg_Config::FORMAT_RGB:
+    case Jpeg_Config::FORMAT_RGBA:
+    case Jpeg_Config::FORMAT_ABGR:
+        break;
+    default:
+        return J_ERROR_BAD_ARGS;
+    }
+
+    JpegReader* r_ptr = new JpegReader();
+    int32_t w = 0, h = 0;
+    // Do JpegReader setup.
+    int32_t errorFlag = r_ptr->setup(env, in, &w, &h, fmt);
+    if (env->ExceptionCheck() || errorFlag != J_SUCCESS) {
+        delete r_ptr;
+        return errorFlag;
+    }
+
+    // Set dimensions to return
+    if (dimens != NULL) {
+        env->CallVoidMethod(dimens, setMethod, static_cast<jint>(w),
+                static_cast<jint>(h));
+        if (env->ExceptionCheck()) {
+            delete r_ptr;
+            return J_EXCEPTION;
+        }
+    }
+    // Store C pointer for reader
+    env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(r_ptr));
+    if (env->ExceptionCheck()) {
+        delete r_ptr;
+        return J_EXCEPTION;
+    }
+    return J_SUCCESS;
+}
+
+static JpegWriter* getWPtr(JNIEnv* env, jobject thiz, jfieldID* fid) {
+    jclass thisClass = env->GetObjectClass(thiz);
+    if (env->ExceptionCheck() || thisClass == NULL) {
+        return NULL;
+    }
+    jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J");
+    if (NULL == fidNumber || env->ExceptionCheck()) {
+        return NULL;
+    }
+    jlong ptr = env->GetLongField(thiz, fidNumber);
+    if (env->ExceptionCheck()) {
+        return NULL;
+    }
+    // Get writer C pointer out of java field.
+    JpegWriter* w_ptr = reinterpret_cast<JpegWriter*>(ptr);
+    if (fid != NULL) {
+        *fid = fidNumber;
+    }
+    return w_ptr;
+}
+
+static JpegReader* getRPtr(JNIEnv* env, jobject thiz, jfieldID* fid) {
+    jclass thisClass = env->GetObjectClass(thiz);
+    if (env->ExceptionCheck() || thisClass == NULL) {
+        return NULL;
+    }
+    jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J");
+    if (NULL == fidNumber || env->ExceptionCheck()) {
+        return NULL;
+    }
+    jlong ptr = env->GetLongField(thiz, fidNumber);
+    if (env->ExceptionCheck()) {
+        return NULL;
+    }
+    // Get reader C pointer out of java field.
+    JpegReader* r_ptr = reinterpret_cast<JpegReader*>(ptr);
+    if (fid != NULL) {
+        *fid = fidNumber;
+    }
+    return r_ptr;
+}
+
+static void OutputStream_cleanup(JNIEnv* env, jobject thiz) {
+    jfieldID fidNumber = NULL;
+    JpegWriter* w_ptr = getWPtr(env, thiz, &fidNumber);
+    if (w_ptr == NULL) {
+        return;
+    }
+    // Update environment
+    w_ptr->updateEnv(env);
+    // Destroy writer object
+    delete w_ptr;
+    w_ptr = NULL;
+    // Set the java field to null
+    env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(w_ptr));
+}
+
+static void InputStream_cleanup(JNIEnv* env, jobject thiz) {
+    jfieldID fidNumber = NULL;
+    JpegReader* r_ptr = getRPtr(env, thiz, &fidNumber);
+    if (r_ptr == NULL) {
+        return;
+    }
+    // Update environment
+    r_ptr->updateEnv(env);
+    // Destroy the reader object
+    delete r_ptr;
+    r_ptr = NULL;
+    // Set the java field to null
+    env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(r_ptr));
+}
+
+static jint OutputStream_writeInputBytes(JNIEnv* env, jobject thiz,
+        jbyteArray inBuffer, jint offset, jint inCount) {
+    JpegWriter* w_ptr = getWPtr(env, thiz, NULL);
+    if (w_ptr == NULL) {
+        return J_EXCEPTION;
+    }
+    // Pin input buffer
+    jbyte* in_buf = (jbyte*) env->GetByteArrayElements(inBuffer, 0);
+    if (env->ExceptionCheck() || in_buf == NULL) {
+        return J_EXCEPTION;
+    }
+
+    int8_t* in_bytes = static_cast<int8_t*>(in_buf);
+    int32_t in_len = static_cast<int32_t>(inCount);
+    int32_t off = static_cast<int32_t>(offset);
+    in_bytes += off;
+    int32_t written = 0;
+
+    // Update environment
+    w_ptr->updateEnv(env);
+    // Write out and unpin buffer.
+    written = w_ptr->write(in_bytes, in_len);
+    env->ReleaseByteArrayElements(inBuffer, in_buf, JNI_ABORT);
+    return written;
+}
+
+static jint InputStream_readDecodedBytes(JNIEnv* env, jobject thiz,
+        jbyteArray inBuffer, jint offset, jint inCount) {
+    JpegReader* r_ptr = getRPtr(env, thiz, NULL);
+    if (r_ptr == NULL) {
+        return J_EXCEPTION;
+    }
+    // Pin input buffer
+    jbyte* in_buf = (jbyte*) env->GetByteArrayElements(inBuffer, 0);
+    if (env->ExceptionCheck() || in_buf == NULL) {
+        return J_EXCEPTION;
+    }
+    int8_t* in_bytes = static_cast<int8_t*>(in_buf);
+    int32_t in_len = static_cast<int32_t>(inCount);
+    int32_t off = static_cast<int32_t>(offset);
+    int32_t read = 0;
+
+    // Update environment
+    r_ptr->updateEnv(env);
+    // Read into buffer
+    read = r_ptr->read(in_bytes, off, in_len);
+
+    // Unpin buffer
+    if (read < 0) {
+        env->ReleaseByteArrayElements(inBuffer, in_buf, JNI_ABORT);
+    } else {
+        env->ReleaseByteArrayElements(inBuffer, in_buf, JNI_COMMIT);
+    }
+    return read;
+}
+
+static jint InputStream_skipDecodedBytes(JNIEnv* env, jobject thiz,
+        jint bytes) {
+    if (bytes <= 0) {
+        return J_ERROR_BAD_ARGS;
+    }
+    JpegReader* r_ptr = getRPtr(env, thiz, NULL);
+    if (r_ptr == NULL) {
+        return J_EXCEPTION;
+    }
+
+    // Update environment
+    r_ptr->updateEnv(env);
+    int32_t skip = 0;
+    // Read with null buffer to skip
+    skip = r_ptr->read(NULL, 0, bytes);
+    return skip;
+}
+
+static const char *outClassPathName =
+        "com/android/gallery3d/jpegstream/JPEGOutputStream";
+static const char *inClassPathName =
+        "com/android/gallery3d/jpegstream/JPEGInputStream";
+
+static JNINativeMethod writeMethods[] = { { "setup",
+        "(Ljava/io/OutputStream;IIII)I", (void*) OutputStream_setup }, {
+        "cleanup", "()V", (void*) OutputStream_cleanup }, { "writeInputBytes",
+        "([BII)I", (void*) OutputStream_writeInputBytes } };
+
+static JNINativeMethod readMethods[] = { { "setup",
+        "(Landroid/graphics/Point;Ljava/io/InputStream;I)I",
+        (void*) InputStream_setup }, { "cleanup", "()V",
+        (void*) InputStream_cleanup }, { "readDecodedBytes", "([BII)I",
+        (void*) InputStream_readDecodedBytes }, { "skipDecodedBytes", "(I)I",
+        (void*) InputStream_skipDecodedBytes } };
+
+static int registerNativeMethods(JNIEnv* env, const char* className,
+        JNINativeMethod* gMethods, int numMethods) {
+    jclass clazz;
+    clazz = env->FindClass(className);
+    if (clazz == NULL) {
+        LOGE("Native registration unable to find class '%s'", className);
+        return JNI_FALSE;
+    }
+    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
+        LOGE("RegisterNatives failed for '%s'", className);
+        return JNI_FALSE;
+    }
+    return JNI_TRUE;
+}
+
+jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+    JNIEnv* env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        LOGE("Error: GetEnv failed in JNI_OnLoad");
+        return -1;
+    }
+    if (!registerNativeMethods(env, outClassPathName, writeMethods,
+            sizeof(writeMethods) / sizeof(writeMethods[0]))) {
+        LOGE("Error: could not register native methods for JPEGOutputStream");
+        return -1;
+    }
+    if (!registerNativeMethods(env, inClassPathName, readMethods,
+            sizeof(readMethods) / sizeof(readMethods[0]))) {
+        LOGE("Error: could not register native methods for JPEGInputStream");
+        return -1;
+    }
+    // cache method IDs for OutputStream
+    jclass outCls = env->FindClass("java/io/OutputStream");
+    if (outCls == NULL) {
+        LOGE("Unable to find class 'OutputStream'");
+        return -1;
+    }
+    jmethodID cachedWriteFun = env->GetMethodID(outCls, "write", "([BII)V");
+    if (cachedWriteFun == NULL) {
+        LOGE("Unable to find write function in class 'OutputStream'");
+        return -1;
+    }
+    OutputStreamWrapper::setWriteMethodID(cachedWriteFun);
+
+    // cache method IDs for InputStream
+    jclass inCls = env->FindClass("java/io/InputStream");
+    if (inCls == NULL) {
+        LOGE("Unable to find class 'InputStream'");
+        return -1;
+    }
+    jmethodID cachedReadFun = env->GetMethodID(inCls, "read", "([BII)I");
+    if (cachedReadFun == NULL) {
+        LOGE("Unable to find read function in class 'InputStream'");
+        return -1;
+    }
+    jmethodID cachedSkipFun = env->GetMethodID(inCls, "skip", "(J)J");
+    if (cachedSkipFun == NULL) {
+        LOGE("Unable to find skip function in class 'InputStream'");
+        return -1;
+    }
+    InputStreamWrapper::setReadSkipMethodIDs(cachedReadFun, cachedSkipFun);
+    return JNI_VERSION_1_6;
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/jni_jpegstream/src/outputstream_wrapper.cpp b/jni_jpegstream/src/outputstream_wrapper.cpp
new file mode 100644
index 0000000..0639b6e
--- /dev/null
+++ b/jni_jpegstream/src/outputstream_wrapper.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include "outputstream_wrapper.h"
+#include "error_codes.h"
+
+jmethodID OutputStreamWrapper::sWriteID = NULL;
+
+int32_t OutputStreamWrapper::write(int32_t length, int32_t offset) {
+    if (offset < 0 || length < 0 || (offset + length) > getBufferSize()) {
+        return J_ERROR_BAD_ARGS;
+    }
+    mEnv->ReleaseByteArrayElements(mByteArray, mBytes, JNI_COMMIT);
+    mBytes = NULL;
+    if (mEnv->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    if (sWriteID == NULL) {
+        LOGE("Uninitialized method ID for OutputStream write function.");
+        return J_ERROR_FATAL;
+    }
+    // Call OutputStream write with byte array.
+    mEnv->CallVoidMethod(mStream, sWriteID, mByteArray, offset, length);
+    if (mEnv->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    mBytes = mEnv->GetByteArrayElements(mByteArray, NULL);
+    if (mBytes == NULL || mEnv->ExceptionCheck()) {
+        return J_EXCEPTION;
+    }
+    return J_SUCCESS;
+}
+
+void OutputStreamWrapper::setWriteMethodID(jmethodID id) {
+    sWriteID = id;
+}
diff --git a/jni_jpegstream/src/outputstream_wrapper.h b/jni_jpegstream/src/outputstream_wrapper.h
new file mode 100644
index 0000000..9b8b007
--- /dev/null
+++ b/jni_jpegstream/src/outputstream_wrapper.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef OUTPUTSTREAM_WRAPPER_H_
+#define OUTPUTSTREAM_WRAPPER_H_
+
+#include "jni_defines.h"
+#include "stream_wrapper.h"
+
+#include <stdint.h>
+
+class OutputStreamWrapper : public StreamWrapper {
+public:
+    virtual int32_t write(int32_t length, int32_t offset);
+
+    // Call this in JNI_OnLoad to cache write method
+    static void setWriteMethodID(jmethodID id);
+protected:
+    static jmethodID sWriteID;
+};
+
+#endif // OUTPUTSTREAM_WRAPPER_H_
diff --git a/jni_jpegstream/src/stream_wrapper.cpp b/jni_jpegstream/src/stream_wrapper.cpp
new file mode 100644
index 0000000..049d84f
--- /dev/null
+++ b/jni_jpegstream/src/stream_wrapper.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#include "stream_wrapper.h"
+
+const int32_t StreamWrapper::END_OF_STREAM = -1;
+const int32_t StreamWrapper::DEFAULT_BUFFER_SIZE = 1 << 16;  // 64Kb
+
+StreamWrapper::StreamWrapper() : mEnv(NULL),
+                                 mStream(NULL),
+                                 mByteArray(NULL),
+                                 mBytes(NULL),
+                                 mByteArrayLen(0) {}
+
+StreamWrapper::~StreamWrapper() {
+    cleanup();
+}
+
+void StreamWrapper::updateEnv(JNIEnv *env) {
+    if (env == NULL) {
+        LOGE("Cannot update StreamWrapper with a null JNIEnv pointer!");
+        return;
+    }
+    mEnv = env;
+}
+
+bool StreamWrapper::init(JNIEnv *env, jobject stream) {
+    if (mEnv != NULL) {
+        LOGW("StreamWrapper already initialized!");
+        return false;
+    }
+    mEnv = env;
+    mStream = env->NewGlobalRef(stream);
+    if (mStream == NULL || env->ExceptionCheck()) {
+        cleanup();
+        return false;
+    }
+    mByteArrayLen = DEFAULT_BUFFER_SIZE;
+    jbyteArray tmp = env->NewByteArray(getBufferSize());
+    if (tmp == NULL || env->ExceptionCheck()){
+        cleanup();
+        return false;
+    }
+    mByteArray = reinterpret_cast<jbyteArray>(env->NewGlobalRef(tmp));
+    if (mByteArray == NULL || env->ExceptionCheck()){
+        cleanup();
+        return false;
+    }
+    mBytes = env->GetByteArrayElements(mByteArray, NULL);
+    if (mBytes == NULL || env->ExceptionCheck()){
+        cleanup();
+        return false;
+    }
+    return true;
+}
+
+void StreamWrapper::cleanup() {
+    if (mEnv != NULL) {
+        if (mStream != NULL) {
+            mEnv->DeleteGlobalRef(mStream);
+            mStream = NULL;
+        }
+        if (mByteArray != NULL) {
+            if (mBytes != NULL) {
+                mEnv->ReleaseByteArrayElements(mByteArray, mBytes, JNI_ABORT);
+                mBytes = NULL;
+            }
+            mEnv->DeleteGlobalRef(mByteArray);
+            mByteArray = NULL;
+        } else {
+            mBytes = NULL;
+        }
+        mByteArrayLen = 0;
+        mEnv = NULL;
+    }
+}
+
+int32_t StreamWrapper::getBufferSize() {
+    return mByteArrayLen;
+}
+
+jbyte* StreamWrapper::getBufferPtr() {
+    return mBytes;
+}
diff --git a/jni_jpegstream/src/stream_wrapper.h b/jni_jpegstream/src/stream_wrapper.h
new file mode 100644
index 0000000..e036a91
--- /dev/null
+++ b/jni_jpegstream/src/stream_wrapper.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+#ifndef STREAM_WRAPPER_H_
+#define STREAM_WRAPPER_H_
+
+#include "jni_defines.h"
+
+#include <stdint.h>
+
+class StreamWrapper {
+public:
+    StreamWrapper();
+    virtual ~StreamWrapper();
+    virtual void updateEnv(JNIEnv *env);
+    virtual bool init(JNIEnv *env, jobject stream);
+    virtual void cleanup();
+    virtual int32_t getBufferSize();
+    virtual jbyte* getBufferPtr();
+
+    const static int32_t DEFAULT_BUFFER_SIZE;
+    const static int32_t END_OF_STREAM;
+protected:
+    JNIEnv *mEnv;
+    jobject mStream;
+    jbyteArray mByteArray;
+    jbyte* mBytes;
+    int32_t mByteArrayLen;
+};
+
+#endif // STREAM_WRAPPER_H_
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 0000000..65104ec
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1,86 @@
+# Disable the warnings of using dynamic method call in common library.
+-dontnote com.android.gallery3d.common.*
+
+# Keep all classes extended from com.android.gallery3d.common.Entry
+# Since we annotate on the fields and use reflection to create SQL
+# according to those field.
+
+-keep class * extends com.android.gallery3d.common.Entry {
+    @com.android.gallery3d.common.Entry$Column <fields>;
+}
+
+# ctors of subclasses of CameraPreference are called with Java reflection.
+-keep class * extends com.android.camera.CameraPreference {
+  <init>(...);
+}
+
+-keep class com.android.camera.CameraActivity {
+  public boolean isRecording();
+  public long getAutoFocusTime();
+  public long getShutterLag();
+  public long getShutterToPictureDisplayedTime();
+  public long getPictureDisplayedToJpegCallbackTime();
+  public long getJpegCallbackFinishTime();
+  public long getCaptureStartTime();
+}
+
+-keep class com.android.camera.VideoModule {
+  public void onCancelBgTraining(...);
+  public void onProtectiveCurtainClick(...);
+}
+
+-keep class * extends android.app.Activity {
+  @com.android.camera.OnClickAttr <methods>;
+}
+
+-keep class com.android.camera.CameraHolder {
+  public static void injectMockCamera(...);
+}
+
+# Disable the warnings of using dynamic method calls in EffectsRecorder
+-dontnote com.android.camera.EffectsRecorder
+
+-keep class android.support.v8.renderscript.** { *; }
+
+# Required for ActionBarSherlock
+-keep class android.support.v4.app.** { *; }
+-keep interface android.support.v4.app.** { *; }
+-keep class com.actionbarsherlock.** { *; }
+-keep interface com.actionbarsherlock.** { *; }
+-keepattributes *Annotation*
+
+# Required for mp4parser
+-keep public class * implements com.coremedia.iso.boxes.Box
+
+#-assumenosideeffects junit.framework.Assert {
+#*;
+#}
+
+# For unit testing:
+
+# - Required for running exif tests on userdebug
+-keep class com.android.gallery3d.exif.ExifTag { *; }
+-keep class com.android.gallery3d.exif.ExifData { *; }
+-keep class com.android.gallery3d.exif.ExifInterface { *; }
+-keepclassmembers class com.android.gallery3d.exif.Util {
+  *** closeSilently(...);
+}
+
+# - Required for running blobcache tests on userdebug
+-keep class com.android.gallery3d.common.BlobCache { *; }
+
+# - Required for running glcanvas tests on userdebug
+-keep class com.android.gallery3d.ui.GLPaint { *; }
+-keep class com.android.gallery3d.ui.GLCanvas { *; }
+-keep class com.android.gallery3d.glrenderer.GLPaint { *; }
+-keep class com.android.gallery3d.glrenderer.GLCanvas { *; }
+-keep class com.android.gallery3d.ui.GLView { *; }
+-keepclassmembers class com.android.gallery3d.util.IntArray {
+  *** toArray(...);
+}
+-keep class com.android.gallery3d.util.ProfileData { *; }
+
+# - Required for running jpeg stream tests on userdebug
+-keep class com.android.gallery3d.jpegstream.JPEGOutputStream { *; }
+-keep class com.android.gallery3d.jpegstream.JPEGInputStream { *; }
+-keep class com.android.gallery3d.jpegstream.StreamUtils { *; }
diff --git a/res/anim/count_down_exit.xml b/res/anim/count_down_exit.xml
new file mode 100644
index 0000000..0091c5b
--- /dev/null
+++ b/res/anim/count_down_exit.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2013, 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">
+        <alpha
+            android:fromAlpha="1.0"
+            android:toAlpha="0.0"
+            android:duration="1000" />
+        <scale
+            android:fromXScale="1.0"
+            android:fromYScale="1.0"
+            android:toXScale="3.0"
+            android:toYScale="3.0"
+            android:pivotX="50%"
+            android:pivotY="50%"
+            android:duration="800" />
+</set>
\ No newline at end of file
diff --git a/res/anim/on_screen_hint_enter.xml b/res/anim/on_screen_hint_enter.xml
new file mode 100644
index 0000000..91653a2
--- /dev/null
+++ b/res/anim/on_screen_hint_enter.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2009, The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+        android:interpolator="@android:anim/decelerate_interpolator"
+        android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="400" />
diff --git a/res/anim/on_screen_hint_exit.xml b/res/anim/on_screen_hint_exit.xml
new file mode 100644
index 0000000..2525816
--- /dev/null
+++ b/res/anim/on_screen_hint_exit.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2009, The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+        android:interpolator="@android:anim/accelerate_interpolator"
+        android:fromAlpha="1.0" android:toAlpha="0.0" android:duration="400" />
diff --git a/res/anim/player_out.xml b/res/anim/player_out.xml
new file mode 100644
index 0000000..b6d90d2
--- /dev/null
+++ b/res/anim/player_out.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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/linear_interpolator"
+    android:fromAlpha="1.0"
+    android:toAlpha="0.0"
+    android:duration="500"/>
diff --git a/res/anim/slide_in_left.xml b/res/anim/slide_in_left.xml
new file mode 100644
index 0000000..6b1de4b
--- /dev/null
+++ b/res/anim/slide_in_left.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2013, 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%"
+           android:toXDelta="0%"
+           android:interpolator="@android:anim/decelerate_interpolator"
+           android:duration="300"/>
\ No newline at end of file
diff --git a/res/anim/slide_in_right.xml b/res/anim/slide_in_right.xml
new file mode 100644
index 0000000..12f7efe
--- /dev/null
+++ b/res/anim/slide_in_right.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2013, 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%"
+           android:toXDelta="0"
+           android:interpolator="@android:anim/decelerate_interpolator"
+           android:duration="300"/>
\ No newline at end of file
diff --git a/res/anim/slide_out_left.xml b/res/anim/slide_out_left.xml
new file mode 100644
index 0000000..be28e55
--- /dev/null
+++ b/res/anim/slide_out_left.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2013, 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%"
+           android:interpolator="@android:anim/decelerate_interpolator"
+           android:duration="300"/>
\ No newline at end of file
diff --git a/res/anim/slide_out_right.xml b/res/anim/slide_out_right.xml
new file mode 100644
index 0000000..4c786e6
--- /dev/null
+++ b/res/anim/slide_out_right.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2013, 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%"
+           android:interpolator="@android:anim/decelerate_interpolator"
+           android:duration="300"/>
\ No newline at end of file
diff --git a/res/color/primary_text.xml b/res/color/primary_text.xml
new file mode 100644
index 0000000..e9fdf7b
--- /dev/null
+++ b/res/color/primary_text.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<!-- Copied from framework resource color/primary_text_holo_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false" android:color="@color/bright_foreground_disabled_holo_dark"/>
+    <item android:state_window_focused="false" android:color="@color/bright_foreground_holo_dark"/>
+    <item android:state_pressed="true" android:color="@color/bright_foreground_holo_dark"/>
+    <item android:state_selected="true" android:color="@color/bright_foreground_holo_dark"/>
+    <item android:state_activated="true" android:color="@color/bright_foreground_holo_dark"/>
+    <item android:color="@color/bright_foreground_holo_dark"/> <!-- not selected -->
+</selector>
+
diff --git a/res/drawable-hdpi/actionbar_translucent.9.png b/res/drawable-hdpi/actionbar_translucent.9.png
new file mode 100644
index 0000000..4b40967
--- /dev/null
+++ b/res/drawable-hdpi/actionbar_translucent.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appwidget_photo_border.9.png b/res/drawable-hdpi/appwidget_photo_border.9.png
new file mode 100644
index 0000000..2d5fd62
--- /dev/null
+++ b/res/drawable-hdpi/appwidget_photo_border.9.png
Binary files differ
diff --git a/res/drawable-hdpi/background.jpg b/res/drawable-hdpi/background.jpg
new file mode 100644
index 0000000..42b74c5
--- /dev/null
+++ b/res/drawable-hdpi/background.jpg
Binary files differ
diff --git a/res/drawable-hdpi/background_portrait.jpg b/res/drawable-hdpi/background_portrait.jpg
new file mode 100644
index 0000000..75309b4
--- /dev/null
+++ b/res/drawable-hdpi/background_portrait.jpg
Binary files differ
diff --git a/res/drawable-hdpi/bg_vidcontrol.png b/res/drawable-hdpi/bg_vidcontrol.png
new file mode 100644
index 0000000..dfe2da1
--- /dev/null
+++ b/res/drawable-hdpi/bg_vidcontrol.png
Binary files differ
diff --git a/res/drawable-hdpi/border_photo_frame_widget_focused_holo.9.png b/res/drawable-hdpi/border_photo_frame_widget_focused_holo.9.png
new file mode 100644
index 0000000..1cb157e
--- /dev/null
+++ b/res/drawable-hdpi/border_photo_frame_widget_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/border_photo_frame_widget_holo.9.png b/res/drawable-hdpi/border_photo_frame_widget_holo.9.png
new file mode 100644
index 0000000..dc7092b
--- /dev/null
+++ b/res/drawable-hdpi/border_photo_frame_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/border_photo_frame_widget_pressed_holo.9.png b/res/drawable-hdpi/border_photo_frame_widget_pressed_holo.9.png
new file mode 100644
index 0000000..86d4cf1
--- /dev/null
+++ b/res/drawable-hdpi/border_photo_frame_widget_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_make_offline_disabled_on_holo_dark.png b/res/drawable-hdpi/btn_make_offline_disabled_on_holo_dark.png
new file mode 100644
index 0000000..dbd10e1
--- /dev/null
+++ b/res/drawable-hdpi/btn_make_offline_disabled_on_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_make_offline_normal_off_holo_dark.png b/res/drawable-hdpi/btn_make_offline_normal_off_holo_dark.png
new file mode 100644
index 0000000..edf5cad
--- /dev/null
+++ b/res/drawable-hdpi/btn_make_offline_normal_off_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_make_offline_normal_on_holo_dark.png b/res/drawable-hdpi/btn_make_offline_normal_on_holo_dark.png
new file mode 100644
index 0000000..e73d399
--- /dev/null
+++ b/res/drawable-hdpi/btn_make_offline_normal_on_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_shutter_default.png b/res/drawable-hdpi/btn_shutter_default.png
new file mode 100644
index 0000000..1d59be1
--- /dev/null
+++ b/res/drawable-hdpi/btn_shutter_default.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_shutter_pressed.png b/res/drawable-hdpi/btn_shutter_pressed.png
new file mode 100644
index 0000000..c732920
--- /dev/null
+++ b/res/drawable-hdpi/btn_shutter_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_shutter_recording.png b/res/drawable-hdpi/btn_shutter_recording.png
new file mode 100644
index 0000000..4a2e452
--- /dev/null
+++ b/res/drawable-hdpi/btn_shutter_recording.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_shutter_video_default.png b/res/drawable-hdpi/btn_shutter_video_default.png
new file mode 100644
index 0000000..1fe24f2
--- /dev/null
+++ b/res/drawable-hdpi/btn_shutter_video_default.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_shutter_video_pressed.png b/res/drawable-hdpi/btn_shutter_video_pressed.png
new file mode 100644
index 0000000..78f4b16
--- /dev/null
+++ b/res/drawable-hdpi/btn_shutter_video_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_shutter_video_recording.png b/res/drawable-hdpi/btn_shutter_video_recording.png
new file mode 100644
index 0000000..e9921d0
--- /dev/null
+++ b/res/drawable-hdpi/btn_shutter_video_recording.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_video_shutter_recording_holo.png b/res/drawable-hdpi/btn_video_shutter_recording_holo.png
new file mode 100644
index 0000000..9bd4e2a
--- /dev/null
+++ b/res/drawable-hdpi/btn_video_shutter_recording_holo.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_video_shutter_recording_holo_large.png b/res/drawable-hdpi/btn_video_shutter_recording_holo_large.png
new file mode 100644
index 0000000..c9b2da3
--- /dev/null
+++ b/res/drawable-hdpi/btn_video_shutter_recording_holo_large.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_video_shutter_recording_pressed_holo.png b/res/drawable-hdpi/btn_video_shutter_recording_pressed_holo.png
new file mode 100644
index 0000000..a10b620
--- /dev/null
+++ b/res/drawable-hdpi/btn_video_shutter_recording_pressed_holo.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_video_shutter_recording_pressed_holo_large.png b/res/drawable-hdpi/btn_video_shutter_recording_pressed_holo_large.png
new file mode 100644
index 0000000..864aa1f
--- /dev/null
+++ b/res/drawable-hdpi/btn_video_shutter_recording_pressed_holo_large.png
Binary files differ
diff --git a/res/drawable-hdpi/cab_divider_vertical_dark.png b/res/drawable-hdpi/cab_divider_vertical_dark.png
new file mode 100644
index 0000000..f7ed6df
--- /dev/null
+++ b/res/drawable-hdpi/cab_divider_vertical_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/camera_crop.png b/res/drawable-hdpi/camera_crop.png
new file mode 100644
index 0000000..97b1b98
--- /dev/null
+++ b/res/drawable-hdpi/camera_crop.png
Binary files differ
diff --git a/res/drawable-hdpi/capture_thumbnail_shadow.9.png b/res/drawable-hdpi/capture_thumbnail_shadow.9.png
new file mode 100644
index 0000000..f7a664e
--- /dev/null
+++ b/res/drawable-hdpi/capture_thumbnail_shadow.9.png
Binary files differ
diff --git a/res/drawable-hdpi/dialog_full_holo_dark.9.png b/res/drawable-hdpi/dialog_full_holo_dark.9.png
new file mode 100644
index 0000000..79e56f5
--- /dev/null
+++ b/res/drawable-hdpi/dialog_full_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png b/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png
new file mode 100644
index 0000000..06e5b47
--- /dev/null
+++ b/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_button_colors_curve.png b/res/drawable-hdpi/filtershow_button_colors_curve.png
new file mode 100644
index 0000000..cc33bf8
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_button_colors_curve.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_button_colors_sharpen.png b/res/drawable-hdpi/filtershow_button_colors_sharpen.png
new file mode 100644
index 0000000..2835b95
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_button_colors_sharpen.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_button_grad.png b/res/drawable-hdpi/filtershow_button_grad.png
new file mode 100644
index 0000000..7903ac7
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_button_grad.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_button_redo.png b/res/drawable-hdpi/filtershow_button_redo.png
new file mode 100644
index 0000000..43e673c
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_button_redo.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_button_undo.png b/res/drawable-hdpi/filtershow_button_undo.png
new file mode 100644
index 0000000..6165a98
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_button_undo.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_scrubber_control_disabled.png b/res/drawable-hdpi/filtershow_scrubber_control_disabled.png
new file mode 100644
index 0000000..7ccb7ec
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_scrubber_control_disabled.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_scrubber_control_focused.png b/res/drawable-hdpi/filtershow_scrubber_control_focused.png
new file mode 100644
index 0000000..f8c50f9
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_scrubber_control_focused.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_scrubber_control_normal.png b/res/drawable-hdpi/filtershow_scrubber_control_normal.png
new file mode 100644
index 0000000..e12ce2e
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_scrubber_control_normal.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_scrubber_control_pressed.png b/res/drawable-hdpi/filtershow_scrubber_control_pressed.png
new file mode 100644
index 0000000..8f0ae42
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_scrubber_control_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_scrubber_primary.9.png b/res/drawable-hdpi/filtershow_scrubber_primary.9.png
new file mode 100644
index 0000000..6e007d3
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_scrubber_primary.9.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_scrubber_secondary.9.png b/res/drawable-hdpi/filtershow_scrubber_secondary.9.png
new file mode 100644
index 0000000..1054715
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_scrubber_secondary.9.png
Binary files differ
diff --git a/res/drawable-hdpi/filtershow_scrubber_track.9.png b/res/drawable-hdpi/filtershow_scrubber_track.9.png
new file mode 100644
index 0000000..0c0ccda
--- /dev/null
+++ b/res/drawable-hdpi/filtershow_scrubber_track.9.png
Binary files differ
diff --git a/res/drawable-hdpi/frame_overlay_gallery_camera.png b/res/drawable-hdpi/frame_overlay_gallery_camera.png
new file mode 100644
index 0000000..b27bbe5
--- /dev/null
+++ b/res/drawable-hdpi/frame_overlay_gallery_camera.png
Binary files differ
diff --git a/res/drawable-hdpi/frame_overlay_gallery_folder.png b/res/drawable-hdpi/frame_overlay_gallery_folder.png
new file mode 100644
index 0000000..3924238
--- /dev/null
+++ b/res/drawable-hdpi/frame_overlay_gallery_folder.png
Binary files differ
diff --git a/res/drawable-hdpi/frame_overlay_gallery_picasa.png b/res/drawable-hdpi/frame_overlay_gallery_picasa.png
new file mode 100644
index 0000000..d16b5fd
--- /dev/null
+++ b/res/drawable-hdpi/frame_overlay_gallery_picasa.png
Binary files differ
diff --git a/res/drawable-hdpi/grid_pressed.9.png b/res/drawable-hdpi/grid_pressed.9.png
new file mode 100644
index 0000000..dff62c7
--- /dev/null
+++ b/res/drawable-hdpi/grid_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi/grid_selected.9.png b/res/drawable-hdpi/grid_selected.9.png
new file mode 100644
index 0000000..d8867ac
--- /dev/null
+++ b/res/drawable-hdpi/grid_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_360pano_holo_light.png b/res/drawable-hdpi/ic_360pano_holo_light.png
new file mode 100644
index 0000000..9873c17
--- /dev/null
+++ b/res/drawable-hdpi/ic_360pano_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_btn_shutter_retake.png b/res/drawable-hdpi/ic_btn_shutter_retake.png
new file mode 100644
index 0000000..cc7a44c
--- /dev/null
+++ b/res/drawable-hdpi/ic_btn_shutter_retake.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_cameraalbum_overlay.png b/res/drawable-hdpi/ic_cameraalbum_overlay.png
new file mode 100644
index 0000000..e58777f
--- /dev/null
+++ b/res/drawable-hdpi/ic_cameraalbum_overlay.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_control_play.png b/res/drawable-hdpi/ic_control_play.png
new file mode 100644
index 0000000..5b1eacb
--- /dev/null
+++ b/res/drawable-hdpi/ic_control_play.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_effects_holo_light.png b/res/drawable-hdpi/ic_effects_holo_light.png
new file mode 100644
index 0000000..03106eb
--- /dev/null
+++ b/res/drawable-hdpi/ic_effects_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_effects_holo_light_large.png b/res/drawable-hdpi/ic_effects_holo_light_large.png
new file mode 100644
index 0000000..eac6dba
--- /dev/null
+++ b/res/drawable-hdpi/ic_effects_holo_light_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_effects_holo_light_xlarge.png b/res/drawable-hdpi/ic_effects_holo_light_xlarge.png
new file mode 100644
index 0000000..eac6dba
--- /dev/null
+++ b/res/drawable-hdpi/ic_effects_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_0.png b/res/drawable-hdpi/ic_exposure_0.png
new file mode 100644
index 0000000..ba19c89
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_0.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_holo_light.png b/res/drawable-hdpi/ic_exposure_holo_light.png
new file mode 100644
index 0000000..39662a4
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_n1.png b/res/drawable-hdpi/ic_exposure_n1.png
new file mode 100644
index 0000000..e52dd74
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_n1.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_n2.png b/res/drawable-hdpi/ic_exposure_n2.png
new file mode 100644
index 0000000..738973d
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_n2.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_n3.png b/res/drawable-hdpi/ic_exposure_n3.png
new file mode 100644
index 0000000..2cd63b4
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_n3.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_p1.png b/res/drawable-hdpi/ic_exposure_p1.png
new file mode 100644
index 0000000..065e0bc
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_p1.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_p2.png b/res/drawable-hdpi/ic_exposure_p2.png
new file mode 100644
index 0000000..92450c4
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_p2.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_exposure_p3.png b/res/drawable-hdpi/ic_exposure_p3.png
new file mode 100644
index 0000000..d1be9f6
--- /dev/null
+++ b/res/drawable-hdpi/ic_exposure_p3.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_flash_auto_holo_light.png b/res/drawable-hdpi/ic_flash_auto_holo_light.png
new file mode 100644
index 0000000..a34d63a
--- /dev/null
+++ b/res/drawable-hdpi/ic_flash_auto_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_flash_off_holo_light.png b/res/drawable-hdpi/ic_flash_off_holo_light.png
new file mode 100644
index 0000000..3fe45fc
--- /dev/null
+++ b/res/drawable-hdpi/ic_flash_off_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_flash_on_holo_light.png b/res/drawable-hdpi/ic_flash_on_holo_light.png
new file mode 100644
index 0000000..2d67e45
--- /dev/null
+++ b/res/drawable-hdpi/ic_flash_on_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_gallery_play.png b/res/drawable-hdpi/ic_gallery_play.png
new file mode 100644
index 0000000..e4060e5
--- /dev/null
+++ b/res/drawable-hdpi/ic_gallery_play.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_gallery_play_big.png b/res/drawable-hdpi/ic_gallery_play_big.png
new file mode 100644
index 0000000..44e0c4e
--- /dev/null
+++ b/res/drawable-hdpi/ic_gallery_play_big.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_grad_add.png b/res/drawable-hdpi/ic_grad_add.png
new file mode 100644
index 0000000..4e0dc2b
--- /dev/null
+++ b/res/drawable-hdpi/ic_grad_add.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_grad_del.png b/res/drawable-hdpi/ic_grad_del.png
new file mode 100644
index 0000000..521541f
--- /dev/null
+++ b/res/drawable-hdpi/ic_grad_del.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_hdr.png b/res/drawable-hdpi/ic_hdr.png
new file mode 100644
index 0000000..cda9890
--- /dev/null
+++ b/res/drawable-hdpi/ic_hdr.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_hdr_off.png b/res/drawable-hdpi/ic_hdr_off.png
new file mode 100644
index 0000000..28db73c
--- /dev/null
+++ b/res/drawable-hdpi/ic_hdr_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_imagesize.png b/res/drawable-hdpi/ic_imagesize.png
new file mode 100644
index 0000000..126208b
--- /dev/null
+++ b/res/drawable-hdpi/ic_imagesize.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_ev_0.png b/res/drawable-hdpi/ic_indicator_ev_0.png
new file mode 100644
index 0000000..9092025
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_ev_0.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_ev_n1.png b/res/drawable-hdpi/ic_indicator_ev_n1.png
new file mode 100644
index 0000000..56c5e60
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_ev_n1.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_ev_n2.png b/res/drawable-hdpi/ic_indicator_ev_n2.png
new file mode 100644
index 0000000..72838de
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_ev_n2.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_ev_n3.png b/res/drawable-hdpi/ic_indicator_ev_n3.png
new file mode 100644
index 0000000..1200d63
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_ev_n3.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_ev_p1.png b/res/drawable-hdpi/ic_indicator_ev_p1.png
new file mode 100644
index 0000000..5497c86
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_ev_p1.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_ev_p2.png b/res/drawable-hdpi/ic_indicator_ev_p2.png
new file mode 100644
index 0000000..25107fe
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_ev_p2.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_ev_p3.png b/res/drawable-hdpi/ic_indicator_ev_p3.png
new file mode 100644
index 0000000..6d0c1e0
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_ev_p3.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_flash_auto.png b/res/drawable-hdpi/ic_indicator_flash_auto.png
new file mode 100644
index 0000000..edb34a0
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_flash_auto.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_flash_off.png b/res/drawable-hdpi/ic_indicator_flash_off.png
new file mode 100644
index 0000000..880ad07
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_flash_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_flash_on.png b/res/drawable-hdpi/ic_indicator_flash_on.png
new file mode 100644
index 0000000..ec3d1d5
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_flash_on.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_loc_off.png b/res/drawable-hdpi/ic_indicator_loc_off.png
new file mode 100644
index 0000000..d5937e4
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_loc_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_loc_on.png b/res/drawable-hdpi/ic_indicator_loc_on.png
new file mode 100644
index 0000000..ae272a1
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_loc_on.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_sce_hdr.png b/res/drawable-hdpi/ic_indicator_sce_hdr.png
new file mode 100644
index 0000000..0b8fdfa
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_sce_hdr.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_sce_off.png b/res/drawable-hdpi/ic_indicator_sce_off.png
new file mode 100644
index 0000000..09bd2c8
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_sce_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_sce_on.png b/res/drawable-hdpi/ic_indicator_sce_on.png
new file mode 100644
index 0000000..ef0f56f
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_sce_on.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_timer_off.png b/res/drawable-hdpi/ic_indicator_timer_off.png
new file mode 100644
index 0000000..e377f89
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_timer_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_timer_on.png b/res/drawable-hdpi/ic_indicator_timer_on.png
new file mode 100644
index 0000000..8ceade6
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_timer_on.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_wb_cloudy.png b/res/drawable-hdpi/ic_indicator_wb_cloudy.png
new file mode 100644
index 0000000..62f5c99
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_wb_cloudy.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_wb_daylight.png b/res/drawable-hdpi/ic_indicator_wb_daylight.png
new file mode 100644
index 0000000..ffadb0e
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_wb_daylight.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_wb_fluorescent.png b/res/drawable-hdpi/ic_indicator_wb_fluorescent.png
new file mode 100644
index 0000000..3c30bfb
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_wb_fluorescent.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_wb_off.png b/res/drawable-hdpi/ic_indicator_wb_off.png
new file mode 100644
index 0000000..abe7405
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_wb_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_indicator_wb_tungsten.png b/res/drawable-hdpi/ic_indicator_wb_tungsten.png
new file mode 100644
index 0000000..6de19d5
--- /dev/null
+++ b/res/drawable-hdpi/ic_indicator_wb_tungsten.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_location.png b/res/drawable-hdpi/ic_location.png
new file mode 100644
index 0000000..ff85d79
--- /dev/null
+++ b/res/drawable-hdpi/ic_location.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_location_off.png b/res/drawable-hdpi/ic_location_off.png
new file mode 100644
index 0000000..ae6e2ea
--- /dev/null
+++ b/res/drawable-hdpi/ic_location_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_camera_holo_light.png b/res/drawable-hdpi/ic_menu_camera_holo_light.png
new file mode 100644
index 0000000..5f0f064
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_camera_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_cancel_holo_light.png b/res/drawable-hdpi/ic_menu_cancel_holo_light.png
new file mode 100644
index 0000000..e0f85c5
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_cancel_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_done_holo_light.png b/res/drawable-hdpi/ic_menu_done_holo_light.png
new file mode 100644
index 0000000..923589e
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_done_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_edit_holo_dark.png b/res/drawable-hdpi/ic_menu_edit_holo_dark.png
new file mode 100644
index 0000000..54952f5
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_edit_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_info_details.png b/res/drawable-hdpi/ic_menu_info_details.png
new file mode 100644
index 0000000..2d1f7f3
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_info_details.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_make_offline.png b/res/drawable-hdpi/ic_menu_make_offline.png
new file mode 100644
index 0000000..de25a7a
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_make_offline.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_ptp_holo_light.png b/res/drawable-hdpi/ic_menu_ptp_holo_light.png
new file mode 100644
index 0000000..5e80ce8
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_ptp_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_revert_holo_dark.png b/res/drawable-hdpi/ic_menu_revert_holo_dark.png
new file mode 100644
index 0000000..6165a98
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_revert_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_savephoto.png b/res/drawable-hdpi/ic_menu_savephoto.png
new file mode 100644
index 0000000..0c0309f
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_savephoto.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_savephoto_disabled.png b/res/drawable-hdpi/ic_menu_savephoto_disabled.png
new file mode 100755
index 0000000..afb34ec
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_savephoto_disabled.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_share_holo_light.png b/res/drawable-hdpi/ic_menu_share_holo_light.png
new file mode 100644
index 0000000..492d609
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_share_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_slideshow_holo_light.png b/res/drawable-hdpi/ic_menu_slideshow_holo_light.png
new file mode 100644
index 0000000..ca13dd8
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_slideshow_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_tiny_planet.png b/res/drawable-hdpi/ic_menu_tiny_planet.png
new file mode 100644
index 0000000..12a0551
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_tiny_planet.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_trash_holo_light.png b/res/drawable-hdpi/ic_menu_trash_holo_light.png
new file mode 100644
index 0000000..721ee5c
--- /dev/null
+++ b/res/drawable-hdpi/ic_menu_trash_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_border_fast.9.png b/res/drawable-hdpi/ic_pan_border_fast.9.png
new file mode 100644
index 0000000..cbd6172
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_border_fast.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_border_fast_large.9.png b/res/drawable-hdpi/ic_pan_border_fast_large.9.png
new file mode 100644
index 0000000..b0df823
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_border_fast_large.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_border_fast_xlarge.9.png b/res/drawable-hdpi/ic_pan_border_fast_xlarge.9.png
new file mode 100644
index 0000000..b0df823
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_border_fast_xlarge.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_left_indicator.png b/res/drawable-hdpi/ic_pan_left_indicator.png
new file mode 100644
index 0000000..c9a6907
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_left_indicator.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_left_indicator_fast.png b/res/drawable-hdpi/ic_pan_left_indicator_fast.png
new file mode 100644
index 0000000..841e1e5
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_left_indicator_fast.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_left_indicator_fast_large.png b/res/drawable-hdpi/ic_pan_left_indicator_fast_large.png
new file mode 100644
index 0000000..244118f
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_left_indicator_fast_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_left_indicator_fast_xlarge.png b/res/drawable-hdpi/ic_pan_left_indicator_fast_xlarge.png
new file mode 100644
index 0000000..244118f
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_left_indicator_fast_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_left_indicator_large.png b/res/drawable-hdpi/ic_pan_left_indicator_large.png
new file mode 100644
index 0000000..60d1f98
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_left_indicator_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_left_indicator_xlarge.png b/res/drawable-hdpi/ic_pan_left_indicator_xlarge.png
new file mode 100644
index 0000000..60d1f98
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_left_indicator_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_progression.png b/res/drawable-hdpi/ic_pan_progression.png
new file mode 100644
index 0000000..69650f0
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_progression.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_progression_large.png b/res/drawable-hdpi/ic_pan_progression_large.png
new file mode 100644
index 0000000..afe9188
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_progression_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_progression_xlarge.png b/res/drawable-hdpi/ic_pan_progression_xlarge.png
new file mode 100644
index 0000000..afe9188
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_progression_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_right_indicator.png b/res/drawable-hdpi/ic_pan_right_indicator.png
new file mode 100644
index 0000000..2c4fe80
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_right_indicator.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_right_indicator_fast.png b/res/drawable-hdpi/ic_pan_right_indicator_fast.png
new file mode 100644
index 0000000..a6d7eec
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_right_indicator_fast.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_right_indicator_fast_large.png b/res/drawable-hdpi/ic_pan_right_indicator_fast_large.png
new file mode 100644
index 0000000..e57f010
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_right_indicator_fast_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_right_indicator_fast_xlarge.png b/res/drawable-hdpi/ic_pan_right_indicator_fast_xlarge.png
new file mode 100644
index 0000000..e57f010
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_right_indicator_fast_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_right_indicator_large.png b/res/drawable-hdpi/ic_pan_right_indicator_large.png
new file mode 100644
index 0000000..f9b2ca2
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_right_indicator_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pan_right_indicator_xlarge.png b/res/drawable-hdpi/ic_pan_right_indicator_xlarge.png
new file mode 100644
index 0000000..f9b2ca2
--- /dev/null
+++ b/res/drawable-hdpi/ic_pan_right_indicator_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_photoeditor_border.png b/res/drawable-hdpi/ic_photoeditor_border.png
new file mode 100644
index 0000000..5fb0d51
--- /dev/null
+++ b/res/drawable-hdpi/ic_photoeditor_border.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_photoeditor_color.png b/res/drawable-hdpi/ic_photoeditor_color.png
new file mode 100644
index 0000000..13972c2
--- /dev/null
+++ b/res/drawable-hdpi/ic_photoeditor_color.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_photoeditor_effects.png b/res/drawable-hdpi/ic_photoeditor_effects.png
new file mode 100644
index 0000000..8ec892b
--- /dev/null
+++ b/res/drawable-hdpi/ic_photoeditor_effects.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_photoeditor_fix.png b/res/drawable-hdpi/ic_photoeditor_fix.png
new file mode 100644
index 0000000..18b11c7
--- /dev/null
+++ b/res/drawable-hdpi/ic_photoeditor_fix.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_recording_indicator.png b/res/drawable-hdpi/ic_recording_indicator.png
new file mode 100644
index 0000000..509eb8b
--- /dev/null
+++ b/res/drawable-hdpi/ic_recording_indicator.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_sce.png b/res/drawable-hdpi/ic_sce.png
new file mode 100644
index 0000000..dd8bbd7
--- /dev/null
+++ b/res/drawable-hdpi/ic_sce.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_sce_action.png b/res/drawable-hdpi/ic_sce_action.png
new file mode 100644
index 0000000..f968592
--- /dev/null
+++ b/res/drawable-hdpi/ic_sce_action.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_sce_night.png b/res/drawable-hdpi/ic_sce_night.png
new file mode 100644
index 0000000..dc42ec9
--- /dev/null
+++ b/res/drawable-hdpi/ic_sce_night.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_sce_off.png b/res/drawable-hdpi/ic_sce_off.png
new file mode 100644
index 0000000..4c64578
--- /dev/null
+++ b/res/drawable-hdpi/ic_sce_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_sce_party.png b/res/drawable-hdpi/ic_sce_party.png
new file mode 100644
index 0000000..6c664ba
--- /dev/null
+++ b/res/drawable-hdpi/ic_sce_party.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_sce_sunset.png b/res/drawable-hdpi/ic_sce_sunset.png
new file mode 100644
index 0000000..82f74e6
--- /dev/null
+++ b/res/drawable-hdpi/ic_sce_sunset.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_scn_holo_light.png b/res/drawable-hdpi/ic_scn_holo_light.png
new file mode 100644
index 0000000..6b62dce
--- /dev/null
+++ b/res/drawable-hdpi/ic_scn_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_scn_holo_light_large.png b/res/drawable-hdpi/ic_scn_holo_light_large.png
new file mode 100644
index 0000000..e0dd705
--- /dev/null
+++ b/res/drawable-hdpi/ic_scn_holo_light_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_scn_holo_light_xlarge.png b/res/drawable-hdpi/ic_scn_holo_light_xlarge.png
new file mode 100644
index 0000000..e0dd705
--- /dev/null
+++ b/res/drawable-hdpi/ic_scn_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_settings_holo_light.png b/res/drawable-hdpi/ic_settings_holo_light.png
new file mode 100644
index 0000000..5d315a3
--- /dev/null
+++ b/res/drawable-hdpi/ic_settings_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_snapshot_border.9.png b/res/drawable-hdpi/ic_snapshot_border.9.png
new file mode 100644
index 0000000..e6baffe
--- /dev/null
+++ b/res/drawable-hdpi/ic_snapshot_border.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_snapshot_border_large.9.png b/res/drawable-hdpi/ic_snapshot_border_large.9.png
new file mode 100644
index 0000000..291d36b
--- /dev/null
+++ b/res/drawable-hdpi/ic_snapshot_border_large.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_snapshot_border_xlarge.9.png b/res/drawable-hdpi/ic_snapshot_border_xlarge.9.png
new file mode 100644
index 0000000..291d36b
--- /dev/null
+++ b/res/drawable-hdpi/ic_snapshot_border_xlarge.9.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_back.png b/res/drawable-hdpi/ic_switch_back.png
new file mode 100644
index 0000000..02b1c6b
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_back.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_camera.png b/res/drawable-hdpi/ic_switch_camera.png
new file mode 100644
index 0000000..11dd39a
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_camera.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_front.png b/res/drawable-hdpi/ic_switch_front.png
new file mode 100644
index 0000000..09cbc24
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_front.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_photo_facing_holo_light.png b/res/drawable-hdpi/ic_switch_photo_facing_holo_light.png
new file mode 100644
index 0000000..968063f
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_photo_facing_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_photo_facing_holo_light_large.png b/res/drawable-hdpi/ic_switch_photo_facing_holo_light_large.png
new file mode 100644
index 0000000..a415e8c
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_photo_facing_holo_light_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_photo_facing_holo_light_xlarge.png b/res/drawable-hdpi/ic_switch_photo_facing_holo_light_xlarge.png
new file mode 100644
index 0000000..a415e8c
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_photo_facing_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_photosphere.png b/res/drawable-hdpi/ic_switch_photosphere.png
new file mode 100644
index 0000000..ea28eae
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_photosphere.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_video.png b/res/drawable-hdpi/ic_switch_video.png
new file mode 100644
index 0000000..7432e8e
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_video.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_video_facing_holo_light.png b/res/drawable-hdpi/ic_switch_video_facing_holo_light.png
new file mode 100644
index 0000000..bc7f3ea
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_video_facing_holo_light.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_video_facing_holo_light_large.png b/res/drawable-hdpi/ic_switch_video_facing_holo_light_large.png
new file mode 100644
index 0000000..743aea3
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_video_facing_holo_light_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switch_video_facing_holo_light_xlarge.png b/res/drawable-hdpi/ic_switch_video_facing_holo_light_xlarge.png
new file mode 100644
index 0000000..743aea3
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_video_facing_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_switcher_menu_indicator.png b/res/drawable-hdpi/ic_switcher_menu_indicator.png
new file mode 100644
index 0000000..fc7474c
--- /dev/null
+++ b/res/drawable-hdpi/ic_switcher_menu_indicator.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_timelapse_none.png b/res/drawable-hdpi/ic_timelapse_none.png
new file mode 100644
index 0000000..6283f57
--- /dev/null
+++ b/res/drawable-hdpi/ic_timelapse_none.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_timelapse_none_large.png b/res/drawable-hdpi/ic_timelapse_none_large.png
new file mode 100644
index 0000000..33e462f
--- /dev/null
+++ b/res/drawable-hdpi/ic_timelapse_none_large.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_timelapse_none_xlarge.png b/res/drawable-hdpi/ic_timelapse_none_xlarge.png
new file mode 100644
index 0000000..33e462f
--- /dev/null
+++ b/res/drawable-hdpi/ic_timelapse_none_xlarge.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_timer.png b/res/drawable-hdpi/ic_timer.png
new file mode 100644
index 0000000..a3cec8d
--- /dev/null
+++ b/res/drawable-hdpi/ic_timer.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_vidcontrol_pause.png b/res/drawable-hdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..3d1a8bf
--- /dev/null
+++ b/res/drawable-hdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_vidcontrol_play.png b/res/drawable-hdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..cc02166
--- /dev/null
+++ b/res/drawable-hdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_vidcontrol_reload.png b/res/drawable-hdpi/ic_vidcontrol_reload.png
new file mode 100644
index 0000000..bf8d529
--- /dev/null
+++ b/res/drawable-hdpi/ic_vidcontrol_reload.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_background_fields_of_wheat_holo.png b/res/drawable-hdpi/ic_video_effects_background_fields_of_wheat_holo.png
new file mode 100644
index 0000000..963c860
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_background_fields_of_wheat_holo.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_background_intergalactic_holo.png b/res/drawable-hdpi/ic_video_effects_background_intergalactic_holo.png
new file mode 100644
index 0000000..dd375e6
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_background_intergalactic_holo.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_background_normal_holo_dark.png b/res/drawable-hdpi/ic_video_effects_background_normal_holo_dark.png
new file mode 100644
index 0000000..f01e41b
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_background_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_faces_big_eyes_holo_dark.png b/res/drawable-hdpi/ic_video_effects_faces_big_eyes_holo_dark.png
new file mode 100644
index 0000000..493ae40
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_faces_big_eyes_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_faces_big_mouth_holo_dark.png b/res/drawable-hdpi/ic_video_effects_faces_big_mouth_holo_dark.png
new file mode 100644
index 0000000..bc28c2e
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_faces_big_mouth_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_faces_big_nose_holo_dark.png b/res/drawable-hdpi/ic_video_effects_faces_big_nose_holo_dark.png
new file mode 100644
index 0000000..0df146d
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_faces_big_nose_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_faces_small_eyes_holo_dark.png b/res/drawable-hdpi/ic_video_effects_faces_small_eyes_holo_dark.png
new file mode 100644
index 0000000..c9db4c7
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_faces_small_eyes_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_faces_small_mouth_holo_dark.png b/res/drawable-hdpi/ic_video_effects_faces_small_mouth_holo_dark.png
new file mode 100644
index 0000000..b284745
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_faces_small_mouth_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_effects_faces_squeeze_holo_dark.png b/res/drawable-hdpi/ic_video_effects_faces_squeeze_holo_dark.png
new file mode 100644
index 0000000..d228423
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_effects_faces_squeeze_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_video_thumb.png b/res/drawable-hdpi/ic_video_thumb.png
new file mode 100644
index 0000000..69f9047
--- /dev/null
+++ b/res/drawable-hdpi/ic_video_thumb.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_view_photosphere.png b/res/drawable-hdpi/ic_view_photosphere.png
new file mode 100644
index 0000000..51c267b
--- /dev/null
+++ b/res/drawable-hdpi/ic_view_photosphere.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_wb_auto.png b/res/drawable-hdpi/ic_wb_auto.png
new file mode 100644
index 0000000..0e07a04
--- /dev/null
+++ b/res/drawable-hdpi/ic_wb_auto.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_wb_cloudy.png b/res/drawable-hdpi/ic_wb_cloudy.png
new file mode 100644
index 0000000..fb42ada
--- /dev/null
+++ b/res/drawable-hdpi/ic_wb_cloudy.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_wb_fluorescent.png b/res/drawable-hdpi/ic_wb_fluorescent.png
new file mode 100644
index 0000000..fbf8c96
--- /dev/null
+++ b/res/drawable-hdpi/ic_wb_fluorescent.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_wb_incandescent.png b/res/drawable-hdpi/ic_wb_incandescent.png
new file mode 100644
index 0000000..295a807
--- /dev/null
+++ b/res/drawable-hdpi/ic_wb_incandescent.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_wb_sunlight.png b/res/drawable-hdpi/ic_wb_sunlight.png
new file mode 100644
index 0000000..d256509
--- /dev/null
+++ b/res/drawable-hdpi/ic_wb_sunlight.png
Binary files differ
diff --git a/res/drawable-hdpi/icn_media_pause_focused_holo_dark.png b/res/drawable-hdpi/icn_media_pause_focused_holo_dark.png
new file mode 100644
index 0000000..6b4047b
--- /dev/null
+++ b/res/drawable-hdpi/icn_media_pause_focused_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/icn_media_pause_normal_holo_dark.png b/res/drawable-hdpi/icn_media_pause_normal_holo_dark.png
new file mode 100644
index 0000000..1945610
--- /dev/null
+++ b/res/drawable-hdpi/icn_media_pause_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/icn_media_pause_pressed_holo_dark.png b/res/drawable-hdpi/icn_media_pause_pressed_holo_dark.png
new file mode 100644
index 0000000..3af4612
--- /dev/null
+++ b/res/drawable-hdpi/icn_media_pause_pressed_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/icn_media_play_focused_holo_dark.png b/res/drawable-hdpi/icn_media_play_focused_holo_dark.png
new file mode 100644
index 0000000..576f247
--- /dev/null
+++ b/res/drawable-hdpi/icn_media_play_focused_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/icn_media_play_normal_holo_dark.png b/res/drawable-hdpi/icn_media_play_normal_holo_dark.png
new file mode 100644
index 0000000..16dd09d
--- /dev/null
+++ b/res/drawable-hdpi/icn_media_play_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/icn_media_play_pressed_holo_dark.png b/res/drawable-hdpi/icn_media_play_pressed_holo_dark.png
new file mode 100644
index 0000000..1bc4f79
--- /dev/null
+++ b/res/drawable-hdpi/icn_media_play_pressed_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/list_divider.9.png b/res/drawable-hdpi/list_divider.9.png
new file mode 100644
index 0000000..986ab0b
--- /dev/null
+++ b/res/drawable-hdpi/list_divider.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_divider_holo_dark.9.png b/res/drawable-hdpi/list_divider_holo_dark.9.png
new file mode 100644
index 0000000..986ab0b
--- /dev/null
+++ b/res/drawable-hdpi/list_divider_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_divider_large.9.png b/res/drawable-hdpi/list_divider_large.9.png
new file mode 100644
index 0000000..986ab0b
--- /dev/null
+++ b/res/drawable-hdpi/list_divider_large.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_pressed_holo_light.9.png b/res/drawable-hdpi/list_pressed_holo_light.9.png
new file mode 100644
index 0000000..5654cd6
--- /dev/null
+++ b/res/drawable-hdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_selector_background_selected.9.png b/res/drawable-hdpi/list_selector_background_selected.9.png
new file mode 100644
index 0000000..cbf50b3
--- /dev/null
+++ b/res/drawable-hdpi/list_selector_background_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi/menu_dropdown_panel_holo_dark.9.png b/res/drawable-hdpi/menu_dropdown_panel_holo_dark.9.png
new file mode 100644
index 0000000..4d3d208
--- /dev/null
+++ b/res/drawable-hdpi/menu_dropdown_panel_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/on_screen_hint_frame.9.png b/res/drawable-hdpi/on_screen_hint_frame.9.png
new file mode 100644
index 0000000..8b78d67
--- /dev/null
+++ b/res/drawable-hdpi/on_screen_hint_frame.9.png
Binary files differ
diff --git a/res/drawable-hdpi/overscroll_edge.png b/res/drawable-hdpi/overscroll_edge.png
new file mode 100644
index 0000000..08fc022
--- /dev/null
+++ b/res/drawable-hdpi/overscroll_edge.png
Binary files differ
diff --git a/res/drawable-hdpi/overscroll_glow.png b/res/drawable-hdpi/overscroll_glow.png
new file mode 100644
index 0000000..8f0c2cb
--- /dev/null
+++ b/res/drawable-hdpi/overscroll_glow.png
Binary files differ
diff --git a/res/drawable-hdpi/panel_undo_holo.9.png b/res/drawable-hdpi/panel_undo_holo.9.png
new file mode 100644
index 0000000..2396b26
--- /dev/null
+++ b/res/drawable-hdpi/panel_undo_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/placeholder_camera.png b/res/drawable-hdpi/placeholder_camera.png
new file mode 100644
index 0000000..50a4d37
--- /dev/null
+++ b/res/drawable-hdpi/placeholder_camera.png
Binary files differ
diff --git a/res/drawable-hdpi/placeholder_empty.png b/res/drawable-hdpi/placeholder_empty.png
new file mode 100644
index 0000000..5f60203
--- /dev/null
+++ b/res/drawable-hdpi/placeholder_empty.png
Binary files differ
diff --git a/res/drawable-hdpi/placeholder_locked.png b/res/drawable-hdpi/placeholder_locked.png
new file mode 100644
index 0000000..073b74f
--- /dev/null
+++ b/res/drawable-hdpi/placeholder_locked.png
Binary files differ
diff --git a/res/drawable-hdpi/preview.png b/res/drawable-hdpi/preview.png
new file mode 100644
index 0000000..6cddeab
--- /dev/null
+++ b/res/drawable-hdpi/preview.png
Binary files differ
diff --git a/res/drawable-hdpi/scrubber_knob.png b/res/drawable-hdpi/scrubber_knob.png
new file mode 100644
index 0000000..426e3da
--- /dev/null
+++ b/res/drawable-hdpi/scrubber_knob.png
Binary files differ
diff --git a/res/drawable-hdpi/spinner_76_inner_holo.png b/res/drawable-hdpi/spinner_76_inner_holo.png
new file mode 100644
index 0000000..7cede92
--- /dev/null
+++ b/res/drawable-hdpi/spinner_76_inner_holo.png
Binary files differ
diff --git a/res/drawable-hdpi/spinner_76_outer_holo.png b/res/drawable-hdpi/spinner_76_outer_holo.png
new file mode 100644
index 0000000..14de2f1
--- /dev/null
+++ b/res/drawable-hdpi/spinner_76_outer_holo.png
Binary files differ
diff --git a/res/drawable-hdpi/switch_bg_focused_holo_dark.9.png b/res/drawable-hdpi/switch_bg_focused_holo_dark.9.png
new file mode 100644
index 0000000..4e2ae0f
--- /dev/null
+++ b/res/drawable-hdpi/switch_bg_focused_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/switch_bg_holo_dark.9.png b/res/drawable-hdpi/switch_bg_holo_dark.9.png
new file mode 100644
index 0000000..933d99b
--- /dev/null
+++ b/res/drawable-hdpi/switch_bg_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/switch_thumb_activated_holo_dark.9.png b/res/drawable-hdpi/switch_thumb_activated_holo_dark.9.png
new file mode 100644
index 0000000..9c5147e
--- /dev/null
+++ b/res/drawable-hdpi/switch_thumb_activated_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/switch_thumb_disabled_holo_dark.9.png b/res/drawable-hdpi/switch_thumb_disabled_holo_dark.9.png
new file mode 100644
index 0000000..a257e26
--- /dev/null
+++ b/res/drawable-hdpi/switch_thumb_disabled_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/switch_thumb_holo_dark.9.png b/res/drawable-hdpi/switch_thumb_holo_dark.9.png
new file mode 100644
index 0000000..dd999d6
--- /dev/null
+++ b/res/drawable-hdpi/switch_thumb_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/switch_thumb_pressed_holo_dark.9.png b/res/drawable-hdpi/switch_thumb_pressed_holo_dark.9.png
new file mode 100644
index 0000000..ea54380
--- /dev/null
+++ b/res/drawable-hdpi/switch_thumb_pressed_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/text_select_handle_left.png b/res/drawable-hdpi/text_select_handle_left.png
new file mode 100644
index 0000000..d2ed06d
--- /dev/null
+++ b/res/drawable-hdpi/text_select_handle_left.png
Binary files differ
diff --git a/res/drawable-hdpi/text_select_handle_right.png b/res/drawable-hdpi/text_select_handle_right.png
new file mode 100644
index 0000000..e419249
--- /dev/null
+++ b/res/drawable-hdpi/text_select_handle_right.png
Binary files differ
diff --git a/res/drawable-hdpi/toast_frame_holo.9.png b/res/drawable-hdpi/toast_frame_holo.9.png
new file mode 100644
index 0000000..f8f75db
--- /dev/null
+++ b/res/drawable-hdpi/toast_frame_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/wallpaper_picker_preview.png b/res/drawable-hdpi/wallpaper_picker_preview.png
new file mode 100644
index 0000000..452b125
--- /dev/null
+++ b/res/drawable-hdpi/wallpaper_picker_preview.png
Binary files differ
diff --git a/res/drawable-land-hdpi/btn_video_shutter_recording_holo_xlarge.png b/res/drawable-land-hdpi/btn_video_shutter_recording_holo_xlarge.png
new file mode 100644
index 0000000..9e8ed18
--- /dev/null
+++ b/res/drawable-land-hdpi/btn_video_shutter_recording_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-land-hdpi/btn_video_shutter_recording_pressed_holo_xlarge.png b/res/drawable-land-hdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
new file mode 100644
index 0000000..c1729ab
--- /dev/null
+++ b/res/drawable-land-hdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-land-hdpi/switcher_bg.9.png b/res/drawable-land-hdpi/switcher_bg.9.png
new file mode 100644
index 0000000..dad08d4
--- /dev/null
+++ b/res/drawable-land-hdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-land-mdpi/btn_video_shutter_recording_holo_xlarge.png b/res/drawable-land-mdpi/btn_video_shutter_recording_holo_xlarge.png
new file mode 100644
index 0000000..ab1d6c5
--- /dev/null
+++ b/res/drawable-land-mdpi/btn_video_shutter_recording_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-land-mdpi/btn_video_shutter_recording_pressed_holo_xlarge.png b/res/drawable-land-mdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
new file mode 100644
index 0000000..a0b4fb1
--- /dev/null
+++ b/res/drawable-land-mdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-land-mdpi/switcher_bg.9.png b/res/drawable-land-mdpi/switcher_bg.9.png
new file mode 100644
index 0000000..2073686
--- /dev/null
+++ b/res/drawable-land-mdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-land-xhdpi/btn_video_shutter_recording_holo_xlarge.png b/res/drawable-land-xhdpi/btn_video_shutter_recording_holo_xlarge.png
new file mode 100644
index 0000000..746faf8
--- /dev/null
+++ b/res/drawable-land-xhdpi/btn_video_shutter_recording_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-land-xhdpi/btn_video_shutter_recording_pressed_holo_xlarge.png b/res/drawable-land-xhdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
new file mode 100644
index 0000000..26ee6b9
--- /dev/null
+++ b/res/drawable-land-xhdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-land-xhdpi/switcher_bg.9.png b/res/drawable-land-xhdpi/switcher_bg.9.png
new file mode 100644
index 0000000..a726dc8
--- /dev/null
+++ b/res/drawable-land-xhdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-mdpi/actionbar_translucent.9.png b/res/drawable-mdpi/actionbar_translucent.9.png
new file mode 100644
index 0000000..a995d44
--- /dev/null
+++ b/res/drawable-mdpi/actionbar_translucent.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appwidget_photo_border.9.png b/res/drawable-mdpi/appwidget_photo_border.9.png
new file mode 100644
index 0000000..7c520fb
--- /dev/null
+++ b/res/drawable-mdpi/appwidget_photo_border.9.png
Binary files differ
diff --git a/res/drawable-mdpi/background.jpg b/res/drawable-mdpi/background.jpg
new file mode 100644
index 0000000..42b74c5
--- /dev/null
+++ b/res/drawable-mdpi/background.jpg
Binary files differ
diff --git a/res/drawable-mdpi/background_portrait.jpg b/res/drawable-mdpi/background_portrait.jpg
new file mode 100644
index 0000000..75309b4
--- /dev/null
+++ b/res/drawable-mdpi/background_portrait.jpg
Binary files differ
diff --git a/res/drawable-mdpi/bg_vidcontrol.png b/res/drawable-mdpi/bg_vidcontrol.png
new file mode 100644
index 0000000..5a5ce55
--- /dev/null
+++ b/res/drawable-mdpi/bg_vidcontrol.png
Binary files differ
diff --git a/res/drawable-mdpi/border_photo_frame_widget_focused_holo.9.png b/res/drawable-mdpi/border_photo_frame_widget_focused_holo.9.png
new file mode 100644
index 0000000..89e2c5d
--- /dev/null
+++ b/res/drawable-mdpi/border_photo_frame_widget_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/border_photo_frame_widget_holo.9.png b/res/drawable-mdpi/border_photo_frame_widget_holo.9.png
new file mode 100644
index 0000000..18d2cc8
--- /dev/null
+++ b/res/drawable-mdpi/border_photo_frame_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/border_photo_frame_widget_pressed_holo.9.png b/res/drawable-mdpi/border_photo_frame_widget_pressed_holo.9.png
new file mode 100644
index 0000000..4732b12
--- /dev/null
+++ b/res/drawable-mdpi/border_photo_frame_widget_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_make_offline_disabled_on_holo_dark.png b/res/drawable-mdpi/btn_make_offline_disabled_on_holo_dark.png
new file mode 100644
index 0000000..2330560
--- /dev/null
+++ b/res/drawable-mdpi/btn_make_offline_disabled_on_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_make_offline_normal_off_holo_dark.png b/res/drawable-mdpi/btn_make_offline_normal_off_holo_dark.png
new file mode 100644
index 0000000..17c44f5
--- /dev/null
+++ b/res/drawable-mdpi/btn_make_offline_normal_off_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_make_offline_normal_on_holo_dark.png b/res/drawable-mdpi/btn_make_offline_normal_on_holo_dark.png
new file mode 100644
index 0000000..f4ada23
--- /dev/null
+++ b/res/drawable-mdpi/btn_make_offline_normal_on_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_shutter_default.png b/res/drawable-mdpi/btn_shutter_default.png
new file mode 100644
index 0000000..ad21d38
--- /dev/null
+++ b/res/drawable-mdpi/btn_shutter_default.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_shutter_pressed.png b/res/drawable-mdpi/btn_shutter_pressed.png
new file mode 100644
index 0000000..0895f7a
--- /dev/null
+++ b/res/drawable-mdpi/btn_shutter_pressed.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_shutter_recording.png b/res/drawable-mdpi/btn_shutter_recording.png
new file mode 100644
index 0000000..4906d70
--- /dev/null
+++ b/res/drawable-mdpi/btn_shutter_recording.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_shutter_video_default.png b/res/drawable-mdpi/btn_shutter_video_default.png
new file mode 100644
index 0000000..058ee32
--- /dev/null
+++ b/res/drawable-mdpi/btn_shutter_video_default.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_shutter_video_pressed.png b/res/drawable-mdpi/btn_shutter_video_pressed.png
new file mode 100644
index 0000000..883c36e
--- /dev/null
+++ b/res/drawable-mdpi/btn_shutter_video_pressed.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_shutter_video_recording.png b/res/drawable-mdpi/btn_shutter_video_recording.png
new file mode 100644
index 0000000..a170eba
--- /dev/null
+++ b/res/drawable-mdpi/btn_shutter_video_recording.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_video_shutter_recording_holo.png b/res/drawable-mdpi/btn_video_shutter_recording_holo.png
new file mode 100644
index 0000000..ca4625f
--- /dev/null
+++ b/res/drawable-mdpi/btn_video_shutter_recording_holo.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_video_shutter_recording_pressed_holo.png b/res/drawable-mdpi/btn_video_shutter_recording_pressed_holo.png
new file mode 100644
index 0000000..b91ce1a
--- /dev/null
+++ b/res/drawable-mdpi/btn_video_shutter_recording_pressed_holo.png
Binary files differ
diff --git a/res/drawable-mdpi/cab_divider_vertical_dark.png b/res/drawable-mdpi/cab_divider_vertical_dark.png
new file mode 100644
index 0000000..f7ed6df
--- /dev/null
+++ b/res/drawable-mdpi/cab_divider_vertical_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/camera_crop.png b/res/drawable-mdpi/camera_crop.png
new file mode 100644
index 0000000..cf38564
--- /dev/null
+++ b/res/drawable-mdpi/camera_crop.png
Binary files differ
diff --git a/res/drawable-mdpi/capture_thumbnail_shadow.9.png b/res/drawable-mdpi/capture_thumbnail_shadow.9.png
new file mode 100644
index 0000000..db0473d
--- /dev/null
+++ b/res/drawable-mdpi/capture_thumbnail_shadow.9.png
Binary files differ
diff --git a/res/drawable-mdpi/dialog_full_holo_dark.9.png b/res/drawable-mdpi/dialog_full_holo_dark.9.png
new file mode 100644
index 0000000..fb3660e
--- /dev/null
+++ b/res/drawable-mdpi/dialog_full_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png b/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png
new file mode 100644
index 0000000..81de1bb
--- /dev/null
+++ b/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_button_colors_curve.png b/res/drawable-mdpi/filtershow_button_colors_curve.png
new file mode 100644
index 0000000..75e7c11
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_button_colors_curve.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_button_colors_sharpen.png b/res/drawable-mdpi/filtershow_button_colors_sharpen.png
new file mode 100644
index 0000000..9b7152d
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_button_colors_sharpen.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_button_grad.png b/res/drawable-mdpi/filtershow_button_grad.png
new file mode 100644
index 0000000..284da5c
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_button_grad.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_button_redo.png b/res/drawable-mdpi/filtershow_button_redo.png
new file mode 100644
index 0000000..6c5595d
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_button_redo.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_button_undo.png b/res/drawable-mdpi/filtershow_button_undo.png
new file mode 100644
index 0000000..97ee13d
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_button_undo.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_scrubber_control_disabled.png b/res/drawable-mdpi/filtershow_scrubber_control_disabled.png
new file mode 100644
index 0000000..dec1853
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_scrubber_control_disabled.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_scrubber_control_focused.png b/res/drawable-mdpi/filtershow_scrubber_control_focused.png
new file mode 100644
index 0000000..6f7a13d
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_scrubber_control_focused.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_scrubber_control_normal.png b/res/drawable-mdpi/filtershow_scrubber_control_normal.png
new file mode 100644
index 0000000..ae79ae3
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_scrubber_control_normal.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_scrubber_control_pressed.png b/res/drawable-mdpi/filtershow_scrubber_control_pressed.png
new file mode 100644
index 0000000..3060104
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_scrubber_control_pressed.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_scrubber_primary.9.png b/res/drawable-mdpi/filtershow_scrubber_primary.9.png
new file mode 100644
index 0000000..f542580
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_scrubber_primary.9.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_scrubber_secondary.9.png b/res/drawable-mdpi/filtershow_scrubber_secondary.9.png
new file mode 100644
index 0000000..1cad029
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_scrubber_secondary.9.png
Binary files differ
diff --git a/res/drawable-mdpi/filtershow_scrubber_track.9.png b/res/drawable-mdpi/filtershow_scrubber_track.9.png
new file mode 100644
index 0000000..b91a4ee
--- /dev/null
+++ b/res/drawable-mdpi/filtershow_scrubber_track.9.png
Binary files differ
diff --git a/res/drawable-mdpi/frame_overlay_gallery_camera.png b/res/drawable-mdpi/frame_overlay_gallery_camera.png
new file mode 100644
index 0000000..3736a5c
--- /dev/null
+++ b/res/drawable-mdpi/frame_overlay_gallery_camera.png
Binary files differ
diff --git a/res/drawable-mdpi/frame_overlay_gallery_folder.png b/res/drawable-mdpi/frame_overlay_gallery_folder.png
new file mode 100644
index 0000000..89f9c55
--- /dev/null
+++ b/res/drawable-mdpi/frame_overlay_gallery_folder.png
Binary files differ
diff --git a/res/drawable-mdpi/frame_overlay_gallery_picasa.png b/res/drawable-mdpi/frame_overlay_gallery_picasa.png
new file mode 100644
index 0000000..971ee7c
--- /dev/null
+++ b/res/drawable-mdpi/frame_overlay_gallery_picasa.png
Binary files differ
diff --git a/res/drawable-mdpi/grid_pressed.9.png b/res/drawable-mdpi/grid_pressed.9.png
new file mode 100644
index 0000000..68a957f
--- /dev/null
+++ b/res/drawable-mdpi/grid_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi/grid_selected.9.png b/res/drawable-mdpi/grid_selected.9.png
new file mode 100644
index 0000000..a729636
--- /dev/null
+++ b/res/drawable-mdpi/grid_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_360pano_holo_light.png b/res/drawable-mdpi/ic_360pano_holo_light.png
new file mode 100644
index 0000000..7de1ec9
--- /dev/null
+++ b/res/drawable-mdpi/ic_360pano_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_btn_shutter_retake.png b/res/drawable-mdpi/ic_btn_shutter_retake.png
new file mode 100644
index 0000000..dc631db
--- /dev/null
+++ b/res/drawable-mdpi/ic_btn_shutter_retake.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_cameraalbum_overlay.png b/res/drawable-mdpi/ic_cameraalbum_overlay.png
new file mode 100644
index 0000000..5d14c32
--- /dev/null
+++ b/res/drawable-mdpi/ic_cameraalbum_overlay.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_control_play.png b/res/drawable-mdpi/ic_control_play.png
new file mode 100644
index 0000000..2de5b4f
--- /dev/null
+++ b/res/drawable-mdpi/ic_control_play.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_effects_holo_light.png b/res/drawable-mdpi/ic_effects_holo_light.png
new file mode 100644
index 0000000..f15daaa
--- /dev/null
+++ b/res/drawable-mdpi/ic_effects_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_effects_holo_light_xlarge.png b/res/drawable-mdpi/ic_effects_holo_light_xlarge.png
new file mode 100644
index 0000000..9935f9c
--- /dev/null
+++ b/res/drawable-mdpi/ic_effects_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_0.png b/res/drawable-mdpi/ic_exposure_0.png
new file mode 100644
index 0000000..a3dcee4
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_0.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_holo_light.png b/res/drawable-mdpi/ic_exposure_holo_light.png
new file mode 100644
index 0000000..ebd2f41
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_n1.png b/res/drawable-mdpi/ic_exposure_n1.png
new file mode 100644
index 0000000..e73f8b3
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_n1.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_n2.png b/res/drawable-mdpi/ic_exposure_n2.png
new file mode 100644
index 0000000..662c1f8
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_n2.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_n3.png b/res/drawable-mdpi/ic_exposure_n3.png
new file mode 100644
index 0000000..e9c5243
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_n3.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_p1.png b/res/drawable-mdpi/ic_exposure_p1.png
new file mode 100644
index 0000000..a6703c2
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_p1.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_p2.png b/res/drawable-mdpi/ic_exposure_p2.png
new file mode 100644
index 0000000..9e29e4b
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_p2.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_exposure_p3.png b/res/drawable-mdpi/ic_exposure_p3.png
new file mode 100644
index 0000000..89e659a
--- /dev/null
+++ b/res/drawable-mdpi/ic_exposure_p3.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_flash_auto_holo_light.png b/res/drawable-mdpi/ic_flash_auto_holo_light.png
new file mode 100644
index 0000000..55794e4
--- /dev/null
+++ b/res/drawable-mdpi/ic_flash_auto_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_flash_off_holo_light.png b/res/drawable-mdpi/ic_flash_off_holo_light.png
new file mode 100644
index 0000000..73b8ca8
--- /dev/null
+++ b/res/drawable-mdpi/ic_flash_off_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_flash_on_holo_light.png b/res/drawable-mdpi/ic_flash_on_holo_light.png
new file mode 100644
index 0000000..9c99d37
--- /dev/null
+++ b/res/drawable-mdpi/ic_flash_on_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_gallery_play.png b/res/drawable-mdpi/ic_gallery_play.png
new file mode 100644
index 0000000..753231d
--- /dev/null
+++ b/res/drawable-mdpi/ic_gallery_play.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_gallery_play_big.png b/res/drawable-mdpi/ic_gallery_play_big.png
new file mode 100644
index 0000000..19c4a79
--- /dev/null
+++ b/res/drawable-mdpi/ic_gallery_play_big.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_grad_add.png b/res/drawable-mdpi/ic_grad_add.png
new file mode 100644
index 0000000..9465734
--- /dev/null
+++ b/res/drawable-mdpi/ic_grad_add.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_grad_del.png b/res/drawable-mdpi/ic_grad_del.png
new file mode 100644
index 0000000..b6221e6
--- /dev/null
+++ b/res/drawable-mdpi/ic_grad_del.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_hdr.png b/res/drawable-mdpi/ic_hdr.png
new file mode 100644
index 0000000..45dae46
--- /dev/null
+++ b/res/drawable-mdpi/ic_hdr.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_hdr_off.png b/res/drawable-mdpi/ic_hdr_off.png
new file mode 100644
index 0000000..c5003e6
--- /dev/null
+++ b/res/drawable-mdpi/ic_hdr_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_imagesize.png b/res/drawable-mdpi/ic_imagesize.png
new file mode 100644
index 0000000..d3f8b62
--- /dev/null
+++ b/res/drawable-mdpi/ic_imagesize.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_ev_0.png b/res/drawable-mdpi/ic_indicator_ev_0.png
new file mode 100644
index 0000000..6634dd9
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_ev_0.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_ev_n1.png b/res/drawable-mdpi/ic_indicator_ev_n1.png
new file mode 100644
index 0000000..528ecd3
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_ev_n1.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_ev_n2.png b/res/drawable-mdpi/ic_indicator_ev_n2.png
new file mode 100644
index 0000000..db4deb1
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_ev_n2.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_ev_n3.png b/res/drawable-mdpi/ic_indicator_ev_n3.png
new file mode 100644
index 0000000..6b01a56
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_ev_n3.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_ev_p1.png b/res/drawable-mdpi/ic_indicator_ev_p1.png
new file mode 100644
index 0000000..9a1f6f3
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_ev_p1.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_ev_p2.png b/res/drawable-mdpi/ic_indicator_ev_p2.png
new file mode 100644
index 0000000..712ded5
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_ev_p2.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_ev_p3.png b/res/drawable-mdpi/ic_indicator_ev_p3.png
new file mode 100644
index 0000000..d01c2c2
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_ev_p3.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_flash_auto.png b/res/drawable-mdpi/ic_indicator_flash_auto.png
new file mode 100644
index 0000000..c60e423
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_flash_auto.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_flash_off.png b/res/drawable-mdpi/ic_indicator_flash_off.png
new file mode 100644
index 0000000..6c2d75a
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_flash_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_flash_on.png b/res/drawable-mdpi/ic_indicator_flash_on.png
new file mode 100644
index 0000000..8427072
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_flash_on.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_loc_off.png b/res/drawable-mdpi/ic_indicator_loc_off.png
new file mode 100644
index 0000000..87841e3
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_loc_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_loc_on.png b/res/drawable-mdpi/ic_indicator_loc_on.png
new file mode 100644
index 0000000..56a3a3b
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_loc_on.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_sce_hdr.png b/res/drawable-mdpi/ic_indicator_sce_hdr.png
new file mode 100644
index 0000000..7907f64
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_sce_hdr.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_sce_off.png b/res/drawable-mdpi/ic_indicator_sce_off.png
new file mode 100644
index 0000000..4bfd91a
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_sce_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_sce_on.png b/res/drawable-mdpi/ic_indicator_sce_on.png
new file mode 100644
index 0000000..6ea6d77
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_sce_on.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_timer_off.png b/res/drawable-mdpi/ic_indicator_timer_off.png
new file mode 100644
index 0000000..c5f8117
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_timer_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_timer_on.png b/res/drawable-mdpi/ic_indicator_timer_on.png
new file mode 100644
index 0000000..03d6c1b
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_timer_on.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_wb_cloudy.png b/res/drawable-mdpi/ic_indicator_wb_cloudy.png
new file mode 100644
index 0000000..858394f
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_wb_cloudy.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_wb_daylight.png b/res/drawable-mdpi/ic_indicator_wb_daylight.png
new file mode 100644
index 0000000..b8520e1
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_wb_daylight.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_wb_fluorescent.png b/res/drawable-mdpi/ic_indicator_wb_fluorescent.png
new file mode 100644
index 0000000..cb34af1
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_wb_fluorescent.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_wb_off.png b/res/drawable-mdpi/ic_indicator_wb_off.png
new file mode 100644
index 0000000..f20ddf5
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_wb_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_indicator_wb_tungsten.png b/res/drawable-mdpi/ic_indicator_wb_tungsten.png
new file mode 100644
index 0000000..ebfb225
--- /dev/null
+++ b/res/drawable-mdpi/ic_indicator_wb_tungsten.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_location.png b/res/drawable-mdpi/ic_location.png
new file mode 100644
index 0000000..57cecba
--- /dev/null
+++ b/res/drawable-mdpi/ic_location.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_location_off.png b/res/drawable-mdpi/ic_location_off.png
new file mode 100644
index 0000000..548fd96
--- /dev/null
+++ b/res/drawable-mdpi/ic_location_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_camera_holo_light.png b/res/drawable-mdpi/ic_menu_camera_holo_light.png
new file mode 100644
index 0000000..d425084
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_camera_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_cancel_holo_light.png b/res/drawable-mdpi/ic_menu_cancel_holo_light.png
new file mode 100644
index 0000000..d5ca918
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_cancel_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_done_holo_light.png b/res/drawable-mdpi/ic_menu_done_holo_light.png
new file mode 100644
index 0000000..d831414
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_done_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_edit_holo_dark.png b/res/drawable-mdpi/ic_menu_edit_holo_dark.png
new file mode 100644
index 0000000..ca9188e
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_edit_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_info_details.png b/res/drawable-mdpi/ic_menu_info_details.png
new file mode 100644
index 0000000..8aca07d
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_info_details.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_make_offline.png b/res/drawable-mdpi/ic_menu_make_offline.png
new file mode 100644
index 0000000..4c82f14
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_make_offline.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_ptp_holo_light.png b/res/drawable-mdpi/ic_menu_ptp_holo_light.png
new file mode 100644
index 0000000..277a620
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_ptp_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_revert_holo_dark.png b/res/drawable-mdpi/ic_menu_revert_holo_dark.png
new file mode 100644
index 0000000..97ee13d
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_revert_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_savephoto.png b/res/drawable-mdpi/ic_menu_savephoto.png
new file mode 100644
index 0000000..b1dd52a
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_savephoto.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_savephoto_disabled.png b/res/drawable-mdpi/ic_menu_savephoto_disabled.png
new file mode 100755
index 0000000..dc9b406
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_savephoto_disabled.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_share_holo_light.png b/res/drawable-mdpi/ic_menu_share_holo_light.png
new file mode 100644
index 0000000..29574f5
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_share_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_slideshow_holo_light.png b/res/drawable-mdpi/ic_menu_slideshow_holo_light.png
new file mode 100644
index 0000000..a1affcf
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_slideshow_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_tiny_planet.png b/res/drawable-mdpi/ic_menu_tiny_planet.png
new file mode 100644
index 0000000..f3c3030
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_tiny_planet.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_trash_holo_light.png b/res/drawable-mdpi/ic_menu_trash_holo_light.png
new file mode 100644
index 0000000..f45540b
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_trash_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_border_fast.9.png b/res/drawable-mdpi/ic_pan_border_fast.9.png
new file mode 100644
index 0000000..1435c31
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_border_fast.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_border_fast_xlarge.9.png b/res/drawable-mdpi/ic_pan_border_fast_xlarge.9.png
new file mode 100644
index 0000000..332441e
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_border_fast_xlarge.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_left_indicator.png b/res/drawable-mdpi/ic_pan_left_indicator.png
new file mode 100644
index 0000000..7b52deb
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_left_indicator.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_left_indicator_fast.png b/res/drawable-mdpi/ic_pan_left_indicator_fast.png
new file mode 100644
index 0000000..fbabbd2
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_left_indicator_fast.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_left_indicator_fast_xlarge.png b/res/drawable-mdpi/ic_pan_left_indicator_fast_xlarge.png
new file mode 100644
index 0000000..68e17c4
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_left_indicator_fast_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_left_indicator_xlarge.png b/res/drawable-mdpi/ic_pan_left_indicator_xlarge.png
new file mode 100644
index 0000000..4579cf7
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_left_indicator_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_progression.png b/res/drawable-mdpi/ic_pan_progression.png
new file mode 100644
index 0000000..9425f32
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_progression.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_progression_xlarge.png b/res/drawable-mdpi/ic_pan_progression_xlarge.png
new file mode 100644
index 0000000..d75ec8b
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_progression_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_right_indicator.png b/res/drawable-mdpi/ic_pan_right_indicator.png
new file mode 100644
index 0000000..0e8059f
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_right_indicator.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_right_indicator_fast.png b/res/drawable-mdpi/ic_pan_right_indicator_fast.png
new file mode 100644
index 0000000..1917f04
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_right_indicator_fast.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_right_indicator_fast_xlarge.png b/res/drawable-mdpi/ic_pan_right_indicator_fast_xlarge.png
new file mode 100644
index 0000000..dd9794d
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_right_indicator_fast_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pan_right_indicator_xlarge.png b/res/drawable-mdpi/ic_pan_right_indicator_xlarge.png
new file mode 100644
index 0000000..de44206
--- /dev/null
+++ b/res/drawable-mdpi/ic_pan_right_indicator_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_photoeditor_border.png b/res/drawable-mdpi/ic_photoeditor_border.png
new file mode 100644
index 0000000..2f19021
--- /dev/null
+++ b/res/drawable-mdpi/ic_photoeditor_border.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_photoeditor_color.png b/res/drawable-mdpi/ic_photoeditor_color.png
new file mode 100644
index 0000000..baec740
--- /dev/null
+++ b/res/drawable-mdpi/ic_photoeditor_color.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_photoeditor_effects.png b/res/drawable-mdpi/ic_photoeditor_effects.png
new file mode 100644
index 0000000..4547040
--- /dev/null
+++ b/res/drawable-mdpi/ic_photoeditor_effects.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_photoeditor_fix.png b/res/drawable-mdpi/ic_photoeditor_fix.png
new file mode 100644
index 0000000..c54a200
--- /dev/null
+++ b/res/drawable-mdpi/ic_photoeditor_fix.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_recording_indicator.png b/res/drawable-mdpi/ic_recording_indicator.png
new file mode 100755
index 0000000..aa8781d
--- /dev/null
+++ b/res/drawable-mdpi/ic_recording_indicator.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_sce.png b/res/drawable-mdpi/ic_sce.png
new file mode 100644
index 0000000..724d278
--- /dev/null
+++ b/res/drawable-mdpi/ic_sce.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_sce_action.png b/res/drawable-mdpi/ic_sce_action.png
new file mode 100644
index 0000000..2cd9a8c
--- /dev/null
+++ b/res/drawable-mdpi/ic_sce_action.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_sce_night.png b/res/drawable-mdpi/ic_sce_night.png
new file mode 100644
index 0000000..9479262
--- /dev/null
+++ b/res/drawable-mdpi/ic_sce_night.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_sce_off.png b/res/drawable-mdpi/ic_sce_off.png
new file mode 100644
index 0000000..2a3f950
--- /dev/null
+++ b/res/drawable-mdpi/ic_sce_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_sce_party.png b/res/drawable-mdpi/ic_sce_party.png
new file mode 100644
index 0000000..5f0f9cf
--- /dev/null
+++ b/res/drawable-mdpi/ic_sce_party.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_sce_sunset.png b/res/drawable-mdpi/ic_sce_sunset.png
new file mode 100644
index 0000000..0cede83
--- /dev/null
+++ b/res/drawable-mdpi/ic_sce_sunset.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_scn_holo_light.png b/res/drawable-mdpi/ic_scn_holo_light.png
new file mode 100644
index 0000000..b413d60
--- /dev/null
+++ b/res/drawable-mdpi/ic_scn_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_scn_holo_light_xlarge.png b/res/drawable-mdpi/ic_scn_holo_light_xlarge.png
new file mode 100644
index 0000000..0b3866e
--- /dev/null
+++ b/res/drawable-mdpi/ic_scn_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_settings_holo_light.png b/res/drawable-mdpi/ic_settings_holo_light.png
new file mode 100644
index 0000000..5b39398
--- /dev/null
+++ b/res/drawable-mdpi/ic_settings_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_snapshot_border.9.png b/res/drawable-mdpi/ic_snapshot_border.9.png
new file mode 100644
index 0000000..1fa9978
--- /dev/null
+++ b/res/drawable-mdpi/ic_snapshot_border.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_snapshot_border_xlarge.9.png b/res/drawable-mdpi/ic_snapshot_border_xlarge.9.png
new file mode 100644
index 0000000..6b76066
--- /dev/null
+++ b/res/drawable-mdpi/ic_snapshot_border_xlarge.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_back.png b/res/drawable-mdpi/ic_switch_back.png
new file mode 100644
index 0000000..6dce903
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_back.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_camera.png b/res/drawable-mdpi/ic_switch_camera.png
new file mode 100644
index 0000000..a978117
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_camera.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_front.png b/res/drawable-mdpi/ic_switch_front.png
new file mode 100644
index 0000000..c8ff3be
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_front.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_photo_facing_holo_light.png b/res/drawable-mdpi/ic_switch_photo_facing_holo_light.png
new file mode 100644
index 0000000..1e6b72b
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_photo_facing_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_photo_facing_holo_light_xlarge.png b/res/drawable-mdpi/ic_switch_photo_facing_holo_light_xlarge.png
new file mode 100644
index 0000000..cbb0c83
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_photo_facing_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_photosphere.png b/res/drawable-mdpi/ic_switch_photosphere.png
new file mode 100644
index 0000000..1b8db05
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_photosphere.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_video.png b/res/drawable-mdpi/ic_switch_video.png
new file mode 100644
index 0000000..61c37ac
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_video.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_video_facing_holo_light.png b/res/drawable-mdpi/ic_switch_video_facing_holo_light.png
new file mode 100644
index 0000000..205db0b
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_video_facing_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_video_facing_holo_light_xlarge.png b/res/drawable-mdpi/ic_switch_video_facing_holo_light_xlarge.png
new file mode 100644
index 0000000..bf353d2
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_video_facing_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switcher_menu_indicator.png b/res/drawable-mdpi/ic_switcher_menu_indicator.png
new file mode 100644
index 0000000..d6c72bc
--- /dev/null
+++ b/res/drawable-mdpi/ic_switcher_menu_indicator.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_timelapse_none.png b/res/drawable-mdpi/ic_timelapse_none.png
new file mode 100644
index 0000000..122e6fa
--- /dev/null
+++ b/res/drawable-mdpi/ic_timelapse_none.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_timelapse_none_xlarge.png b/res/drawable-mdpi/ic_timelapse_none_xlarge.png
new file mode 100644
index 0000000..67e36a6
--- /dev/null
+++ b/res/drawable-mdpi/ic_timelapse_none_xlarge.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_timer.png b/res/drawable-mdpi/ic_timer.png
new file mode 100644
index 0000000..b55555f
--- /dev/null
+++ b/res/drawable-mdpi/ic_timer.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_vidcontrol_pause.png b/res/drawable-mdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..9319079
--- /dev/null
+++ b/res/drawable-mdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_vidcontrol_play.png b/res/drawable-mdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..8d7cd83
--- /dev/null
+++ b/res/drawable-mdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_vidcontrol_reload.png b/res/drawable-mdpi/ic_vidcontrol_reload.png
new file mode 100644
index 0000000..51b1f48
--- /dev/null
+++ b/res/drawable-mdpi/ic_vidcontrol_reload.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_background_fields_of_wheat_holo.png b/res/drawable-mdpi/ic_video_effects_background_fields_of_wheat_holo.png
new file mode 100644
index 0000000..7b4e579
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_background_fields_of_wheat_holo.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_background_intergalactic_holo.png b/res/drawable-mdpi/ic_video_effects_background_intergalactic_holo.png
new file mode 100644
index 0000000..ec9c1bc
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_background_intergalactic_holo.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_background_normal_holo_dark.png b/res/drawable-mdpi/ic_video_effects_background_normal_holo_dark.png
new file mode 100644
index 0000000..f055c48
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_background_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_faces_big_eyes_holo_dark.png b/res/drawable-mdpi/ic_video_effects_faces_big_eyes_holo_dark.png
new file mode 100644
index 0000000..4759c4f
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_faces_big_eyes_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_faces_big_mouth_holo_dark.png b/res/drawable-mdpi/ic_video_effects_faces_big_mouth_holo_dark.png
new file mode 100644
index 0000000..ebd4eeb
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_faces_big_mouth_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_faces_big_nose_holo_dark.png b/res/drawable-mdpi/ic_video_effects_faces_big_nose_holo_dark.png
new file mode 100644
index 0000000..d292b41
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_faces_big_nose_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_faces_small_eyes_holo_dark.png b/res/drawable-mdpi/ic_video_effects_faces_small_eyes_holo_dark.png
new file mode 100644
index 0000000..42f55ff
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_faces_small_eyes_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_faces_small_mouth_holo_dark.png b/res/drawable-mdpi/ic_video_effects_faces_small_mouth_holo_dark.png
new file mode 100644
index 0000000..0bab7fb
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_faces_small_mouth_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_effects_faces_squeeze_holo_dark.png b/res/drawable-mdpi/ic_video_effects_faces_squeeze_holo_dark.png
new file mode 100644
index 0000000..46630fa
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_effects_faces_squeeze_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_video_thumb.png b/res/drawable-mdpi/ic_video_thumb.png
new file mode 100644
index 0000000..f329175
--- /dev/null
+++ b/res/drawable-mdpi/ic_video_thumb.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_view_photosphere.png b/res/drawable-mdpi/ic_view_photosphere.png
new file mode 100644
index 0000000..9b79e61
--- /dev/null
+++ b/res/drawable-mdpi/ic_view_photosphere.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_wb_auto.png b/res/drawable-mdpi/ic_wb_auto.png
new file mode 100644
index 0000000..3da3a5c
--- /dev/null
+++ b/res/drawable-mdpi/ic_wb_auto.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_wb_cloudy.png b/res/drawable-mdpi/ic_wb_cloudy.png
new file mode 100644
index 0000000..5dc6fbd
--- /dev/null
+++ b/res/drawable-mdpi/ic_wb_cloudy.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_wb_fluorescent.png b/res/drawable-mdpi/ic_wb_fluorescent.png
new file mode 100644
index 0000000..87c001b
--- /dev/null
+++ b/res/drawable-mdpi/ic_wb_fluorescent.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_wb_incandescent.png b/res/drawable-mdpi/ic_wb_incandescent.png
new file mode 100644
index 0000000..f6512ef
--- /dev/null
+++ b/res/drawable-mdpi/ic_wb_incandescent.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_wb_sunlight.png b/res/drawable-mdpi/ic_wb_sunlight.png
new file mode 100644
index 0000000..4a07ec4
--- /dev/null
+++ b/res/drawable-mdpi/ic_wb_sunlight.png
Binary files differ
diff --git a/res/drawable-mdpi/icn_media_pause_focused_holo_dark.png b/res/drawable-mdpi/icn_media_pause_focused_holo_dark.png
new file mode 100644
index 0000000..52043b2
--- /dev/null
+++ b/res/drawable-mdpi/icn_media_pause_focused_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/icn_media_pause_normal_holo_dark.png b/res/drawable-mdpi/icn_media_pause_normal_holo_dark.png
new file mode 100644
index 0000000..8573b8f
--- /dev/null
+++ b/res/drawable-mdpi/icn_media_pause_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/icn_media_pause_pressed_holo_dark.png b/res/drawable-mdpi/icn_media_pause_pressed_holo_dark.png
new file mode 100644
index 0000000..afe534b
--- /dev/null
+++ b/res/drawable-mdpi/icn_media_pause_pressed_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/icn_media_play_focused_holo_dark.png b/res/drawable-mdpi/icn_media_play_focused_holo_dark.png
new file mode 100644
index 0000000..c71bcad
--- /dev/null
+++ b/res/drawable-mdpi/icn_media_play_focused_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/icn_media_play_normal_holo_dark.png b/res/drawable-mdpi/icn_media_play_normal_holo_dark.png
new file mode 100644
index 0000000..f8d5e69
--- /dev/null
+++ b/res/drawable-mdpi/icn_media_play_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/icn_media_play_pressed_holo_dark.png b/res/drawable-mdpi/icn_media_play_pressed_holo_dark.png
new file mode 100644
index 0000000..817f476
--- /dev/null
+++ b/res/drawable-mdpi/icn_media_play_pressed_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/list_divider.9.png b/res/drawable-mdpi/list_divider.9.png
new file mode 100644
index 0000000..986ab0b
--- /dev/null
+++ b/res/drawable-mdpi/list_divider.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_divider_holo_dark.9.png b/res/drawable-mdpi/list_divider_holo_dark.9.png
new file mode 100644
index 0000000..986ab0b
--- /dev/null
+++ b/res/drawable-mdpi/list_divider_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_pressed_holo_light.9.png b/res/drawable-mdpi/list_pressed_holo_light.9.png
new file mode 100644
index 0000000..6e77525
--- /dev/null
+++ b/res/drawable-mdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_selector_background_selected.9.png b/res/drawable-mdpi/list_selector_background_selected.9.png
new file mode 100644
index 0000000..a4ac1e3
--- /dev/null
+++ b/res/drawable-mdpi/list_selector_background_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi/menu_dropdown_panel_holo_dark.9.png b/res/drawable-mdpi/menu_dropdown_panel_holo_dark.9.png
new file mode 100644
index 0000000..460ec46
--- /dev/null
+++ b/res/drawable-mdpi/menu_dropdown_panel_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/on_screen_hint_frame.9.png b/res/drawable-mdpi/on_screen_hint_frame.9.png
new file mode 100644
index 0000000..8b78d67
--- /dev/null
+++ b/res/drawable-mdpi/on_screen_hint_frame.9.png
Binary files differ
diff --git a/res/drawable-mdpi/overscroll_edge.png b/res/drawable-mdpi/overscroll_edge.png
new file mode 100644
index 0000000..4c87a8b
--- /dev/null
+++ b/res/drawable-mdpi/overscroll_edge.png
Binary files differ
diff --git a/res/drawable-mdpi/overscroll_glow.png b/res/drawable-mdpi/overscroll_glow.png
new file mode 100644
index 0000000..8389ef4
--- /dev/null
+++ b/res/drawable-mdpi/overscroll_glow.png
Binary files differ
diff --git a/res/drawable-mdpi/panel_undo_holo.9.png b/res/drawable-mdpi/panel_undo_holo.9.png
new file mode 100644
index 0000000..291a936
--- /dev/null
+++ b/res/drawable-mdpi/panel_undo_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/placeholder_camera.png b/res/drawable-mdpi/placeholder_camera.png
new file mode 100644
index 0000000..f7cbb1a
--- /dev/null
+++ b/res/drawable-mdpi/placeholder_camera.png
Binary files differ
diff --git a/res/drawable-mdpi/placeholder_empty.png b/res/drawable-mdpi/placeholder_empty.png
new file mode 100644
index 0000000..3d6118d
--- /dev/null
+++ b/res/drawable-mdpi/placeholder_empty.png
Binary files differ
diff --git a/res/drawable-mdpi/placeholder_locked.png b/res/drawable-mdpi/placeholder_locked.png
new file mode 100644
index 0000000..500aaa0
--- /dev/null
+++ b/res/drawable-mdpi/placeholder_locked.png
Binary files differ
diff --git a/res/drawable-mdpi/preview.png b/res/drawable-mdpi/preview.png
new file mode 100644
index 0000000..6cddeab
--- /dev/null
+++ b/res/drawable-mdpi/preview.png
Binary files differ
diff --git a/res/drawable-mdpi/scrubber_knob.png b/res/drawable-mdpi/scrubber_knob.png
new file mode 100644
index 0000000..9205d9c
--- /dev/null
+++ b/res/drawable-mdpi/scrubber_knob.png
Binary files differ
diff --git a/res/drawable-mdpi/spinner_76_inner_holo.png b/res/drawable-mdpi/spinner_76_inner_holo.png
new file mode 100644
index 0000000..21a52cd
--- /dev/null
+++ b/res/drawable-mdpi/spinner_76_inner_holo.png
Binary files differ
diff --git a/res/drawable-mdpi/spinner_76_outer_holo.png b/res/drawable-mdpi/spinner_76_outer_holo.png
new file mode 100644
index 0000000..df9d88c
--- /dev/null
+++ b/res/drawable-mdpi/spinner_76_outer_holo.png
Binary files differ
diff --git a/res/drawable-mdpi/switch_bg_focused_holo_dark.9.png b/res/drawable-mdpi/switch_bg_focused_holo_dark.9.png
new file mode 100644
index 0000000..914e433
--- /dev/null
+++ b/res/drawable-mdpi/switch_bg_focused_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/switch_bg_holo_dark.9.png b/res/drawable-mdpi/switch_bg_holo_dark.9.png
new file mode 100644
index 0000000..b5582b5
--- /dev/null
+++ b/res/drawable-mdpi/switch_bg_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/switch_thumb_activated_holo_dark.9.png b/res/drawable-mdpi/switch_thumb_activated_holo_dark.9.png
new file mode 100644
index 0000000..3d7c236
--- /dev/null
+++ b/res/drawable-mdpi/switch_thumb_activated_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/switch_thumb_disabled_holo_dark.9.png b/res/drawable-mdpi/switch_thumb_disabled_holo_dark.9.png
new file mode 100644
index 0000000..82f05d6
--- /dev/null
+++ b/res/drawable-mdpi/switch_thumb_disabled_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/switch_thumb_holo_dark.9.png b/res/drawable-mdpi/switch_thumb_holo_dark.9.png
new file mode 100644
index 0000000..9bc7a68
--- /dev/null
+++ b/res/drawable-mdpi/switch_thumb_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/switch_thumb_pressed_holo_dark.9.png b/res/drawable-mdpi/switch_thumb_pressed_holo_dark.9.png
new file mode 100644
index 0000000..670dc2e
--- /dev/null
+++ b/res/drawable-mdpi/switch_thumb_pressed_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/text_select_handle_left.png b/res/drawable-mdpi/text_select_handle_left.png
new file mode 100644
index 0000000..750cdea
--- /dev/null
+++ b/res/drawable-mdpi/text_select_handle_left.png
Binary files differ
diff --git a/res/drawable-mdpi/text_select_handle_right.png b/res/drawable-mdpi/text_select_handle_right.png
new file mode 100644
index 0000000..fc3d144
--- /dev/null
+++ b/res/drawable-mdpi/text_select_handle_right.png
Binary files differ
diff --git a/res/drawable-mdpi/toast_frame_holo.9.png b/res/drawable-mdpi/toast_frame_holo.9.png
new file mode 100644
index 0000000..f8f75db
--- /dev/null
+++ b/res/drawable-mdpi/toast_frame_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/wallpaper_picker_preview.png b/res/drawable-mdpi/wallpaper_picker_preview.png
new file mode 100644
index 0000000..452b125
--- /dev/null
+++ b/res/drawable-mdpi/wallpaper_picker_preview.png
Binary files differ
diff --git a/res/drawable-nodpi/brush_marker.png b/res/drawable-nodpi/brush_marker.png
new file mode 100644
index 0000000..24eb747
--- /dev/null
+++ b/res/drawable-nodpi/brush_marker.png
Binary files differ
diff --git a/res/drawable-nodpi/brush_spatter.png b/res/drawable-nodpi/brush_spatter.png
new file mode 100644
index 0000000..ae15c22
--- /dev/null
+++ b/res/drawable-nodpi/brush_spatter.png
Binary files differ
diff --git a/res/drawable-nodpi/filtershow_icon_vignette.png b/res/drawable-nodpi/filtershow_icon_vignette.png
new file mode 100644
index 0000000..88d1a96
--- /dev/null
+++ b/res/drawable-nodpi/filtershow_icon_vignette.png
Binary files differ
diff --git a/res/drawable-nodpi/geometry_shadow.9.png b/res/drawable-nodpi/geometry_shadow.9.png
new file mode 100644
index 0000000..2f7abdc
--- /dev/null
+++ b/res/drawable-nodpi/geometry_shadow.9.png
Binary files differ
diff --git a/res/drawable-port-hdpi/btn_video_shutter_recording_holo_xlarge.png b/res/drawable-port-hdpi/btn_video_shutter_recording_holo_xlarge.png
new file mode 100644
index 0000000..8420c06
--- /dev/null
+++ b/res/drawable-port-hdpi/btn_video_shutter_recording_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-port-hdpi/btn_video_shutter_recording_pressed_holo_xlarge.png b/res/drawable-port-hdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
new file mode 100644
index 0000000..e8441f9
--- /dev/null
+++ b/res/drawable-port-hdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-port-hdpi/switcher_bg.9.png b/res/drawable-port-hdpi/switcher_bg.9.png
new file mode 100644
index 0000000..e6b74a4
--- /dev/null
+++ b/res/drawable-port-hdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-port-mdpi/btn_video_shutter_recording_holo_xlarge.png b/res/drawable-port-mdpi/btn_video_shutter_recording_holo_xlarge.png
new file mode 100644
index 0000000..c35e6ff
--- /dev/null
+++ b/res/drawable-port-mdpi/btn_video_shutter_recording_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-port-mdpi/btn_video_shutter_recording_pressed_holo_xlarge.png b/res/drawable-port-mdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
new file mode 100644
index 0000000..85a6b13
--- /dev/null
+++ b/res/drawable-port-mdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-port-mdpi/switcher_bg.9.png b/res/drawable-port-mdpi/switcher_bg.9.png
new file mode 100644
index 0000000..6c87aa3
--- /dev/null
+++ b/res/drawable-port-mdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-port-xhdpi/btn_video_shutter_recording_holo_xlarge.png b/res/drawable-port-xhdpi/btn_video_shutter_recording_holo_xlarge.png
new file mode 100644
index 0000000..fc27944
--- /dev/null
+++ b/res/drawable-port-xhdpi/btn_video_shutter_recording_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-port-xhdpi/btn_video_shutter_recording_pressed_holo_xlarge.png b/res/drawable-port-xhdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
new file mode 100644
index 0000000..f58dba8
--- /dev/null
+++ b/res/drawable-port-xhdpi/btn_video_shutter_recording_pressed_holo_xlarge.png
Binary files differ
diff --git a/res/drawable-port-xhdpi/switcher_bg.9.png b/res/drawable-port-xhdpi/switcher_bg.9.png
new file mode 100644
index 0000000..bbe21ba
--- /dev/null
+++ b/res/drawable-port-xhdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/btn_shutter_default.png b/res/drawable-sw600dp-hdpi/btn_shutter_default.png
new file mode 100644
index 0000000..bdd641f
--- /dev/null
+++ b/res/drawable-sw600dp-hdpi/btn_shutter_default.png
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/btn_shutter_pressed.png b/res/drawable-sw600dp-hdpi/btn_shutter_pressed.png
new file mode 100644
index 0000000..80f7ef5
--- /dev/null
+++ b/res/drawable-sw600dp-hdpi/btn_shutter_pressed.png
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/btn_shutter_recording.png b/res/drawable-sw600dp-hdpi/btn_shutter_recording.png
new file mode 100644
index 0000000..80f04ff
--- /dev/null
+++ b/res/drawable-sw600dp-hdpi/btn_shutter_recording.png
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/btn_shutter_video_default.png b/res/drawable-sw600dp-hdpi/btn_shutter_video_default.png
new file mode 100644
index 0000000..44ea9b0
--- /dev/null
+++ b/res/drawable-sw600dp-hdpi/btn_shutter_video_default.png
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/btn_shutter_video_pressed.png b/res/drawable-sw600dp-hdpi/btn_shutter_video_pressed.png
new file mode 100644
index 0000000..fc8173d
--- /dev/null
+++ b/res/drawable-sw600dp-hdpi/btn_shutter_video_pressed.png
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/btn_shutter_video_recording.png b/res/drawable-sw600dp-hdpi/btn_shutter_video_recording.png
new file mode 100644
index 0000000..65d9879
--- /dev/null
+++ b/res/drawable-sw600dp-hdpi/btn_shutter_video_recording.png
Binary files differ
diff --git a/res/drawable-sw600dp-land-hdpi/switcher_bg.9.png b/res/drawable-sw600dp-land-hdpi/switcher_bg.9.png
new file mode 100644
index 0000000..21375b1
--- /dev/null
+++ b/res/drawable-sw600dp-land-hdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-land-mdpi/switcher_bg.9.png b/res/drawable-sw600dp-land-mdpi/switcher_bg.9.png
new file mode 100644
index 0000000..bfd996a
--- /dev/null
+++ b/res/drawable-sw600dp-land-mdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-land-xhdpi/switcher_bg.9.png b/res/drawable-sw600dp-land-xhdpi/switcher_bg.9.png
new file mode 100644
index 0000000..35a71db
--- /dev/null
+++ b/res/drawable-sw600dp-land-xhdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/btn_shutter_default.png b/res/drawable-sw600dp-mdpi/btn_shutter_default.png
new file mode 100644
index 0000000..bf6cf2c
--- /dev/null
+++ b/res/drawable-sw600dp-mdpi/btn_shutter_default.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/btn_shutter_pressed.png b/res/drawable-sw600dp-mdpi/btn_shutter_pressed.png
new file mode 100644
index 0000000..d8b29e6
--- /dev/null
+++ b/res/drawable-sw600dp-mdpi/btn_shutter_pressed.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/btn_shutter_recording.png b/res/drawable-sw600dp-mdpi/btn_shutter_recording.png
new file mode 100644
index 0000000..5f56b49
--- /dev/null
+++ b/res/drawable-sw600dp-mdpi/btn_shutter_recording.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/btn_shutter_video_default.png b/res/drawable-sw600dp-mdpi/btn_shutter_video_default.png
new file mode 100644
index 0000000..67f5532
--- /dev/null
+++ b/res/drawable-sw600dp-mdpi/btn_shutter_video_default.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/btn_shutter_video_pressed.png b/res/drawable-sw600dp-mdpi/btn_shutter_video_pressed.png
new file mode 100644
index 0000000..3a1e613
--- /dev/null
+++ b/res/drawable-sw600dp-mdpi/btn_shutter_video_pressed.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/btn_shutter_video_recording.png b/res/drawable-sw600dp-mdpi/btn_shutter_video_recording.png
new file mode 100644
index 0000000..ecf0276
--- /dev/null
+++ b/res/drawable-sw600dp-mdpi/btn_shutter_video_recording.png
Binary files differ
diff --git a/res/drawable-sw600dp-port-hdpi/switcher_bg.9.png b/res/drawable-sw600dp-port-hdpi/switcher_bg.9.png
new file mode 100644
index 0000000..250276a
--- /dev/null
+++ b/res/drawable-sw600dp-port-hdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-port-mdpi/switcher_bg.9.png b/res/drawable-sw600dp-port-mdpi/switcher_bg.9.png
new file mode 100644
index 0000000..9c4e293
--- /dev/null
+++ b/res/drawable-sw600dp-port-mdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-port-xhdpi/switcher_bg.9.png b/res/drawable-sw600dp-port-xhdpi/switcher_bg.9.png
new file mode 100644
index 0000000..2d0171e
--- /dev/null
+++ b/res/drawable-sw600dp-port-xhdpi/switcher_bg.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/btn_shutter_default.png b/res/drawable-sw600dp-xhdpi/btn_shutter_default.png
new file mode 100644
index 0000000..b7aa393
--- /dev/null
+++ b/res/drawable-sw600dp-xhdpi/btn_shutter_default.png
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/btn_shutter_pressed.png b/res/drawable-sw600dp-xhdpi/btn_shutter_pressed.png
new file mode 100644
index 0000000..4a08947
--- /dev/null
+++ b/res/drawable-sw600dp-xhdpi/btn_shutter_pressed.png
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/btn_shutter_recording.png b/res/drawable-sw600dp-xhdpi/btn_shutter_recording.png
new file mode 100644
index 0000000..0a0e108
--- /dev/null
+++ b/res/drawable-sw600dp-xhdpi/btn_shutter_recording.png
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/btn_shutter_video_default.png b/res/drawable-sw600dp-xhdpi/btn_shutter_video_default.png
new file mode 100644
index 0000000..acb8d21
--- /dev/null
+++ b/res/drawable-sw600dp-xhdpi/btn_shutter_video_default.png
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/btn_shutter_video_pressed.png b/res/drawable-sw600dp-xhdpi/btn_shutter_video_pressed.png
new file mode 100644
index 0000000..7621341
--- /dev/null
+++ b/res/drawable-sw600dp-xhdpi/btn_shutter_video_pressed.png
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/btn_shutter_video_recording.png b/res/drawable-sw600dp-xhdpi/btn_shutter_video_recording.png
new file mode 100644
index 0000000..aa3a4bd
--- /dev/null
+++ b/res/drawable-sw600dp-xhdpi/btn_shutter_video_recording.png
Binary files differ
diff --git a/res/drawable-sw600dp/bg_vidcontrol.png b/res/drawable-sw600dp/bg_vidcontrol.png
new file mode 100644
index 0000000..dfe2da1
--- /dev/null
+++ b/res/drawable-sw600dp/bg_vidcontrol.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_pan_thumb.9.png b/res/drawable-sw600dp/ic_pan_thumb.9.png
new file mode 100644
index 0000000..1648026
--- /dev/null
+++ b/res/drawable-sw600dp/ic_pan_thumb.9.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_vidcontrol_pause.png b/res/drawable-sw600dp/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..3d1a8bf
--- /dev/null
+++ b/res/drawable-sw600dp/ic_vidcontrol_pause.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_vidcontrol_play.png b/res/drawable-sw600dp/ic_vidcontrol_play.png
new file mode 100644
index 0000000..cc02166
--- /dev/null
+++ b/res/drawable-sw600dp/ic_vidcontrol_play.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_vidcontrol_reload.png b/res/drawable-sw600dp/ic_vidcontrol_reload.png
new file mode 100644
index 0000000..bf8d529
--- /dev/null
+++ b/res/drawable-sw600dp/ic_vidcontrol_reload.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_video_thumb.png b/res/drawable-sw600dp/ic_video_thumb.png
new file mode 100644
index 0000000..743cec2
--- /dev/null
+++ b/res/drawable-sw600dp/ic_video_thumb.png
Binary files differ
diff --git a/res/drawable-sw600dp/scrubber_knob.png b/res/drawable-sw600dp/scrubber_knob.png
new file mode 100644
index 0000000..426e3da
--- /dev/null
+++ b/res/drawable-sw600dp/scrubber_knob.png
Binary files differ
diff --git a/res/drawable-xhdpi/actionbar_translucent.9.png b/res/drawable-xhdpi/actionbar_translucent.9.png
new file mode 100644
index 0000000..f4ed5fa
--- /dev/null
+++ b/res/drawable-xhdpi/actionbar_translucent.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/bg_vidcontrol.png b/res/drawable-xhdpi/bg_vidcontrol.png
new file mode 100644
index 0000000..0eb8148
--- /dev/null
+++ b/res/drawable-xhdpi/bg_vidcontrol.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_shutter_default.png b/res/drawable-xhdpi/btn_shutter_default.png
new file mode 100644
index 0000000..5d2b1d2
--- /dev/null
+++ b/res/drawable-xhdpi/btn_shutter_default.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_shutter_pressed.png b/res/drawable-xhdpi/btn_shutter_pressed.png
new file mode 100644
index 0000000..303e361
--- /dev/null
+++ b/res/drawable-xhdpi/btn_shutter_pressed.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_shutter_recording.png b/res/drawable-xhdpi/btn_shutter_recording.png
new file mode 100644
index 0000000..1bd1743
--- /dev/null
+++ b/res/drawable-xhdpi/btn_shutter_recording.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_shutter_video_default.png b/res/drawable-xhdpi/btn_shutter_video_default.png
new file mode 100644
index 0000000..10482f5
--- /dev/null
+++ b/res/drawable-xhdpi/btn_shutter_video_default.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_shutter_video_pressed.png b/res/drawable-xhdpi/btn_shutter_video_pressed.png
new file mode 100644
index 0000000..35f1ddc
--- /dev/null
+++ b/res/drawable-xhdpi/btn_shutter_video_pressed.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_shutter_video_recording.png b/res/drawable-xhdpi/btn_shutter_video_recording.png
new file mode 100644
index 0000000..ba1fb5c
--- /dev/null
+++ b/res/drawable-xhdpi/btn_shutter_video_recording.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_video_shutter_recording_holo.png b/res/drawable-xhdpi/btn_video_shutter_recording_holo.png
new file mode 100644
index 0000000..e164cae
--- /dev/null
+++ b/res/drawable-xhdpi/btn_video_shutter_recording_holo.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_video_shutter_recording_pressed_holo.png b/res/drawable-xhdpi/btn_video_shutter_recording_pressed_holo.png
new file mode 100644
index 0000000..164cd36
--- /dev/null
+++ b/res/drawable-xhdpi/btn_video_shutter_recording_pressed_holo.png
Binary files differ
diff --git a/res/drawable-xhdpi/camera_crop.png b/res/drawable-xhdpi/camera_crop.png
new file mode 100644
index 0000000..e0a53bc
--- /dev/null
+++ b/res/drawable-xhdpi/camera_crop.png
Binary files differ
diff --git a/res/drawable-xhdpi/capture_thumbnail_shadow.9.png b/res/drawable-xhdpi/capture_thumbnail_shadow.9.png
new file mode 100644
index 0000000..3d771f7
--- /dev/null
+++ b/res/drawable-xhdpi/capture_thumbnail_shadow.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/dialog_full_holo_dark.9.png b/res/drawable-xhdpi/dialog_full_holo_dark.9.png
new file mode 100644
index 0000000..f4970ad
--- /dev/null
+++ b/res/drawable-xhdpi/dialog_full_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png b/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png
new file mode 100644
index 0000000..36d8cf4
--- /dev/null
+++ b/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_button_colors_curve.png b/res/drawable-xhdpi/filtershow_button_colors_curve.png
new file mode 100644
index 0000000..7d80a59
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_button_colors_curve.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_button_colors_sharpen.png b/res/drawable-xhdpi/filtershow_button_colors_sharpen.png
new file mode 100644
index 0000000..951de4e
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_button_colors_sharpen.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_button_grad.png b/res/drawable-xhdpi/filtershow_button_grad.png
new file mode 100644
index 0000000..c5f7035
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_button_grad.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_button_redo.png b/res/drawable-xhdpi/filtershow_button_redo.png
new file mode 100644
index 0000000..61eafb9
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_button_redo.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_button_undo.png b/res/drawable-xhdpi/filtershow_button_undo.png
new file mode 100644
index 0000000..48ff5bc
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_button_undo.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_scrubber_control_disabled.png b/res/drawable-xhdpi/filtershow_scrubber_control_disabled.png
new file mode 100644
index 0000000..f7ba38f
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_scrubber_control_disabled.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_scrubber_control_focused.png b/res/drawable-xhdpi/filtershow_scrubber_control_focused.png
new file mode 100644
index 0000000..d40136c
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_scrubber_control_focused.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_scrubber_control_normal.png b/res/drawable-xhdpi/filtershow_scrubber_control_normal.png
new file mode 100644
index 0000000..877935d
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_scrubber_control_normal.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_scrubber_control_pressed.png b/res/drawable-xhdpi/filtershow_scrubber_control_pressed.png
new file mode 100644
index 0000000..216aaf8
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_scrubber_control_pressed.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_scrubber_primary.9.png b/res/drawable-xhdpi/filtershow_scrubber_primary.9.png
new file mode 100644
index 0000000..aae37aa
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_scrubber_primary.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_scrubber_secondary.9.png b/res/drawable-xhdpi/filtershow_scrubber_secondary.9.png
new file mode 100644
index 0000000..defea53
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_scrubber_secondary.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/filtershow_scrubber_track.9.png b/res/drawable-xhdpi/filtershow_scrubber_track.9.png
new file mode 100644
index 0000000..bfb2048
--- /dev/null
+++ b/res/drawable-xhdpi/filtershow_scrubber_track.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/frame_overlay_gallery_camera.png b/res/drawable-xhdpi/frame_overlay_gallery_camera.png
new file mode 100644
index 0000000..e2109ac
--- /dev/null
+++ b/res/drawable-xhdpi/frame_overlay_gallery_camera.png
Binary files differ
diff --git a/res/drawable-xhdpi/frame_overlay_gallery_folder.png b/res/drawable-xhdpi/frame_overlay_gallery_folder.png
new file mode 100644
index 0000000..2c9b333
--- /dev/null
+++ b/res/drawable-xhdpi/frame_overlay_gallery_folder.png
Binary files differ
diff --git a/res/drawable-xhdpi/frame_overlay_gallery_picasa.png b/res/drawable-xhdpi/frame_overlay_gallery_picasa.png
new file mode 100644
index 0000000..6364316
--- /dev/null
+++ b/res/drawable-xhdpi/frame_overlay_gallery_picasa.png
Binary files differ
diff --git a/res/drawable-xhdpi/grid_pressed.9.png b/res/drawable-xhdpi/grid_pressed.9.png
new file mode 100644
index 0000000..ea317b5
--- /dev/null
+++ b/res/drawable-xhdpi/grid_pressed.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/grid_selected.9.png b/res/drawable-xhdpi/grid_selected.9.png
new file mode 100644
index 0000000..dec94ce
--- /dev/null
+++ b/res/drawable-xhdpi/grid_selected.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_360pano_holo_light.png b/res/drawable-xhdpi/ic_360pano_holo_light.png
new file mode 100644
index 0000000..0ea4310
--- /dev/null
+++ b/res/drawable-xhdpi/ic_360pano_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_btn_shutter_retake.png b/res/drawable-xhdpi/ic_btn_shutter_retake.png
new file mode 100644
index 0000000..ec8c50e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_btn_shutter_retake.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_cameraalbum_overlay.png b/res/drawable-xhdpi/ic_cameraalbum_overlay.png
new file mode 100644
index 0000000..bf71eaa
--- /dev/null
+++ b/res/drawable-xhdpi/ic_cameraalbum_overlay.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_effects_holo_light.png b/res/drawable-xhdpi/ic_effects_holo_light.png
new file mode 100644
index 0000000..5756fb7
--- /dev/null
+++ b/res/drawable-xhdpi/ic_effects_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_effects_holo_light_xlarge.png b/res/drawable-xhdpi/ic_effects_holo_light_xlarge.png
new file mode 100644
index 0000000..8590a9e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_effects_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_0.png b/res/drawable-xhdpi/ic_exposure_0.png
new file mode 100644
index 0000000..5752ed7
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_0.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_holo_light.png b/res/drawable-xhdpi/ic_exposure_holo_light.png
new file mode 100644
index 0000000..06501ad
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_n1.png b/res/drawable-xhdpi/ic_exposure_n1.png
new file mode 100644
index 0000000..d7fe917
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_n1.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_n2.png b/res/drawable-xhdpi/ic_exposure_n2.png
new file mode 100644
index 0000000..5c47b96
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_n2.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_n3.png b/res/drawable-xhdpi/ic_exposure_n3.png
new file mode 100644
index 0000000..6bf4ccb
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_n3.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_p1.png b/res/drawable-xhdpi/ic_exposure_p1.png
new file mode 100644
index 0000000..c30265e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_p1.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_p2.png b/res/drawable-xhdpi/ic_exposure_p2.png
new file mode 100644
index 0000000..183d25f
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_p2.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_exposure_p3.png b/res/drawable-xhdpi/ic_exposure_p3.png
new file mode 100644
index 0000000..581a9a7
--- /dev/null
+++ b/res/drawable-xhdpi/ic_exposure_p3.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_flash_auto_holo_light.png b/res/drawable-xhdpi/ic_flash_auto_holo_light.png
new file mode 100644
index 0000000..266d1c2
--- /dev/null
+++ b/res/drawable-xhdpi/ic_flash_auto_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_flash_off_holo_light.png b/res/drawable-xhdpi/ic_flash_off_holo_light.png
new file mode 100644
index 0000000..e289ac2
--- /dev/null
+++ b/res/drawable-xhdpi/ic_flash_off_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_flash_on_holo_light.png b/res/drawable-xhdpi/ic_flash_on_holo_light.png
new file mode 100644
index 0000000..e2c63c7
--- /dev/null
+++ b/res/drawable-xhdpi/ic_flash_on_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_gallery_play.png b/res/drawable-xhdpi/ic_gallery_play.png
new file mode 100644
index 0000000..70901e5
--- /dev/null
+++ b/res/drawable-xhdpi/ic_gallery_play.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_gallery_play_big.png b/res/drawable-xhdpi/ic_gallery_play_big.png
new file mode 100644
index 0000000..f677b26
--- /dev/null
+++ b/res/drawable-xhdpi/ic_gallery_play_big.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_grad_add.png b/res/drawable-xhdpi/ic_grad_add.png
new file mode 100644
index 0000000..ca7b654
--- /dev/null
+++ b/res/drawable-xhdpi/ic_grad_add.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_grad_del.png b/res/drawable-xhdpi/ic_grad_del.png
new file mode 100644
index 0000000..9dfb392
--- /dev/null
+++ b/res/drawable-xhdpi/ic_grad_del.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_hdr.png b/res/drawable-xhdpi/ic_hdr.png
new file mode 100644
index 0000000..12203a2
--- /dev/null
+++ b/res/drawable-xhdpi/ic_hdr.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_hdr_off.png b/res/drawable-xhdpi/ic_hdr_off.png
new file mode 100644
index 0000000..66fe29e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_hdr_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_imagesize.png b/res/drawable-xhdpi/ic_imagesize.png
new file mode 100644
index 0000000..54fd008
--- /dev/null
+++ b/res/drawable-xhdpi/ic_imagesize.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_ev_0.png b/res/drawable-xhdpi/ic_indicator_ev_0.png
new file mode 100644
index 0000000..b86bb30
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_ev_0.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_ev_n1.png b/res/drawable-xhdpi/ic_indicator_ev_n1.png
new file mode 100644
index 0000000..e99cc66
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_ev_n1.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_ev_n2.png b/res/drawable-xhdpi/ic_indicator_ev_n2.png
new file mode 100644
index 0000000..aced71d
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_ev_n2.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_ev_n3.png b/res/drawable-xhdpi/ic_indicator_ev_n3.png
new file mode 100644
index 0000000..41b9f59
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_ev_n3.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_ev_p1.png b/res/drawable-xhdpi/ic_indicator_ev_p1.png
new file mode 100644
index 0000000..2850c95
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_ev_p1.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_ev_p2.png b/res/drawable-xhdpi/ic_indicator_ev_p2.png
new file mode 100644
index 0000000..c6355b1
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_ev_p2.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_ev_p3.png b/res/drawable-xhdpi/ic_indicator_ev_p3.png
new file mode 100644
index 0000000..99b860d
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_ev_p3.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_flash_auto.png b/res/drawable-xhdpi/ic_indicator_flash_auto.png
new file mode 100644
index 0000000..08f507b
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_flash_auto.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_flash_off.png b/res/drawable-xhdpi/ic_indicator_flash_off.png
new file mode 100644
index 0000000..a3580f1
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_flash_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_flash_on.png b/res/drawable-xhdpi/ic_indicator_flash_on.png
new file mode 100644
index 0000000..7e05e15
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_flash_on.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_loc_off.png b/res/drawable-xhdpi/ic_indicator_loc_off.png
new file mode 100644
index 0000000..966855c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_loc_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_loc_on.png b/res/drawable-xhdpi/ic_indicator_loc_on.png
new file mode 100644
index 0000000..3a4b44e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_loc_on.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_sce_hdr.png b/res/drawable-xhdpi/ic_indicator_sce_hdr.png
new file mode 100644
index 0000000..318c8fa
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_sce_hdr.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_sce_off.png b/res/drawable-xhdpi/ic_indicator_sce_off.png
new file mode 100644
index 0000000..429d6ec
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_sce_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_sce_on.png b/res/drawable-xhdpi/ic_indicator_sce_on.png
new file mode 100644
index 0000000..8b2440c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_sce_on.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_timer_off.png b/res/drawable-xhdpi/ic_indicator_timer_off.png
new file mode 100644
index 0000000..0bbb9ee
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_timer_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_timer_on.png b/res/drawable-xhdpi/ic_indicator_timer_on.png
new file mode 100644
index 0000000..07cef15
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_timer_on.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_wb_cloudy.png b/res/drawable-xhdpi/ic_indicator_wb_cloudy.png
new file mode 100644
index 0000000..862ad5f
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_wb_cloudy.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_wb_daylight.png b/res/drawable-xhdpi/ic_indicator_wb_daylight.png
new file mode 100644
index 0000000..5c2a4dd
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_wb_daylight.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_wb_fluorescent.png b/res/drawable-xhdpi/ic_indicator_wb_fluorescent.png
new file mode 100644
index 0000000..226b36c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_wb_fluorescent.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_wb_off.png b/res/drawable-xhdpi/ic_indicator_wb_off.png
new file mode 100644
index 0000000..c5e38b1
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_wb_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_indicator_wb_tungsten.png b/res/drawable-xhdpi/ic_indicator_wb_tungsten.png
new file mode 100644
index 0000000..a0b4485
--- /dev/null
+++ b/res/drawable-xhdpi/ic_indicator_wb_tungsten.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_location.png b/res/drawable-xhdpi/ic_location.png
new file mode 100644
index 0000000..ab553dc
--- /dev/null
+++ b/res/drawable-xhdpi/ic_location.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_location_off.png b/res/drawable-xhdpi/ic_location_off.png
new file mode 100644
index 0000000..24594d1
--- /dev/null
+++ b/res/drawable-xhdpi/ic_location_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_cancel_holo_light.png b/res/drawable-xhdpi/ic_menu_cancel_holo_light.png
new file mode 100644
index 0000000..a9e1eb0
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_cancel_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_done_holo_light.png b/res/drawable-xhdpi/ic_menu_done_holo_light.png
new file mode 100644
index 0000000..7351f21
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_done_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_edit_holo_dark.png b/res/drawable-xhdpi/ic_menu_edit_holo_dark.png
new file mode 100644
index 0000000..65e72c1
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_edit_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_make_offline.png b/res/drawable-xhdpi/ic_menu_make_offline.png
new file mode 100644
index 0000000..808e2d1
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_make_offline.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_revert_holo_dark.png b/res/drawable-xhdpi/ic_menu_revert_holo_dark.png
new file mode 100644
index 0000000..48ff5bc
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_revert_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_savephoto.png b/res/drawable-xhdpi/ic_menu_savephoto.png
new file mode 100644
index 0000000..4b1210a
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_savephoto.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_savephoto_disabled.png b/res/drawable-xhdpi/ic_menu_savephoto_disabled.png
new file mode 100755
index 0000000..fab1c55
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_savephoto_disabled.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_tiny_planet.png b/res/drawable-xhdpi/ic_menu_tiny_planet.png
new file mode 100644
index 0000000..565c7d2
--- /dev/null
+++ b/res/drawable-xhdpi/ic_menu_tiny_planet.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_border_fast.9.png b/res/drawable-xhdpi/ic_pan_border_fast.9.png
new file mode 100644
index 0000000..1006961
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_border_fast.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_border_fast_xlarge.9.png b/res/drawable-xhdpi/ic_pan_border_fast_xlarge.9.png
new file mode 100644
index 0000000..9eeaa47
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_border_fast_xlarge.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_left_indicator.png b/res/drawable-xhdpi/ic_pan_left_indicator.png
new file mode 100644
index 0000000..cab6c90
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_left_indicator.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_left_indicator_fast.png b/res/drawable-xhdpi/ic_pan_left_indicator_fast.png
new file mode 100644
index 0000000..d983a49
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_left_indicator_fast.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_left_indicator_fast_xlarge.png b/res/drawable-xhdpi/ic_pan_left_indicator_fast_xlarge.png
new file mode 100644
index 0000000..b87cf6c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_left_indicator_fast_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_left_indicator_xlarge.png b/res/drawable-xhdpi/ic_pan_left_indicator_xlarge.png
new file mode 100644
index 0000000..0bc4d06
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_left_indicator_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_progression.png b/res/drawable-xhdpi/ic_pan_progression.png
new file mode 100644
index 0000000..756edb7
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_progression.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_progression_xlarge.png b/res/drawable-xhdpi/ic_pan_progression_xlarge.png
new file mode 100644
index 0000000..22d8a4e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_progression_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_right_indicator.png b/res/drawable-xhdpi/ic_pan_right_indicator.png
new file mode 100644
index 0000000..7ffe6ac
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_right_indicator.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_right_indicator_fast.png b/res/drawable-xhdpi/ic_pan_right_indicator_fast.png
new file mode 100644
index 0000000..9c7dc3f
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_right_indicator_fast.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_right_indicator_fast_xlarge.png b/res/drawable-xhdpi/ic_pan_right_indicator_fast_xlarge.png
new file mode 100644
index 0000000..b8fc167
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_right_indicator_fast_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pan_right_indicator_xlarge.png b/res/drawable-xhdpi/ic_pan_right_indicator_xlarge.png
new file mode 100644
index 0000000..640e47d
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pan_right_indicator_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_photoeditor_border.png b/res/drawable-xhdpi/ic_photoeditor_border.png
new file mode 100644
index 0000000..df459d2
--- /dev/null
+++ b/res/drawable-xhdpi/ic_photoeditor_border.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_photoeditor_color.png b/res/drawable-xhdpi/ic_photoeditor_color.png
new file mode 100644
index 0000000..626d91b
--- /dev/null
+++ b/res/drawable-xhdpi/ic_photoeditor_color.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_photoeditor_effects.png b/res/drawable-xhdpi/ic_photoeditor_effects.png
new file mode 100644
index 0000000..0896f80
--- /dev/null
+++ b/res/drawable-xhdpi/ic_photoeditor_effects.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_photoeditor_fix.png b/res/drawable-xhdpi/ic_photoeditor_fix.png
new file mode 100644
index 0000000..9116375
--- /dev/null
+++ b/res/drawable-xhdpi/ic_photoeditor_fix.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_recording_indicator.png b/res/drawable-xhdpi/ic_recording_indicator.png
new file mode 100644
index 0000000..fb3fc69
--- /dev/null
+++ b/res/drawable-xhdpi/ic_recording_indicator.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_sce.png b/res/drawable-xhdpi/ic_sce.png
new file mode 100644
index 0000000..44d0d7a
--- /dev/null
+++ b/res/drawable-xhdpi/ic_sce.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_sce_action.png b/res/drawable-xhdpi/ic_sce_action.png
new file mode 100644
index 0000000..d79a7e0
--- /dev/null
+++ b/res/drawable-xhdpi/ic_sce_action.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_sce_night.png b/res/drawable-xhdpi/ic_sce_night.png
new file mode 100644
index 0000000..5dd62fc
--- /dev/null
+++ b/res/drawable-xhdpi/ic_sce_night.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_sce_off.png b/res/drawable-xhdpi/ic_sce_off.png
new file mode 100644
index 0000000..0279dcc
--- /dev/null
+++ b/res/drawable-xhdpi/ic_sce_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_sce_party.png b/res/drawable-xhdpi/ic_sce_party.png
new file mode 100644
index 0000000..638d2d3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_sce_party.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_sce_sunset.png b/res/drawable-xhdpi/ic_sce_sunset.png
new file mode 100644
index 0000000..721966e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_sce_sunset.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_scn_holo_light.png b/res/drawable-xhdpi/ic_scn_holo_light.png
new file mode 100644
index 0000000..503f11f
--- /dev/null
+++ b/res/drawable-xhdpi/ic_scn_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_scn_holo_light_xlarge.png b/res/drawable-xhdpi/ic_scn_holo_light_xlarge.png
new file mode 100644
index 0000000..4369205
--- /dev/null
+++ b/res/drawable-xhdpi/ic_scn_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_settings_holo_light.png b/res/drawable-xhdpi/ic_settings_holo_light.png
new file mode 100644
index 0000000..0da5b5e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_settings_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_snapshot_border.9.png b/res/drawable-xhdpi/ic_snapshot_border.9.png
new file mode 100644
index 0000000..aae096b
--- /dev/null
+++ b/res/drawable-xhdpi/ic_snapshot_border.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_snapshot_border_xlarge.9.png b/res/drawable-xhdpi/ic_snapshot_border_xlarge.9.png
new file mode 100644
index 0000000..b4e1332
--- /dev/null
+++ b/res/drawable-xhdpi/ic_snapshot_border_xlarge.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_back.png b/res/drawable-xhdpi/ic_switch_back.png
new file mode 100644
index 0000000..26f5e09
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_back.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_camera.png b/res/drawable-xhdpi/ic_switch_camera.png
new file mode 100644
index 0000000..7d24062
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_camera.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_front.png b/res/drawable-xhdpi/ic_switch_front.png
new file mode 100644
index 0000000..8c0383a
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_front.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_photo_facing_holo_light.png b/res/drawable-xhdpi/ic_switch_photo_facing_holo_light.png
new file mode 100644
index 0000000..b44ad4c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_photo_facing_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_photo_facing_holo_light_xlarge.png b/res/drawable-xhdpi/ic_switch_photo_facing_holo_light_xlarge.png
new file mode 100644
index 0000000..cf8edf8
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_photo_facing_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_photosphere.png b/res/drawable-xhdpi/ic_switch_photosphere.png
new file mode 100644
index 0000000..8ff60a3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_photosphere.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_refocus.png b/res/drawable-xhdpi/ic_switch_refocus.png
new file mode 100644
index 0000000..3175f35
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_refocus.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_video.png b/res/drawable-xhdpi/ic_switch_video.png
new file mode 100644
index 0000000..ed5f31d
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_video.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_video_facing_holo_light.png b/res/drawable-xhdpi/ic_switch_video_facing_holo_light.png
new file mode 100644
index 0000000..8d4d495
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_video_facing_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_video_facing_holo_light_xlarge.png b/res/drawable-xhdpi/ic_switch_video_facing_holo_light_xlarge.png
new file mode 100644
index 0000000..d48e403
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_video_facing_holo_light_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switcher_menu_indicator.png b/res/drawable-xhdpi/ic_switcher_menu_indicator.png
new file mode 100644
index 0000000..36252ce
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switcher_menu_indicator.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_timelapse_none.png b/res/drawable-xhdpi/ic_timelapse_none.png
new file mode 100644
index 0000000..265f59b
--- /dev/null
+++ b/res/drawable-xhdpi/ic_timelapse_none.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_timelapse_none_xlarge.png b/res/drawable-xhdpi/ic_timelapse_none_xlarge.png
new file mode 100644
index 0000000..ace6b36
--- /dev/null
+++ b/res/drawable-xhdpi/ic_timelapse_none_xlarge.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_timer.png b/res/drawable-xhdpi/ic_timer.png
new file mode 100644
index 0000000..1764fdd
--- /dev/null
+++ b/res/drawable-xhdpi/ic_timer.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_vidcontrol_pause.png b/res/drawable-xhdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..4d274c0
--- /dev/null
+++ b/res/drawable-xhdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_vidcontrol_play.png b/res/drawable-xhdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..6f97a64
--- /dev/null
+++ b/res/drawable-xhdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_vidcontrol_reload.png b/res/drawable-xhdpi/ic_vidcontrol_reload.png
new file mode 100644
index 0000000..2aaf491
--- /dev/null
+++ b/res/drawable-xhdpi/ic_vidcontrol_reload.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_background_fields_of_wheat_holo.png b/res/drawable-xhdpi/ic_video_effects_background_fields_of_wheat_holo.png
new file mode 100644
index 0000000..cfca0b2
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_background_fields_of_wheat_holo.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_background_intergalactic_holo.png b/res/drawable-xhdpi/ic_video_effects_background_intergalactic_holo.png
new file mode 100644
index 0000000..7426510
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_background_intergalactic_holo.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_background_normal_holo_dark.png b/res/drawable-xhdpi/ic_video_effects_background_normal_holo_dark.png
new file mode 100644
index 0000000..93ca230
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_background_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_faces_big_eyes_holo_dark.png b/res/drawable-xhdpi/ic_video_effects_faces_big_eyes_holo_dark.png
new file mode 100644
index 0000000..43a8ffb
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_faces_big_eyes_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_faces_big_mouth_holo_dark.png b/res/drawable-xhdpi/ic_video_effects_faces_big_mouth_holo_dark.png
new file mode 100644
index 0000000..05be947
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_faces_big_mouth_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_faces_big_nose_holo_dark.png b/res/drawable-xhdpi/ic_video_effects_faces_big_nose_holo_dark.png
new file mode 100644
index 0000000..2eb8a34
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_faces_big_nose_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_faces_small_eyes_holo_dark.png b/res/drawable-xhdpi/ic_video_effects_faces_small_eyes_holo_dark.png
new file mode 100644
index 0000000..21a5939
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_faces_small_eyes_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_faces_small_mouth_holo_dark.png b/res/drawable-xhdpi/ic_video_effects_faces_small_mouth_holo_dark.png
new file mode 100644
index 0000000..434812f
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_faces_small_mouth_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_effects_faces_squeeze_holo_dark.png b/res/drawable-xhdpi/ic_video_effects_faces_squeeze_holo_dark.png
new file mode 100644
index 0000000..0717522
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_effects_faces_squeeze_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_video_thumb.png b/res/drawable-xhdpi/ic_video_thumb.png
new file mode 100644
index 0000000..e0f53b3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_video_thumb.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_view_photosphere.png b/res/drawable-xhdpi/ic_view_photosphere.png
new file mode 100644
index 0000000..23a87be
--- /dev/null
+++ b/res/drawable-xhdpi/ic_view_photosphere.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_wb_auto.png b/res/drawable-xhdpi/ic_wb_auto.png
new file mode 100644
index 0000000..cbacd04
--- /dev/null
+++ b/res/drawable-xhdpi/ic_wb_auto.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_wb_cloudy.png b/res/drawable-xhdpi/ic_wb_cloudy.png
new file mode 100644
index 0000000..cf8ec8c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_wb_cloudy.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_wb_fluorescent.png b/res/drawable-xhdpi/ic_wb_fluorescent.png
new file mode 100644
index 0000000..6e03a85
--- /dev/null
+++ b/res/drawable-xhdpi/ic_wb_fluorescent.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_wb_incandescent.png b/res/drawable-xhdpi/ic_wb_incandescent.png
new file mode 100644
index 0000000..367ded3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_wb_incandescent.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_wb_sunlight.png b/res/drawable-xhdpi/ic_wb_sunlight.png
new file mode 100644
index 0000000..497c4bd
--- /dev/null
+++ b/res/drawable-xhdpi/ic_wb_sunlight.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_divider.9.png b/res/drawable-xhdpi/list_divider.9.png
new file mode 100644
index 0000000..e62f011
--- /dev/null
+++ b/res/drawable-xhdpi/list_divider.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_divider_holo_dark.9.png b/res/drawable-xhdpi/list_divider_holo_dark.9.png
new file mode 100644
index 0000000..e62f011
--- /dev/null
+++ b/res/drawable-xhdpi/list_divider_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_pressed_holo_light.9.png b/res/drawable-xhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 0000000..e4b3393
--- /dev/null
+++ b/res/drawable-xhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_selector_background_selected.9.png b/res/drawable-xhdpi/list_selector_background_selected.9.png
new file mode 100644
index 0000000..78358fe
--- /dev/null
+++ b/res/drawable-xhdpi/list_selector_background_selected.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/menu_dropdown_panel_holo_dark.9.png b/res/drawable-xhdpi/menu_dropdown_panel_holo_dark.9.png
new file mode 100644
index 0000000..e2aff72
--- /dev/null
+++ b/res/drawable-xhdpi/menu_dropdown_panel_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/overscroll_edge.png b/res/drawable-xhdpi/overscroll_edge.png
new file mode 100644
index 0000000..4fe6c27
--- /dev/null
+++ b/res/drawable-xhdpi/overscroll_edge.png
Binary files differ
diff --git a/res/drawable-xhdpi/overscroll_glow.png b/res/drawable-xhdpi/overscroll_glow.png
new file mode 100644
index 0000000..75c3eb4
--- /dev/null
+++ b/res/drawable-xhdpi/overscroll_glow.png
Binary files differ
diff --git a/res/drawable-xhdpi/panel_undo_holo.9.png b/res/drawable-xhdpi/panel_undo_holo.9.png
new file mode 100644
index 0000000..1dc4927
--- /dev/null
+++ b/res/drawable-xhdpi/panel_undo_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/placeholder_camera.png b/res/drawable-xhdpi/placeholder_camera.png
new file mode 100644
index 0000000..e282a60
--- /dev/null
+++ b/res/drawable-xhdpi/placeholder_camera.png
Binary files differ
diff --git a/res/drawable-xhdpi/placeholder_empty.png b/res/drawable-xhdpi/placeholder_empty.png
new file mode 100644
index 0000000..83e8e81
--- /dev/null
+++ b/res/drawable-xhdpi/placeholder_empty.png
Binary files differ
diff --git a/res/drawable-xhdpi/placeholder_locked.png b/res/drawable-xhdpi/placeholder_locked.png
new file mode 100644
index 0000000..c39d9e3
--- /dev/null
+++ b/res/drawable-xhdpi/placeholder_locked.png
Binary files differ
diff --git a/res/drawable-xhdpi/preview.png b/res/drawable-xhdpi/preview.png
new file mode 100644
index 0000000..928c5f9
--- /dev/null
+++ b/res/drawable-xhdpi/preview.png
Binary files differ
diff --git a/res/drawable-xhdpi/scrubber_knob.png b/res/drawable-xhdpi/scrubber_knob.png
new file mode 100644
index 0000000..4e415a2
--- /dev/null
+++ b/res/drawable-xhdpi/scrubber_knob.png
Binary files differ
diff --git a/res/drawable-xhdpi/spinner_76_inner_holo.png b/res/drawable-xhdpi/spinner_76_inner_holo.png
new file mode 100644
index 0000000..0c88299
--- /dev/null
+++ b/res/drawable-xhdpi/spinner_76_inner_holo.png
Binary files differ
diff --git a/res/drawable-xhdpi/spinner_76_outer_holo.png b/res/drawable-xhdpi/spinner_76_outer_holo.png
new file mode 100644
index 0000000..04a8ab0
--- /dev/null
+++ b/res/drawable-xhdpi/spinner_76_outer_holo.png
Binary files differ
diff --git a/res/drawable-xhdpi/switch_bg_focused_holo_dark.9.png b/res/drawable-xhdpi/switch_bg_focused_holo_dark.9.png
new file mode 100644
index 0000000..e85103d
--- /dev/null
+++ b/res/drawable-xhdpi/switch_bg_focused_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/switch_bg_holo_dark.9.png b/res/drawable-xhdpi/switch_bg_holo_dark.9.png
new file mode 100644
index 0000000..732481e
--- /dev/null
+++ b/res/drawable-xhdpi/switch_bg_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/switch_thumb_activated_holo_dark.9.png b/res/drawable-xhdpi/switch_thumb_activated_holo_dark.9.png
new file mode 100644
index 0000000..ca48bd8
--- /dev/null
+++ b/res/drawable-xhdpi/switch_thumb_activated_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/switch_thumb_disabled_holo_dark.9.png b/res/drawable-xhdpi/switch_thumb_disabled_holo_dark.9.png
new file mode 100644
index 0000000..c3d80f0
--- /dev/null
+++ b/res/drawable-xhdpi/switch_thumb_disabled_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/switch_thumb_holo_dark.9.png b/res/drawable-xhdpi/switch_thumb_holo_dark.9.png
new file mode 100644
index 0000000..df236df
--- /dev/null
+++ b/res/drawable-xhdpi/switch_thumb_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/switch_thumb_pressed_holo_dark.9.png b/res/drawable-xhdpi/switch_thumb_pressed_holo_dark.9.png
new file mode 100644
index 0000000..4acb32b
--- /dev/null
+++ b/res/drawable-xhdpi/switch_thumb_pressed_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/text_select_handle_left.png b/res/drawable-xhdpi/text_select_handle_left.png
new file mode 100644
index 0000000..98d10c9
--- /dev/null
+++ b/res/drawable-xhdpi/text_select_handle_left.png
Binary files differ
diff --git a/res/drawable-xhdpi/text_select_handle_right.png b/res/drawable-xhdpi/text_select_handle_right.png
new file mode 100644
index 0000000..b3a0c9f
--- /dev/null
+++ b/res/drawable-xhdpi/text_select_handle_right.png
Binary files differ
diff --git a/res/drawable-xhdpi/toast_frame_holo.9.png b/res/drawable-xhdpi/toast_frame_holo.9.png
new file mode 100644
index 0000000..f8f75db
--- /dev/null
+++ b/res/drawable-xhdpi/toast_frame_holo.9.png
Binary files differ
diff --git a/res/drawable/action_bar_two_line_background.xml b/res/drawable/action_bar_two_line_background.xml
new file mode 100644
index 0000000..a5a1855
--- /dev/null
+++ b/res/drawable/action_bar_two_line_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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_activated="true"
+        android:drawable="@drawable/list_selector_background_selected" />
+    <item android:drawable="@android:color/transparent" />
+</selector>
diff --git a/res/drawable/bg_pressed.xml b/res/drawable/bg_pressed.xml
new file mode 100644
index 0000000..979cc86
--- /dev/null
+++ b/res/drawable/bg_pressed.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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/list_pressed_holo_light" />
+    <item android:drawable="@android:color/transparent" />
+</selector>
diff --git a/res/drawable/bg_pressed_exit_fading.xml b/res/drawable/bg_pressed_exit_fading.xml
new file mode 100644
index 0000000..d317e8b
--- /dev/null
+++ b/res/drawable/bg_pressed_exit_fading.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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"
+        android:exitFadeDuration="@android:integer/config_mediumAnimTime">
+    <item android:state_pressed="true" android:drawable="@drawable/list_pressed_holo_light" />
+    <item android:drawable="@android:color/transparent" />
+</selector>
diff --git a/res/drawable/bg_text_on_preview.xml b/res/drawable/bg_text_on_preview.xml
new file mode 100644
index 0000000..cdc2b04
--- /dev/null
+++ b/res/drawable/bg_text_on_preview.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shape="rectangle">
+   <solid android:color="@color/on_viewfinder_label_background_color"/>
+   <corners android:radius="3dp"/>
+</shape>
diff --git a/res/drawable/border_photo_frame_widget.xml b/res/drawable/border_photo_frame_widget.xml
new file mode 100644
index 0000000..5d25de5
--- /dev/null
+++ b/res/drawable/border_photo_frame_widget.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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_focused="true" android:state_pressed="true" android:drawable="@drawable/border_photo_frame_widget_focused_holo" />
+    <item android:state_focused="true" android:drawable="@drawable/border_photo_frame_widget_focused_holo" />
+    <item android:state_pressed="true" android:drawable="@drawable/border_photo_frame_widget_pressed_holo" />
+    <item android:drawable="@drawable/border_photo_frame_widget_holo" />
+</selector>
diff --git a/res/drawable/btn_new_shutter.xml b/res/drawable/btn_new_shutter.xml
new file mode 100644
index 0000000..7a3eb81
--- /dev/null
+++ b/res/drawable/btn_new_shutter.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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/btn_shutter_pressed" />
+    <item android:drawable="@drawable/btn_shutter_default" />
+</selector>
+
diff --git a/res/drawable/btn_new_shutter_video.xml b/res/drawable/btn_new_shutter_video.xml
new file mode 100644
index 0000000..e87b456
--- /dev/null
+++ b/res/drawable/btn_new_shutter_video.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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/btn_shutter_video_pressed" />
+    <item android:drawable="@drawable/btn_shutter_video_default" />
+</selector>
+
diff --git a/res/drawable/btn_shutter_video_recording.xml b/res/drawable/btn_shutter_video_recording.xml
new file mode 100644
index 0000000..55967a1
--- /dev/null
+++ b/res/drawable/btn_shutter_video_recording.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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/btn_video_shutter_recording_pressed_holo" />
+    <item android:drawable="@drawable/btn_video_shutter_recording_holo" />
+</selector>
+
diff --git a/res/drawable/filtershow_addpoint.png b/res/drawable/filtershow_addpoint.png
new file mode 100644
index 0000000..5abfc74
--- /dev/null
+++ b/res/drawable/filtershow_addpoint.png
Binary files differ
diff --git a/res/drawable/filtershow_background.png b/res/drawable/filtershow_background.png
new file mode 100644
index 0000000..22e1641
--- /dev/null
+++ b/res/drawable/filtershow_background.png
Binary files differ
diff --git a/res/drawable/filtershow_border_4x5.9.png b/res/drawable/filtershow_border_4x5.9.png
new file mode 100644
index 0000000..4cddf15
--- /dev/null
+++ b/res/drawable/filtershow_border_4x5.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_black.9.png b/res/drawable/filtershow_border_black.9.png
new file mode 100755
index 0000000..24bb5e1
--- /dev/null
+++ b/res/drawable/filtershow_border_black.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_brush.9.png b/res/drawable/filtershow_border_brush.9.png
new file mode 100644
index 0000000..db51d24
--- /dev/null
+++ b/res/drawable/filtershow_border_brush.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_film.png b/res/drawable/filtershow_border_film.png
new file mode 100755
index 0000000..9fbd637
--- /dev/null
+++ b/res/drawable/filtershow_border_film.png
Binary files differ
diff --git a/res/drawable/filtershow_border_grunge.9.png b/res/drawable/filtershow_border_grunge.9.png
new file mode 100644
index 0000000..fa2d474
--- /dev/null
+++ b/res/drawable/filtershow_border_grunge.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_rounded_black.9.png b/res/drawable/filtershow_border_rounded_black.9.png
new file mode 100755
index 0000000..590a343
--- /dev/null
+++ b/res/drawable/filtershow_border_rounded_black.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_rounded_white.9.png b/res/drawable/filtershow_border_rounded_white.9.png
new file mode 100755
index 0000000..4ddc97a
--- /dev/null
+++ b/res/drawable/filtershow_border_rounded_white.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_sumi_e.9.png b/res/drawable/filtershow_border_sumi_e.9.png
new file mode 100644
index 0000000..45f1094
--- /dev/null
+++ b/res/drawable/filtershow_border_sumi_e.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_tape.9.png b/res/drawable/filtershow_border_tape.9.png
new file mode 100644
index 0000000..3837c5d
--- /dev/null
+++ b/res/drawable/filtershow_border_tape.9.png
Binary files differ
diff --git a/res/drawable/filtershow_border_white.9.png b/res/drawable/filtershow_border_white.9.png
new file mode 100755
index 0000000..d8c8826
--- /dev/null
+++ b/res/drawable/filtershow_border_white.9.png
Binary files differ
diff --git a/res/drawable/filtershow_button_background.xml b/res/drawable/filtershow_button_background.xml
new file mode 100644
index 0000000..ee8a92d
--- /dev/null
+++ b/res/drawable/filtershow_button_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@android:color/holo_blue_light" android:state_pressed="true"/>
+    <item android:drawable="@drawable/filtershow_button_selected_background" android:state_selected="true"/>
+    <item android:drawable="@android:color/transparent" android:state_selected="false"/>
+
+</selector>
\ No newline at end of file
diff --git a/res/drawable/filtershow_button_border.png b/res/drawable/filtershow_button_border.png
new file mode 100644
index 0000000..69195a9
--- /dev/null
+++ b/res/drawable/filtershow_button_border.png
Binary files differ
diff --git a/res/drawable/filtershow_button_colors.png b/res/drawable/filtershow_button_colors.png
new file mode 100644
index 0000000..566773d
--- /dev/null
+++ b/res/drawable/filtershow_button_colors.png
Binary files differ
diff --git a/res/drawable/filtershow_button_colors_contrast.png b/res/drawable/filtershow_button_colors_contrast.png
new file mode 100644
index 0000000..ccb2dc6
--- /dev/null
+++ b/res/drawable/filtershow_button_colors_contrast.png
Binary files differ
diff --git a/res/drawable/filtershow_button_colors_vignette.png b/res/drawable/filtershow_button_colors_vignette.png
new file mode 100644
index 0000000..ac3d53f
--- /dev/null
+++ b/res/drawable/filtershow_button_colors_vignette.png
Binary files differ
diff --git a/res/drawable/filtershow_button_current.png b/res/drawable/filtershow_button_current.png
new file mode 100644
index 0000000..8c4b379
--- /dev/null
+++ b/res/drawable/filtershow_button_current.png
Binary files differ
diff --git a/res/drawable/filtershow_button_fx.png b/res/drawable/filtershow_button_fx.png
new file mode 100644
index 0000000..c887fe4
--- /dev/null
+++ b/res/drawable/filtershow_button_fx.png
Binary files differ
diff --git a/res/drawable/filtershow_button_geometry.png b/res/drawable/filtershow_button_geometry.png
new file mode 100644
index 0000000..4b8f3b8
--- /dev/null
+++ b/res/drawable/filtershow_button_geometry.png
Binary files differ
diff --git a/res/drawable/filtershow_button_geometry_crop.png b/res/drawable/filtershow_button_geometry_crop.png
new file mode 100644
index 0000000..eb7da1b
--- /dev/null
+++ b/res/drawable/filtershow_button_geometry_crop.png
Binary files differ
diff --git a/res/drawable/filtershow_button_geometry_flip.png b/res/drawable/filtershow_button_geometry_flip.png
new file mode 100644
index 0000000..dd74813
--- /dev/null
+++ b/res/drawable/filtershow_button_geometry_flip.png
Binary files differ
diff --git a/res/drawable/filtershow_button_geometry_rotate.png b/res/drawable/filtershow_button_geometry_rotate.png
new file mode 100644
index 0000000..fa50ce2
--- /dev/null
+++ b/res/drawable/filtershow_button_geometry_rotate.png
Binary files differ
diff --git a/res/drawable/filtershow_button_geometry_straighten.png b/res/drawable/filtershow_button_geometry_straighten.png
new file mode 100644
index 0000000..309eb5a
--- /dev/null
+++ b/res/drawable/filtershow_button_geometry_straighten.png
Binary files differ
diff --git a/res/drawable/filtershow_button_operations.png b/res/drawable/filtershow_button_operations.png
new file mode 100644
index 0000000..79e9a44
--- /dev/null
+++ b/res/drawable/filtershow_button_operations.png
Binary files differ
diff --git a/res/drawable/filtershow_button_origin.png b/res/drawable/filtershow_button_origin.png
new file mode 100644
index 0000000..0cd0bc2
--- /dev/null
+++ b/res/drawable/filtershow_button_origin.png
Binary files differ
diff --git a/res/drawable/filtershow_button_redo.png b/res/drawable/filtershow_button_redo.png
new file mode 100644
index 0000000..9daa01c
--- /dev/null
+++ b/res/drawable/filtershow_button_redo.png
Binary files differ
diff --git a/res/drawable/filtershow_button_selected_background.9.png b/res/drawable/filtershow_button_selected_background.9.png
new file mode 100644
index 0000000..bb41245
--- /dev/null
+++ b/res/drawable/filtershow_button_selected_background.9.png
Binary files differ
diff --git a/res/drawable/filtershow_button_settings.png b/res/drawable/filtershow_button_settings.png
new file mode 100644
index 0000000..df3925a
--- /dev/null
+++ b/res/drawable/filtershow_button_settings.png
Binary files differ
diff --git a/res/drawable/filtershow_button_show_original.png b/res/drawable/filtershow_button_show_original.png
new file mode 100644
index 0000000..925954b
--- /dev/null
+++ b/res/drawable/filtershow_button_show_original.png
Binary files differ
diff --git a/res/drawable/filtershow_button_undo.png b/res/drawable/filtershow_button_undo.png
new file mode 100644
index 0000000..0a7e0d1
--- /dev/null
+++ b/res/drawable/filtershow_button_undo.png
Binary files differ
diff --git a/res/drawable/filtershow_color_picker_circle.xml b/res/drawable/filtershow_color_picker_circle.xml
new file mode 100644
index 0000000..4444e0f
--- /dev/null
+++ b/res/drawable/filtershow_color_picker_circle.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval" >
+    <size android:width="20dp" android:height="20dp"/>
+    <corners
+        android:radius="10dp" />
+    <solid android:color="@color/red"/>
+</shape>
+
diff --git a/res/drawable/filtershow_color_picker_roundrect.xml b/res/drawable/filtershow_color_picker_roundrect.xml
new file mode 100644
index 0000000..89add5e
--- /dev/null
+++ b/res/drawable/filtershow_color_picker_roundrect.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" >
+
+    <size android:width="20dp" android:height="20dp"/>
+    <corners android:radius="10dp" />
+    <solid android:color="@color/red"/>
+
+</shape>
+
diff --git a/res/drawable/filtershow_delpoint.png b/res/drawable/filtershow_delpoint.png
new file mode 100644
index 0000000..84f2e5b
--- /dev/null
+++ b/res/drawable/filtershow_delpoint.png
Binary files differ
diff --git a/res/drawable/filtershow_drawing.png b/res/drawable/filtershow_drawing.png
new file mode 100644
index 0000000..566773d
--- /dev/null
+++ b/res/drawable/filtershow_drawing.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0000_vintage.png b/res/drawable/filtershow_fx_0000_vintage.png
new file mode 100644
index 0000000..6783bb6
--- /dev/null
+++ b/res/drawable/filtershow_fx_0000_vintage.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0001_instant.png b/res/drawable/filtershow_fx_0001_instant.png
new file mode 100644
index 0000000..1652a4b
--- /dev/null
+++ b/res/drawable/filtershow_fx_0001_instant.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0002_bleach.png b/res/drawable/filtershow_fx_0002_bleach.png
new file mode 100644
index 0000000..afb8a82
--- /dev/null
+++ b/res/drawable/filtershow_fx_0002_bleach.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0003_blue_crush.png b/res/drawable/filtershow_fx_0003_blue_crush.png
new file mode 100644
index 0000000..2b238e3
--- /dev/null
+++ b/res/drawable/filtershow_fx_0003_blue_crush.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0004_bw_contrast.png b/res/drawable/filtershow_fx_0004_bw_contrast.png
new file mode 100644
index 0000000..40eb397
--- /dev/null
+++ b/res/drawable/filtershow_fx_0004_bw_contrast.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0005_punch.png b/res/drawable/filtershow_fx_0005_punch.png
new file mode 100644
index 0000000..e7e0803
--- /dev/null
+++ b/res/drawable/filtershow_fx_0005_punch.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0006_x_process.png b/res/drawable/filtershow_fx_0006_x_process.png
new file mode 100644
index 0000000..5de9bb4
--- /dev/null
+++ b/res/drawable/filtershow_fx_0006_x_process.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0007_washout.png b/res/drawable/filtershow_fx_0007_washout.png
new file mode 100644
index 0000000..20dfb5e
--- /dev/null
+++ b/res/drawable/filtershow_fx_0007_washout.png
Binary files differ
diff --git a/res/drawable/filtershow_fx_0008_washout_color.png b/res/drawable/filtershow_fx_0008_washout_color.png
new file mode 100644
index 0000000..bb6602b
--- /dev/null
+++ b/res/drawable/filtershow_fx_0008_washout_color.png
Binary files differ
diff --git a/res/drawable/filtershow_grad_button.xml b/res/drawable/filtershow_grad_button.xml
new file mode 100644
index 0000000..4bf84c1
--- /dev/null
+++ b/res/drawable/filtershow_grad_button.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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_checked="false" android:drawable="@android:color/transparent"  />
+    <item android:state_checked="true" android:drawable="@android:color/holo_blue_light"  />
+
+</selector>
\ No newline at end of file
diff --git a/res/drawable/filtershow_menu_marker.png b/res/drawable/filtershow_menu_marker.png
new file mode 100644
index 0000000..1537a71
--- /dev/null
+++ b/res/drawable/filtershow_menu_marker.png
Binary files differ
diff --git a/res/drawable/filtershow_scrubber.xml b/res/drawable/filtershow_scrubber.xml
new file mode 100644
index 0000000..744d1da
--- /dev/null
+++ b/res/drawable/filtershow_scrubber.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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_enabled="false" android:drawable="@drawable/filtershow_scrubber_control_disabled" />
+    <item android:state_pressed="true" android:drawable="@drawable/filtershow_scrubber_control_pressed" />
+    <item android:state_selected="true" android:drawable="@drawable/filtershow_scrubber_control_focused" />
+    <item android:drawable="@drawable/filtershow_scrubber_control_normal" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable/filtershow_slider.xml b/res/drawable/filtershow_slider.xml
new file mode 100644
index 0000000..23457a6
--- /dev/null
+++ b/res/drawable/filtershow_slider.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/background"
+          android:drawable="@drawable/filtershow_scrubber_track" />
+    <item android:id="@android:id/secondaryProgress">
+        <scale android:scaleWidth="100%"
+               android:drawable="@drawable/filtershow_scrubber_secondary" />
+    </item>
+    <item android:id="@android:id/progress">
+        <scale android:scaleWidth="100%"
+               android:drawable="@drawable/filtershow_scrubber_primary" />
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/filtershow_state_button_background b/res/drawable/filtershow_state_button_background
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/res/drawable/filtershow_state_button_background
diff --git a/res/drawable/filtershow_tiled_background.xml b/res/drawable/filtershow_tiled_background.xml
new file mode 100644
index 0000000..055fbcd
--- /dev/null
+++ b/res/drawable/filtershow_tiled_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+<bitmap
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/filtershow_background"
+        android:tileMode="repeat"
+        android:dither="false" />
\ No newline at end of file
diff --git a/res/drawable/filtershow_vertical_bar.png b/res/drawable/filtershow_vertical_bar.png
new file mode 100644
index 0000000..5ac0a9f
--- /dev/null
+++ b/res/drawable/filtershow_vertical_bar.png
Binary files differ
diff --git a/res/drawable/filtershow_vertical_line.xml b/res/drawable/filtershow_vertical_line.xml
new file mode 100644
index 0000000..611c7e0
--- /dev/null
+++ b/res/drawable/filtershow_vertical_line.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" >
+    <size android:width="1dp" android:height="20dp"/>
+    <corners android:radius="0dp" />
+    <solid android:color="@color/toolbar_separation_line"/>
+</shape>
\ No newline at end of file
diff --git a/res/drawable/icn_media_pause.xml b/res/drawable/icn_media_pause.xml
new file mode 100644
index 0000000..cb5014f
--- /dev/null
+++ b/res/drawable/icn_media_pause.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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/icn_media_pause_pressed_holo_dark" />
+    <item android:state_focused="true"
+          android:drawable="@drawable/icn_media_pause_focused_holo_dark" />
+    <item android:drawable="@drawable/icn_media_pause_normal_holo_dark" />
+</selector>
diff --git a/res/drawable/icn_media_play.xml b/res/drawable/icn_media_play.xml
new file mode 100644
index 0000000..a21e082
--- /dev/null
+++ b/res/drawable/icn_media_play.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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/icn_media_play_pressed_holo_dark" />
+    <item android:state_focused="true"
+          android:drawable="@drawable/icn_media_play_focused_holo_dark" />
+    <item android:drawable="@drawable/icn_media_play_normal_holo_dark" />
+</selector>
diff --git a/res/drawable/ingest_item_list_selector.xml b/res/drawable/ingest_item_list_selector.xml
new file mode 100644
index 0000000..1a4541f
--- /dev/null
+++ b/res/drawable/ingest_item_list_selector.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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:drawable="@color/ingest_highlight_semitransparent"
+        android:state_checked="true" />
+    <item android:drawable="@color/ingest_highlight_semitransparent"
+        android:state_selected="true" />
+    <item android:drawable="@color/ingest_highlight_semitransparent"
+        android:state_pressed="true" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable/menu_save_photo.xml b/res/drawable/menu_save_photo.xml
new file mode 100644
index 0000000..0b92ac9
--- /dev/null
+++ b/res/drawable/menu_save_photo.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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_enabled="true" android:drawable="@drawable/ic_menu_savephoto" />
+    <item android:state_enabled="false" android:drawable="@drawable/ic_menu_savephoto_disabled" />
+</selector>
diff --git a/res/drawable/photoeditor_effect_redeye.png b/res/drawable/photoeditor_effect_redeye.png
new file mode 100644
index 0000000..ba845b5
--- /dev/null
+++ b/res/drawable/photoeditor_effect_redeye.png
Binary files differ
diff --git a/res/drawable/photopage_bottom_button_background.xml b/res/drawable/photopage_bottom_button_background.xml
new file mode 100644
index 0000000..0c772ad
--- /dev/null
+++ b/res/drawable/photopage_bottom_button_background.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/holo_blue_light" android:state_pressed="true"/>
+    <item android:drawable="@color/button_dark_transparent_background" android:state_selected="false"/>
+</selector>
diff --git a/res/drawable/setting_picker.xml b/res/drawable/setting_picker.xml
new file mode 100644
index 0000000..c3bff41
--- /dev/null
+++ b/res/drawable/setting_picker.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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_checked="true"
+              android:drawable="@drawable/list_pressed_holo_light" />
+    <item android:drawable="@android:color/transparent" />
+</selector>
diff --git a/res/drawable/switch_inner_holo_dark.xml b/res/drawable/switch_inner_holo_dark.xml
new file mode 100644
index 0000000..c0b00bb
--- /dev/null
+++ b/res/drawable/switch_inner_holo_dark.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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_enabled="false" android:drawable="@drawable/switch_thumb_disabled_holo_dark" />
+    <item android:state_pressed="true"  android:drawable="@drawable/switch_thumb_pressed_holo_dark" />
+    <item android:state_checked="true"  android:drawable="@drawable/switch_thumb_activated_holo_dark" />
+    <item                               android:drawable="@drawable/switch_thumb_holo_dark" />
+</selector>
diff --git a/res/drawable/switch_track_holo_dark.xml b/res/drawable/switch_track_holo_dark.xml
new file mode 100644
index 0000000..3712a61
--- /dev/null
+++ b/res/drawable/switch_track_holo_dark.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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_focused="true"  android:drawable="@drawable/switch_bg_focused_holo_dark" />
+    <item                               android:drawable="@drawable/switch_bg_holo_dark" />
+</selector>
diff --git a/res/drawable/transparent_button_background.xml b/res/drawable/transparent_button_background.xml
new file mode 100644
index 0000000..f1cc153
--- /dev/null
+++ b/res/drawable/transparent_button_background.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/holo_blue_light" android:state_pressed="true"/>
+    <item android:drawable="@android:color/holo_blue_light" android:state_pressed="true"/>
+    <item android:drawable="@android:color/transparent" android:state_selected="false"/>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/white_text_bg_gradient.xml b/res/drawable/white_text_bg_gradient.xml
new file mode 100644
index 0000000..c355ce5
--- /dev/null
+++ b/res/drawable/white_text_bg_gradient.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#DD000000"
+        android:endColor="#00FFFFFF"
+        android:angle="90"
+     />
+</shape>
\ No newline at end of file
diff --git a/res/interpolator/decelerate_cubic.xml b/res/interpolator/decelerate_cubic.xml
new file mode 100644
index 0000000..0bdd01d
--- /dev/null
+++ b/res/interpolator/decelerate_cubic.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012, 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.
+-->
+<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+        android:factor="1.5" />
diff --git a/res/interpolator/decelerate_quint.xml b/res/interpolator/decelerate_quint.xml
new file mode 100644
index 0000000..1939141
--- /dev/null
+++ b/res/interpolator/decelerate_quint.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012, 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.
+-->
+<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+        android:factor="2.5" />
diff --git a/res/layout-land/camera_controls.xml b/res/layout-land/camera_controls.xml
new file mode 100644
index 0000000..432ae9e
--- /dev/null
+++ b/res/layout-land/camera_controls.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<com.android.camera.ui.CameraControls
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/camera_controls"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+        <View
+            android:id="@+id/blocker"
+            android:clickable="true"
+            android:layout_height="match_parent"
+            android:layout_width="@dimen/switcher_size"
+            android:layout_gravity="right" />
+
+        <include layout="@layout/menu_indicators"
+            android:layout_width="64dip"
+            android:layout_height="64dip"
+            android:layout_marginTop="-5dip"
+            android:layout_marginRight="6dip"
+            android:layout_gravity="top|right"/>
+
+        <com.android.camera.ui.PieMenuButton
+            android:id="@+id/menu"
+            style="@style/SwitcherButton"
+            android:contentDescription="@string/accessibility_menu_button"
+            android:layout_gravity="right|top"
+            android:layout_marginRight="2dip" />
+
+        <com.android.camera.ui.CameraSwitcher
+            android:id="@+id/camera_switcher"
+            style="@style/SwitcherButton"
+            android:layout_gravity="right|bottom"
+            android:layout_marginRight="2dip"
+            android:contentDescription="@string/accessibility_mode_picker" />
+
+        <com.android.camera.ShutterButton
+            android:id="@+id/shutter_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="right|center_vertical"
+            android:layout_marginRight="@dimen/shutter_offset"
+            android:clickable="true"
+            android:contentDescription="@string/accessibility_shutter_button"
+            android:focusable="true"
+            android:scaleType="center"
+            android:src="@drawable/btn_new_shutter" />
+
+        <View
+            android:id="@+id/preview_thumb"
+            android:visibility="invisible"
+            android:layout_width="@dimen/capture_size"
+            android:layout_height="@dimen/capture_size"
+            android:layout_gravity="top|right" />
+
+</com.android.camera.ui.CameraControls>
diff --git a/res/layout-land/filtershow_activity.xml b/res/layout-land/filtershow_activity.xml
new file mode 100644
index 0000000..4d098e6
--- /dev/null
+++ b/res/layout-land/filtershow_activity.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:id="@+id/mainView"
+             android:background="@drawable/filtershow_tiled_background">
+
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="horizontal"
+            android:animateLayoutChanges="true">
+
+        <LinearLayout
+                android:layout_weight="1"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:orientation="horizontal">
+
+            <FrameLayout
+                    android:id="@+id/editorContainer"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"/>
+
+            <com.android.gallery3d.filtershow.imageshow.ImageShow
+                    android:id="@+id/imageShow"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1" />
+
+        </LinearLayout>
+
+        <LinearLayout
+                android:id="@+id/mainPanel"
+                android:layout_width="350dip"
+                android:layout_height="match_parent"
+                android:orientation="vertical"
+                android:animateLayoutChanges="true" >
+
+            <FrameLayout android:id="@+id/main_panel_container"
+                         android:layout_width="350dip"
+                         android:layout_height="0dip"
+                         android:layout_weight="1" />
+
+            <FrameLayout
+                    android:layout_gravity="bottom"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:visibility="gone">
+
+
+                <ProgressBar
+                        android:id="@+id/loading"
+                        style="@android:style/Widget.Holo.ProgressBar.Large"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="center"
+                        android:indeterminate="true"
+                        android:indeterminateOnly="true"
+                        android:background="@color/background_screen"/>
+
+            </FrameLayout>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/res/layout-land/filtershow_category_panel_new.xml b/res/layout-land/filtershow_category_panel_new.xml
new file mode 100644
index 0000000..10a6c97
--- /dev/null
+++ b/res/layout-land/filtershow_category_panel_new.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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="horizontal"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <ListView
+            android:id="@+id/listItems"
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_margin="8dip"
+            android:divider="@android:color/transparent"
+            android:dividerHeight="8dip" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-land/filtershow_editor_panel.xml b/res/layout-land/filtershow_editor_panel.xml
new file mode 100644
index 0000000..015fa26
--- /dev/null
+++ b/res/layout-land/filtershow_editor_panel.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:id="@+id/top"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical"
+              android:visibility="visible">
+
+    <Button
+            android:id="@+id/toggle_state"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/imageState"
+            android:background="@color/background_main_toolbar"
+            />
+
+    <FrameLayout android:id="@+id/state_panel_container"
+                 android:layout_width="match_parent"
+                 android:layout_height="0dip"
+                 android:visibility="visible"
+                 android:layout_gravity="top"
+                 android:layout_weight="1" />
+
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:layout_gravity="bottom">
+
+        <LinearLayout
+                android:id="@+id/controlArea"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:layout_alignParentBottom="true"
+                android:visibility="visible">
+
+            <SeekBar
+                    android:id="@+id/primarySeekBar"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    style="@style/FilterShowSlider"/>
+
+        </LinearLayout>
+
+        <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="56dip"
+                android:background="@color/background_main_toolbar"
+                android:orientation="horizontal"
+                android:baselineAligned="false"
+                android:visibility="visible">
+
+            <ImageButton
+                    android:id="@+id/cancelFilter"
+                    android:layout_width="wrap_content"
+                    android:layout_height="fill_parent"
+                    android:layout_gravity="left|center_vertical"
+                    android:background="@android:color/transparent"
+                    android:layout_weight=".1"
+                    android:gravity="center"
+                    android:src="@drawable/ic_menu_cancel_holo_light"
+                    android:textSize="18dip"/>
+
+            <ImageView
+                    android:layout_width="2dp"
+                    android:layout_height="fill_parent"
+                    android:src="@drawable/filtershow_vertical_bar"/>
+
+            <LinearLayout
+                    android:id="@+id/panelAccessoryViewList"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:orientation="horizontal"
+                    android:visibility="visible">
+
+                <com.android.gallery3d.filtershow.editors.SwapButton
+                        android:id="@+id/applyEffect"
+                        android:layout_width="fill_parent"
+                        android:layout_height="fill_parent"
+                        android:layout_gravity="center"
+                        android:background="@android:color/transparent"
+                        android:gravity="center"
+                        android:text="@string/apply_effect"
+                        android:textSize="18dip"
+                        android:drawableRight="@drawable/filtershow_menu_marker"
+                        android:textAllCaps="true" />
+
+            </LinearLayout>
+
+            <ImageView
+                    android:layout_width="2dp"
+                    android:layout_height="fill_parent"
+                    android:src="@drawable/filtershow_vertical_bar"/>
+
+            <ImageButton
+                    android:id="@+id/applyFilter"
+                    android:layout_width="wrap_content"
+                    android:layout_height="fill_parent"
+                    android:layout_gravity="right|center_vertical"
+                    android:layout_weight=".1"
+                    android:background="@android:color/transparent"
+                    android:gravity="center"
+                    android:src="@drawable/ic_menu_done_holo_light"
+                    android:textSize="18dip"/>
+        </LinearLayout>
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout-land/filtershow_main_panel.xml b/res/layout-land/filtershow_main_panel.xml
new file mode 100644
index 0000000..705eb69
--- /dev/null
+++ b/res/layout-land/filtershow_main_panel.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:baselineAligned="false"
+              android:orientation="vertical"
+              android:animateLayoutChanges="true"
+              android:visibility="visible" >
+
+    <FrameLayout android:id="@+id/state_panel_container"
+                 android:layout_width="match_parent"
+                 android:layout_height="0dip"
+                 android:visibility="visible"
+                 android:layout_gravity="top"
+                 android:layout_weight="1" />
+
+    <FrameLayout android:id="@+id/category_panel_container"
+                 android:layout_width="match_parent"
+                 android:layout_height="0dip"
+                 android:layout_weight="1"/>
+
+    <View
+            android:background="@color/toolbar_separation_line"
+            android:layout_height="1dip"
+            android:layout_width="match_parent"/>
+
+    <com.android.gallery3d.filtershow.CenteredLinearLayout
+            xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+            android:layout_width="match_parent"
+            android:layout_height="48dip"
+            android:layout_gravity="center|bottom"
+            custom:max_width="400dip"
+            android:orientation="vertical">
+
+        <LinearLayout android:layout_width="wrap_content"
+                      android:layout_height="match_parent"
+                      android:background="@color/background_main_toolbar">
+
+            <ImageButton
+                    android:id="@+id/fxButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_effects"/>
+
+            <ImageButton
+                    android:id="@+id/borderButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:padding="2dip"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_border"/>
+
+            <ImageButton
+                    android:id="@+id/geometryButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:padding="2dip"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_fix"/>
+
+            <ImageButton
+                    android:id="@+id/colorsButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:padding="2dip"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_color"/>
+
+        </LinearLayout>
+
+    </com.android.gallery3d.filtershow.CenteredLinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-land/filtershow_state_panel_new.xml b/res/layout-land/filtershow_state_panel_new.xml
new file mode 100644
index 0000000..c83cd88
--- /dev/null
+++ b/res/layout-land/filtershow_state_panel_new.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="0dip"
+            android:layout_weight="1"
+            android:scrollbars="none">
+
+        <com.android.gallery3d.filtershow.state.StatePanelTrack
+                android:id="@+id/listStates"
+                android:orientation="vertical"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                custom:elemSize="72dip"
+                custom:elemEndSize="32dip"
+                android:layout_margin="8dip"
+                android:animateLayoutChanges="true" />
+
+    </ScrollView>
+
+    <View
+            android:background="@color/state_panel_separation_line"
+            android:layout_height="6dip"
+            android:layout_width="match_parent"
+            android:paddingTop="8dip"/>
+
+</LinearLayout>
diff --git a/res/layout-land/keyguard_widget.xml b/res/layout-land/keyguard_widget.xml
new file mode 100644
index 0000000..f0f4362
--- /dev/null
+++ b/res/layout-land/keyguard_widget.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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/camera_controls"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/default_background" >
+
+    <ImageView
+        android:id="@+id/shutter_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="@dimen/shutter_offset"
+        android:contentDescription="@string/accessibility_shutter_button"
+        android:scaleType="center"
+        android:src="@drawable/btn_new_shutter" />
+
+    <include
+        android:layout_width="64dip"
+        android:layout_height="64dip"
+        android:layout_above="@id/shutter_button"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="6dip"
+        android:layout_marginTop="-5dip"
+        layout="@layout/menu_indicators_keyguard" />
+
+    <ImageView
+        android:id="@+id/camera_switcher"
+        style="@style/SwitcherButton"
+        android:layout_below="@id/shutter_button"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="2dip"
+        android:contentDescription="@string/accessibility_mode_picker"
+        android:scaleType="center"
+        android:src="@drawable/ic_switch_camera" />
+
+    <ImageView
+        android:id="@+id/camera_switcher_ind"
+        style="@style/SwitcherButton"
+        android:layout_below="@id/shutter_button"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="2dip"
+        android:contentDescription="@string/accessibility_mode_picker"
+        android:scaleType="center"
+        android:src="@drawable/ic_switcher_menu_indicator" />
+
+</RelativeLayout>
diff --git a/res/layout-land/on_screen_hint.xml b/res/layout-land/on_screen_hint.xml
new file mode 100644
index 0000000..59b0d1a
--- /dev/null
+++ b/res/layout-land/on_screen_hint.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012, 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:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="vertical"
+        android:background="@drawable/on_screen_hint_frame">
+    <TextView
+            android:id="@+id/message"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textAppearance="@style/OnScreenHintTextAppearance.Small"
+            android:textColor="#ffffffff"
+            android:shadowColor="#BB000000"
+            android:shadowRadius="2.75" />
+</LinearLayout>
diff --git a/res/layout-land/review_module_control.xml b/res/layout-land/review_module_control.xml
new file mode 100644
index 0000000..9f8b0cd
--- /dev/null
+++ b/res/layout-land/review_module_control.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+<com.android.camera.ui.RotatableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/CameraControls"
+        android:layout_gravity="right|center_vertical"
+        android:layout_marginRight="2dip">
+    <ImageView android:id="@+id/btn_done"
+            style="@style/ReviewControlIcon"
+            android:contentDescription="@string/accessibility_review_ok"
+            android:visibility="gone"
+            android:scaleType="center"
+            android:layout_gravity="top|right"
+            android:background="@drawable/bg_pressed"
+            android:src="@drawable/ic_menu_done_holo_light" />
+
+    <ImageView android:id="@+id/btn_retake"
+        style="@style/ReviewControlIcon"
+        android:contentDescription="@string/accessibility_review_retake"
+        android:layout_gravity="right|center_vertical"
+        android:scaleType="center"
+        android:focusable="true"
+        android:visibility="gone"
+        android:background="@drawable/bg_pressed"
+        android:src="@drawable/ic_btn_shutter_retake" />
+
+    <ImageView android:id="@+id/btn_cancel"
+            style="@style/ReviewControlIcon"
+            android:contentDescription="@string/accessibility_review_cancel"
+            android:visibility="gone"
+            android:scaleType="center"
+            android:layout_gravity="bottom|right"
+            android:background="@drawable/bg_pressed"
+            android:src="@drawable/ic_menu_cancel_holo_light" />
+</com.android.camera.ui.RotatableLayout>
diff --git a/res/layout-land/switcher_popup.xml b/res/layout-land/switcher_popup.xml
new file mode 100644
index 0000000..fc2d7bc
--- /dev/null
+++ b/res/layout-land/switcher_popup.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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:id="@+id/content"
+    android:orientation="horizontal"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom|right"
+    android:layout_marginRight="8dip"
+    android:layout_marginBottom="8dip"
+    android:paddingLeft="8dip"
+    android:paddingRight="8dip"
+    android:paddingTop="16dip"
+    android:paddingBottom="16dip"
+    android:background="#80000000" />
diff --git a/res/layout-port/camera_controls.xml b/res/layout-port/camera_controls.xml
new file mode 100644
index 0000000..7dd66e4
--- /dev/null
+++ b/res/layout-port/camera_controls.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<com.android.camera.ui.CameraControls
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/camera_controls"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+        <View
+            android:id="@+id/blocker"
+            android:clickable="true"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/switcher_size"
+            android:layout_gravity="bottom" />
+
+        <include layout="@layout/menu_indicators"
+            android:layout_width="64dip"
+            android:layout_height="64dip"
+            android:layout_gravity="bottom|right"
+            android:layout_marginBottom="6dip"
+            android:layout_marginRight="-5dip" />
+
+        <com.android.camera.ui.PieMenuButton
+            android:id="@+id/menu"
+            style="@style/SwitcherButton"
+            android:layout_gravity="bottom|right"
+            android:layout_marginBottom="2dip"
+            android:contentDescription="@string/accessibility_menu_button" />
+
+        <com.android.camera.ui.CameraSwitcher
+           android:id="@+id/camera_switcher"
+           style="@style/SwitcherButton"
+           android:layout_gravity="bottom|left"
+           android:layout_marginBottom="2dip"
+           android:contentDescription="@string/accessibility_mode_picker" />
+
+        <com.android.camera.ShutterButton
+            android:id="@+id/shutter_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom|center_horizontal"
+            android:layout_marginBottom="@dimen/shutter_offset"
+            android:clickable="true"
+            android:contentDescription="@string/accessibility_shutter_button"
+            android:focusable="true"
+            android:scaleType="center"
+            android:src="@drawable/btn_new_shutter" />
+
+        <View
+            android:id="@+id/preview_thumb"
+            android:visibility="invisible"
+            android:layout_width="@dimen/capture_size"
+            android:layout_height="@dimen/capture_size"
+            android:layout_gravity="top|right" />
+
+</com.android.camera.ui.CameraControls>
\ No newline at end of file
diff --git a/res/layout-port/keyguard_widget.xml b/res/layout-port/keyguard_widget.xml
new file mode 100644
index 0000000..28b59c4
--- /dev/null
+++ b/res/layout-port/keyguard_widget.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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/camera_controls"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/default_background" >
+
+    <ImageView
+        android:id="@+id/shutter"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="@dimen/shutter_offset"
+        android:src="@drawable/btn_new_shutter" />
+
+    <include layout="@layout/menu_indicators_keyguard"
+        android:layout_width="64dip"
+        android:layout_height="64dip"
+        android:layout_toRightOf="@id/shutter"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="6dip"
+        android:layout_marginRight="-5dip" />
+
+    <ImageView
+        android:id="@+id/camera_switcher"
+        style="@style/SwitcherButton"
+        android:layout_toLeftOf="@id/shutter"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="2dip"
+        android:scaleType="center"
+        android:contentDescription="@string/accessibility_mode_picker"
+        android:src="@drawable/ic_switch_camera" />
+
+    <ImageView
+        android:id="@+id/camera_switcher_ind"
+        style="@style/SwitcherButton"
+        android:layout_toLeftOf="@id/shutter"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="2dip"
+        android:scaleType="center"
+        android:contentDescription="@string/accessibility_mode_picker"
+        android:src="@drawable/ic_switcher_menu_indicator" />
+
+</RelativeLayout>
diff --git a/res/layout-port/on_screen_hint.xml b/res/layout-port/on_screen_hint.xml
new file mode 100644
index 0000000..467b67f
--- /dev/null
+++ b/res/layout-port/on_screen_hint.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2009, The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="horizontal"
+        android:background="@drawable/on_screen_hint_frame">
+    <TextView
+            android:id="@+id/message"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textAppearance="@style/OnScreenHintTextAppearance.Small"
+            android:textColor="#ffffffff"
+            android:shadowColor="#BB000000"
+            android:shadowRadius="2.75" />
+</LinearLayout>
+
+
diff --git a/res/layout-port/review_module_control.xml b/res/layout-port/review_module_control.xml
new file mode 100644
index 0000000..3c4280e
--- /dev/null
+++ b/res/layout-port/review_module_control.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+<com.android.camera.ui.RotatableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/CameraControls"
+        android:layout_gravity="bottom|center_horizontal"
+        android:layout_marginBottom="2dip">
+    <ImageView android:id="@+id/btn_done"
+            style="@style/ReviewControlIcon"
+            android:contentDescription="@string/accessibility_review_ok"
+            android:visibility="gone"
+            android:scaleType="center"
+            android:layout_gravity="right|bottom"
+            android:background="@drawable/bg_pressed"
+            android:src="@drawable/ic_menu_done_holo_light" />
+
+    <ImageView android:id="@+id/btn_retake"
+        style="@style/ReviewControlIcon"
+        android:contentDescription="@string/accessibility_review_retake"
+        android:layout_gravity="bottom|center_horizontal"
+        android:scaleType="center"
+        android:focusable="true"
+        android:visibility="gone"
+        android:background="@drawable/bg_pressed"
+        android:src="@drawable/ic_btn_shutter_retake" />
+
+    <ImageView android:id="@+id/btn_cancel"
+            style="@style/ReviewControlIcon"
+            android:contentDescription="@string/accessibility_review_cancel"
+            android:visibility="gone"
+            android:scaleType="center"
+            android:layout_gravity="left|bottom"
+            android:background="@drawable/bg_pressed"
+            android:src="@drawable/ic_menu_cancel_holo_light" />
+</com.android.camera.ui.RotatableLayout>
diff --git a/res/layout-port/switcher_popup.xml b/res/layout-port/switcher_popup.xml
new file mode 100644
index 0000000..8fe09a3
--- /dev/null
+++ b/res/layout-port/switcher_popup.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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:id="@+id/content"
+    android:orientation="vertical"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom|left"
+    android:layout_marginLeft="8dip"
+    android:layout_marginBottom="8dip"
+    android:paddingLeft="16dip"
+    android:paddingRight="16dip"
+    android:paddingTop="8dip"
+    android:paddingBottom="8dip"
+    android:background="#80000000" />
diff --git a/res/layout/action_bar_text.xml b/res/layout/action_bar_text.xml
new file mode 100644
index 0000000..a332647
--- /dev/null
+++ b/res/layout/action_bar_text.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/text1"
+    android:background="?android:attr/activatedBackgroundIndicator"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:gravity="center_vertical"
+    android:paddingLeft="18dp"
+    android:paddingRight="18dp"
+    android:singleLine="true"
+    android:minHeight="?attr/listPreferredItemHeightSmall"
+/>
diff --git a/res/layout/action_bar_two_line_text.xml b/res/layout/action_bar_two_line_text.xml
new file mode 100644
index 0000000..92a4af9
--- /dev/null
+++ b/res/layout/action_bar_two_line_text.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/ActionBarTwoLineItem"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:gravity="center_vertical"
+        android:duplicateParentState="false"
+        android:layout_alignParentLeft="true"
+        android:layout_width="wrap_content" >
+        <TextView
+            android:id="@android:id/text1"
+            style="@style/ActionBarTwoLinePrimary"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:includeFontPadding="false"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+        <TextView
+            android:id="@android:id/text2"
+            style="@style/ActionBarTwoLineSecondary"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:includeFontPadding="false"
+            android:layout_marginRight="4dp"
+            android:layout_below="@android:id/text1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+</TwoLineListItem>
diff --git a/res/layout/action_mode.xml b/res/layout/action_mode.xml
new file mode 100644
index 0000000..6c516e6
--- /dev/null
+++ b/res/layout/action_mode.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:id="@+id/navigation_bar"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+
+    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent">
+        <ImageView android:layout_gravity="right"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:src="@drawable/dropdown_ic_arrow_normal_holo_dark" />
+        <Button android:id="@+id/selection_menu"
+                style="?android:attr/actionButtonStyle"
+                android:divider="?android:attr/listDividerAlertDialog"
+                android:textAppearance="?android:attr/textAppearanceLargePopupMenu"
+                android:textColor="?android:attr/actionMenuTextColor"
+                android:singleLine="true"
+                android:gravity="left|center_vertical"
+                android:paddingRight="25dip"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent" />
+    </FrameLayout>
+    <ImageView android:layout_marginLeft="16dip"
+            android:layout_marginRight="8dip"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="bottom"
+            android:src="@drawable/cab_divider_vertical_dark" />
+</LinearLayout>
diff --git a/res/layout/album_content.xml b/res/layout/album_content.xml
new file mode 100644
index 0000000..97509fd
--- /dev/null
+++ b/res/layout/album_content.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <LinearLayout android:id="@+id/progressContainer"
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone"
+            android:gravity="center">
+
+        <ProgressBar style="?android:attr/progressBarStyleLarge"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        <TextView android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:text="@string/loading"
+                android:paddingTop="4dip"
+                android:singleLine="true" />
+
+    </LinearLayout>
+
+    <FrameLayout android:id="@+id/gridContainer"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+        <com.android.photos.views.HeaderGridView android:id="@android:id/list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:choiceMode="multipleChoiceModal"
+                android:numColumns="auto_fit"
+                android:stretchMode="columnWidth"
+                android:drawSelectorOnTop="true" />
+        <TextView android:id="@android:id/empty"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:textAppearance="?android:attr/textAppearanceMedium" />
+    </FrameLayout>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/album_header.xml b/res/layout/album_header.xml
new file mode 100644
index 0000000..76c9a45
--- /dev/null
+++ b/res/layout/album_header.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content" >
+
+    <ImageView
+        android:id="@+id/album_header_image"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentLeft="true"
+        android:paddingLeft="15dip"
+        android:paddingBottom="10dip"
+        android:paddingTop="20dip"
+        android:background="@drawable/white_text_bg_gradient"
+        android:layout_gravity="bottom"
+        android:orientation="vertical" >
+
+        <TextView
+            android:id="@+id/album_header_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/white"
+            android:textAppearance="?android:attr/textAppearanceLarge" />
+
+        <TextView
+            android:id="@+id/album_header_subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/white"
+            android:textAppearance="?android:attr/textAppearanceSmall" />
+
+    </LinearLayout>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/album_set_item.xml b/res/layout/album_set_item.xml
new file mode 100644
index 0000000..ad0e0db
--- /dev/null
+++ b/res/layout/album_set_item.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:orientation="vertical"
+    android:background="?android:attr/activatedBackgroundIndicator"
+    android:padding="2dp" >
+
+    <LinearLayout
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:orientation="vertical"
+        android:padding="10dp"
+        android:background="#FFF" >
+
+        <TextView
+            android:id="@+id/album_set_item_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceMedium" />
+
+        <TextView
+            android:id="@+id/album_set_item_count"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="#AAA" />
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/album_set_item_image"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/album_set_item_image_height"
+        android:scaleType="centerCrop" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/appwidget_loading_item.xml b/res/layout/appwidget_loading_item.xml
new file mode 100644
index 0000000..ee8a206
--- /dev/null
+++ b/res/layout/appwidget_loading_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/appwidget_photo_border">
+    <RelativeLayout
+            android:layout_width="@dimen/stack_photo_width"
+            android:layout_height="@dimen/stack_photo_height"
+            android:background="@android:color/darker_gray">
+        <ProgressBar
+                android:id="@+id/appwidget_loading_item"
+                android:layout_centerInParent="true"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content" />
+    </RelativeLayout>
+</FrameLayout>
diff --git a/res/layout/appwidget_main.xml b/res/layout/appwidget_main.xml
new file mode 100644
index 0000000..0accabb
--- /dev/null
+++ b/res/layout/appwidget_main.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <RelativeLayout
+            android:id="@+id/appwidget_empty_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone">
+        <FrameLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:background="@drawable/appwidget_photo_border">
+            <TextView
+                    android:id="@+id/appwidget_photo_item"
+                    android:layout_width="@dimen/stack_photo_width"
+                    android:layout_height="@dimen/stack_photo_height"
+                    android:gravity="center"
+                    android:text="@string/appwidget_empty_text"/>
+        </FrameLayout>
+    </RelativeLayout>
+    <StackView
+            android:id="@+id/appwidget_stack_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:loopViews="true" />
+</FrameLayout>
diff --git a/res/layout/appwidget_photo_item.xml b/res/layout/appwidget_photo_item.xml
new file mode 100644
index 0000000..a56a6d7
--- /dev/null
+++ b/res/layout/appwidget_photo_item.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:background="@drawable/appwidget_photo_border">
+    <ImageView
+            android:id="@+id/appwidget_photo_item"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:scaleType="fitCenter"
+            android:adjustViewBounds="true" />
+</FrameLayout>
diff --git a/res/layout/bg_replacement_training_message.xml b/res/layout/bg_replacement_training_message.xml
new file mode 100644
index 0000000..8d881d6
--- /dev/null
+++ b/res/layout/bg_replacement_training_message.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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/bg_replace_message_frame"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:visibility="gone"
+        android:onClick="onProtectiveCurtainClick"
+        android:background="#77000000">
+    <com.android.camera.ui.RotateLayout
+            android:id="@+id/bg_replace_message"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:layout_centerInParent="true">
+        <LinearLayout
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:orientation="vertical"
+                android:background="@drawable/dialog_full_holo_dark">
+            <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textAppearance="?android:attr/textAppearanceMedium"
+                    android:text="@string/bg_replacement_message"
+                    android:padding="32dp" />
+
+            <View
+                    android:layout_width="match_parent"
+                    android:layout_height="1dp"
+                    android:background="#aaaaaa" />
+
+            <Button android:layout_width="match_parent"
+                    android:layout_height="48dip"
+                    android:layout_gravity="center"
+                    android:textAppearance="?android:attr/textAppearanceMedium"
+                    style="?android:attr/borderlessButtonStyle"
+                    android:text="@android:string/cancel"
+                    android:onClick="onCancelBgTraining"
+                    android:contentDescription="@android:string/cancel" />
+        </LinearLayout>
+    </com.android.camera.ui.RotateLayout>
+</RelativeLayout>
diff --git a/res/layout/camera.xml b/res/layout/camera.xml
new file mode 100644
index 0000000..9a3a01a
--- /dev/null
+++ b/res/layout/camera.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<com.android.camera.ui.CameraRootView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/camera_app_root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+</com.android.camera.ui.CameraRootView>
diff --git a/res/layout/camera_filmstrip.xml b/res/layout/camera_filmstrip.xml
new file mode 100644
index 0000000..935f38a
--- /dev/null
+++ b/res/layout/camera_filmstrip.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <com.android.camera.ui.FilmStripView
+        android:id="@+id/filmstrip_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <ImageButton
+        android:id="@+id/filmstrip_bottom_control_panorama"
+        android:layout_width="70dp"
+        android:layout_height="70dp"
+        android:layout_gravity="bottom|center_horizontal"
+        android:background="@drawable/transparent_button_background"
+        android:clickable="true"
+        android:paddingBottom="5dp"
+        android:paddingLeft="5dp"
+        android:paddingRight="5dp"
+        android:paddingTop="5dp"
+        android:visibility="gone"
+        android:src="@drawable/ic_view_photosphere" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/choose_widget_type.xml b/res/layout/choose_widget_type.xml
new file mode 100644
index 0000000..5f1739a
--- /dev/null
+++ b/res/layout/choose_widget_type.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/widget_type"
+        android:paddingLeft="32dp"
+        android:paddingRight="32dp"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+    <RadioButton android:id="@+id/widget_type_album"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:minHeight="48dp"
+            android:text="@string/widget_type_album"/>
+    <RadioButton android:id="@+id/widget_type_photo"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:minHeight="48dp"
+            android:text="@string/widget_type_photo"/>
+    <RadioButton android:id="@+id/widget_type_shuffle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:minHeight="48dp"
+            android:text="@string/widget_type_shuffle"/>
+    <View android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_weight="0"
+            android:layout_marginLeft="16dp"
+            android:layout_marginRight="16dp"
+            android:background="?android:attr/dividerHorizontal" />
+    <Button style="?android:attr/buttonBarButtonStyle"
+            android:id="@+id/cancel"
+            android:layout_weight="0"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@android:string/cancel" />
+</RadioGroup>
diff --git a/res/layout/count_down_to_capture.xml b/res/layout/count_down_to_capture.xml
new file mode 100644
index 0000000..68276ad
--- /dev/null
+++ b/res/layout/count_down_to_capture.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<com.android.camera.ui.CountDownView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/count_down_to_capture"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:visibility="invisible" >
+    <TextView android:id="@+id/remaining_seconds"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:textSize="160sp"
+        android:textColor="@android:color/white"
+        android:gravity="center" />
+    <TextView android:id="@+id/count_down_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingLeft="10dp"
+        android:paddingTop="20dp"
+        android:textSize="20sp"
+        android:textColor="@android:color/white"
+        android:text="@string/count_down_title_text" />
+</com.android.camera.ui.CountDownView>
\ No newline at end of file
diff --git a/res/layout/countdown_setting_popup.xml b/res/layout/countdown_setting_popup.xml
new file mode 100644
index 0000000..22acd92
--- /dev/null
+++ b/res/layout/countdown_setting_popup.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2013, 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.
+-->
+
+<com.android.camera.ui.CountdownTimerPopup xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SettingPopupWindow">
+
+    <LinearLayout android:orientation="vertical"
+            android:background="@color/popup_background"
+            android:layout_height="wrap_content"
+            android:layout_width="@dimen/big_setting_popup_window_width">
+
+    <TextView
+            android:id="@+id/title"
+            style="@style/PopupTitleText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:gravity="center_vertical|center_horizontal"
+            android:minHeight="@dimen/popup_title_frame_min_height" />
+
+    <View style="@style/PopupTitleSeparator" />
+
+    <LinearLayout
+            android:id="@+id/time_duration_picker"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:orientation="vertical" >
+
+            <TextView
+                android:id="@+id/set_time_interval_title"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:paddingTop="5dip"
+                android:text="@string/set_duration"
+                android:textAppearance="?android:attr/textAppearanceMedium" />
+            <!-- A number picker to set timer -->
+
+            <NumberPicker
+                android:id="@+id/duration"
+                android:layout_width="160dp"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginLeft="16dip"
+                android:layout_marginRight="16dip"
+                android:focusable="false" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical" >
+
+            <View
+                android:background="#40ffffff"
+                android:layout_width="match_parent"
+                android:layout_height="1dip" />
+            <LinearLayout
+                android:id="@+id/timer_sound"
+                style="@style/SettingRow" >
+
+                <TextView android:id="@+id/beep_title"
+                    style="@style/SettingItemTitle"
+                    android:text="@string/pref_camera_timer_sound_title" />
+
+                <CheckBox android:id="@+id/sound_check_box"
+                    android:layout_gravity="center_vertical|right"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent" />
+            </LinearLayout>
+
+            <View
+                android:background="#40ffffff"
+                android:layout_width="match_parent"
+                android:layout_height="1dip" />
+
+            <Button
+                android:id="@+id/timer_set_button"
+                style="?android:attr/buttonBarButtonStyle"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:text="@string/time_lapse_interval_set"
+                android:textAppearance="?android:attr/textAppearanceMedium" />
+        </LinearLayout>
+    </LinearLayout>
+</com.android.camera.ui.CountdownTimerPopup>
diff --git a/res/layout/crop_activity.xml b/res/layout/crop_activity.xml
new file mode 100644
index 0000000..9ff223f
--- /dev/null
+++ b/res/layout/crop_activity.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/mainView"
+    android:background="@drawable/filtershow_tiled_background">
+
+    <LinearLayout
+        android:id="@+id/mainPanel"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical" >
+
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" >
+
+            <com.android.gallery3d.filtershow.crop.CropView
+                android:id="@+id/cropView"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+            <ProgressBar
+                android:id="@+id/loading"
+                style="@android:style/Widget.Holo.ProgressBar.Large"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:indeterminate="true"
+                android:indeterminateOnly="true"
+                android:background="@android:color/transparent" />
+
+        </FrameLayout>
+
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/res/layout/cropimage.xml b/res/layout/cropimage.xml
new file mode 100644
index 0000000..c434fb6
--- /dev/null
+++ b/res/layout/cropimage.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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <include layout="@layout/gl_root_group"/>
+</FrameLayout>
diff --git a/res/layout/details.xml b/res/layout/details.xml
new file mode 100644
index 0000000..dfda0ee
--- /dev/null
+++ b/res/layout/details.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/text1"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:gravity="left"
+/>
diff --git a/res/layout/details_list.xml b/res/layout/details_list.xml
new file mode 100644
index 0000000..b80ab6c
--- /dev/null
+++ b/res/layout/details_list.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="16dp"
+    android:dividerHeight="8dp"
+/>
diff --git a/res/layout/dialog_picker.xml b/res/layout/dialog_picker.xml
new file mode 100644
index 0000000..ccc5121
--- /dev/null
+++ b/res/layout/dialog_picker.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <FrameLayout
+            android:layout_weight="1"
+            android:layout_width="match_parent"
+            android:layout_height="0dp">
+        <include layout="@layout/gl_root_group" />
+    </FrameLayout>
+    <ImageView android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="16dp"
+            android:layout_marginRight="16dp"
+            android:background="@drawable/list_divider_holo_dark" />
+    <Button style="?android:attr/buttonBarButtonStyle"
+            android:id="@+id/cancel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@android:string/cancel"
+            android:visibility="gone" />
+</LinearLayout>
diff --git a/res/layout/editor_grad_button.xml b/res/layout/editor_grad_button.xml
new file mode 100644
index 0000000..4d1b10b
--- /dev/null
+++ b/res/layout/editor_grad_button.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:layout_alignParentTop="true"
+    android:layout_marginLeft="26dp"
+    android:layout_marginTop="21dp"
+    android:orientation="horizontal" >
+
+  <com.android.gallery3d.filtershow.ui.FramedTextButton
+      android:id="@+id/editorGradButton"
+      android:layout_width="84dip"
+      android:layout_height="84dip"
+      android:layout_gravity="center_vertical|left"
+      android:background="@drawable/filtershow_button_background"
+      android:scaleType="centerInside"
+      android:visibility="visible"
+      android:text="@string/editor_grad_style" />
+
+  <ToggleButton
+      android:id="@+id/editor_grad_new"
+      android:layout_width="84dip"
+      android:layout_height="84dip"
+      android:layout_gravity="center_vertical|left"
+      android:background="@drawable/filtershow_grad_button"
+      android:scaleType="centerInside"
+      android:visibility="visible"
+      android:textOff="@string/editor_grad_new"
+      android:textOn="@string/editor_grad_new" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/effect_setting_item.xml b/res/layout/effect_setting_item.xml
new file mode 100644
index 0000000..655625c
--- /dev/null
+++ b/res/layout/effect_setting_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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"
+        xmlns:tools="http://schemas.android.com/tools"
+        tools:ignore="UseCompoundDrawables"
+        style="@style/EffectSettingItem">
+
+        <ImageView android:id="@+id/image"
+                android:layout_height="@dimen/effect_setting_item_icon_width"
+                android:layout_width="@dimen/effect_setting_item_icon_width"
+                android:layout_gravity="center_horizontal"
+                android:scaleType="fitCenter"
+                android:adjustViewBounds="true" />
+        <TextView android:id="@+id/text"
+                style="@style/EffectSettingItemTitle"/>
+</LinearLayout>
diff --git a/res/layout/effect_setting_popup.xml b/res/layout/effect_setting_popup.xml
new file mode 100644
index 0000000..63b7ab4
--- /dev/null
+++ b/res/layout/effect_setting_popup.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<com.android.camera.ui.EffectSettingPopup xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SettingPopupWindow">
+    <LinearLayout android:orientation="vertical"
+            android:background="@color/popup_background"
+            android:layout_height="wrap_content"
+            android:layout_width="@dimen/big_setting_popup_window_width">
+        <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="@dimen/popup_title_frame_min_height">
+            <TextView android:id="@+id/title"
+                    style="@style/PopupTitleText" />
+        </FrameLayout>
+        <View style="@style/PopupTitleSeparator" />
+        <ScrollView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+            <LinearLayout
+                    android:orientation="vertical"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+                <TextView android:id="@+id/clear_effects"
+                        android:text="@string/clear_effects"
+                        style="@style/EffectSettingTypeTitle"
+                        android:textSize="@dimen/effect_setting_clear_text_size"
+                        android:minHeight="@dimen/effect_setting_clear_text_min_height"
+                        android:background="@drawable/bg_pressed"/>
+                <TextView android:id="@+id/effect_silly_faces_title"
+                        android:text="@string/effect_silly_faces"
+                        android:visibility="gone"
+                        style="@style/EffectSettingTypeTitle"/>
+                <View android:id="@+id/effect_silly_faces_title_separator"
+                        android:visibility="gone"
+                        style="@style/EffectTypeSeparator"/>
+                <com.android.camera.ui.ExpandedGridView android:id="@+id/effect_silly_faces"
+                        style="@style/EffectSettingGrid"/>
+                <View android:id="@+id/effect_background_separator"
+                        android:visibility="gone"
+                        style="@style/EffectTitleSeparator"/>
+                <TextView android:id="@+id/effect_background_title"
+                        android:text="@string/effect_background"
+                        android:visibility="gone"
+                        style="@style/EffectSettingTypeTitle"/>
+                <View android:id="@+id/effect_background_title_separator"
+                        android:visibility="gone"
+                        style="@style/EffectTypeSeparator"/>
+                <com.android.camera.ui.ExpandedGridView android:id="@+id/effect_background"
+                        android:visibility="gone"
+                        style="@style/EffectSettingGrid"/>
+            </LinearLayout>
+        </ScrollView>
+    </LinearLayout>
+</com.android.camera.ui.EffectSettingPopup>
diff --git a/res/layout/face_view.xml b/res/layout/face_view.xml
new file mode 100644
index 0000000..63e7886
--- /dev/null
+++ b/res/layout/face_view.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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.
+-->
+<com.android.camera.ui.FaceView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"/>
diff --git a/res/layout/filtershow_actionbar.xml b/res/layout/filtershow_actionbar.xml
new file mode 100644
index 0000000..5f0aa3f
--- /dev/null
+++ b/res/layout/filtershow_actionbar.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012 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.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:background="@drawable/filtershow_button_background"
+    android:id="@+id/filtershow_done"
+    android:textAllCaps="true"
+    android:text="@string/save"
+    android:gravity="center_vertical"
+    android:textSize="14sp"
+    android:drawableLeft="@drawable/menu_save_photo"
+    android:drawablePadding="8dip" />
\ No newline at end of file
diff --git a/res/layout/filtershow_activity.xml b/res/layout/filtershow_activity.xml
new file mode 100644
index 0000000..f5684ff
--- /dev/null
+++ b/res/layout/filtershow_activity.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:id="@+id/mainView"
+             android:background="@drawable/filtershow_tiled_background">
+
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+        <LinearLayout
+                android:layout_weight="1"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal">
+
+            <FrameLayout
+                    android:id="@+id/editorContainer"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"/>
+
+            <com.android.gallery3d.filtershow.imageshow.ImageShow
+                    android:id="@+id/imageShow"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1" />
+
+        </LinearLayout>
+
+        <com.android.gallery3d.filtershow.CenteredLinearLayout
+                xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+                android:id="@+id/mainPanel"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center|bottom"
+                custom:max_width="650dip"
+                android:orientation="vertical" >
+
+            <FrameLayout android:id="@+id/main_panel_container"
+                         android:layout_gravity="center"
+                         android:layout_width="match_parent"
+                         android:layout_height="0dip"
+                         android:layout_weight="1" />
+
+            <FrameLayout
+                    android:layout_gravity="bottom"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:visibility="gone">
+
+
+                <ProgressBar
+                        android:id="@+id/loading"
+                        style="@android:style/Widget.Holo.ProgressBar.Large"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="center"
+                        android:indeterminate="true"
+                        android:indeterminateOnly="true"
+                        android:background="@color/background_screen"/>
+
+            </FrameLayout>
+
+        </com.android.gallery3d.filtershow.CenteredLinearLayout>
+
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/res/layout/filtershow_category_panel.xml b/res/layout/filtershow_category_panel.xml
new file mode 100644
index 0000000..c1b8bbe
--- /dev/null
+++ b/res/layout/filtershow_category_panel.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="match_parent"
+    android:layout_height="48dip"
+    android:background="@color/background_main_toolbar" >
+
+    <ImageButton
+        android:id="@+id/fxButton"
+        android:layout_width="@dimen/thumbnail_size"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:background="@drawable/filtershow_button_background"
+        android:scaleType="centerInside"
+        android:src="@drawable/ic_photoeditor_effects" />
+
+    <ImageButton
+        android:id="@+id/borderButton"
+        android:layout_width="@dimen/thumbnail_size"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:background="@drawable/filtershow_button_background"
+        android:padding="2dip"
+        android:scaleType="centerInside"
+        android:src="@drawable/ic_photoeditor_border" />
+
+    <ImageButton
+        android:id="@+id/geometryButton"
+        android:layout_width="@dimen/thumbnail_size"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:background="@drawable/filtershow_button_background"
+        android:padding="2dip"
+        android:scaleType="centerInside"
+        android:src="@drawable/ic_photoeditor_fix" />
+
+    <ImageButton
+        android:id="@+id/colorsButton"
+        android:layout_width="@dimen/thumbnail_size"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:background="@drawable/filtershow_button_background"
+        android:padding="2dip"
+        android:scaleType="centerInside"
+        android:src="@drawable/ic_photoeditor_color" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_category_panel_new.xml b/res/layout/filtershow_category_panel_new.xml
new file mode 100644
index 0000000..e98f29e
--- /dev/null
+++ b/res/layout/filtershow_category_panel_new.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+              xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+              android:orientation="horizontal"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content">
+
+    <HorizontalScrollView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:scrollbars="none"
+            android:background="@color/background_main_toolbar" >
+
+        <com.android.gallery3d.filtershow.category.CategoryTrack
+                android:id="@+id/listItems"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/category_panel_height"
+                custom:iconSize="@dimen/category_panel_icon_size"
+                android:divider="@android:color/transparent"
+                android:dividerPadding="8dip"
+                />
+
+    </HorizontalScrollView>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_color_gird.xml b/res/layout/filtershow_color_gird.xml
new file mode 100644
index 0000000..2dbbc5f
--- /dev/null
+++ b/res/layout/filtershow_color_gird.xml
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <TextView
+        android:id="@+id/textView1"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <TableLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" >
+
+        <TableRow
+            android:id="@+id/tableRow1"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+
+            <Button
+                android:id="@+id/cp_grid_button01"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button2"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button03"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button04"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button05"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+        </TableRow>
+
+        <TableRow
+            android:id="@+id/tableRow2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" >
+
+            <Button
+                android:id="@+id/Button06"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button07"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button08"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button09"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button10"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+        </TableRow>
+
+        <TableRow
+            android:id="@+id/tableRow3"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" >
+
+            <Button
+                android:id="@+id/Button11"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button12"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button13"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button14"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button15"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+        </TableRow>
+
+        <TableRow
+            android:id="@+id/tableRow4"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" >
+
+            <Button
+                android:id="@+id/Button16"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button17"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button18"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button19"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+
+            <Button
+                android:id="@+id/button20"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@drawable/filtershow_color_picker_circle" />
+        </TableRow>
+    </TableLayout>
+
+    <Button
+        android:id="@+id/filtershow_cp_custom"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/color_pick_select" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_color_picker.xml b/res/layout/filtershow_color_picker.xml
new file mode 100644
index 0000000..fc49729
--- /dev/null
+++ b/res/layout/filtershow_color_picker.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/RelativeLayout1"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/default_background"
+    tools:context=".ColorPickerActivity" >
+
+    <com.android.gallery3d.filtershow.colorpicker.ColorRectView
+        android:id="@+id/colorRectView"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:layout_marginLeft="10dp"
+        android:layout_marginTop="10dp"
+        android:layout_marginBottom="10dp"
+        android:layout_marginRight="1dp"
+        android:layout_above="@+id/colorOpacityView"
+        android:layout_toLeftOf="@+id/colorValueView" />
+
+    <com.android.gallery3d.filtershow.colorpicker.ColorValueView
+        android:id="@+id/colorValueView"
+               android:layout_width="90dp"
+        android:layout_height="fill_parent"
+        android:layout_alignParentRight = "true"
+        android:layout_above="@+id/colorOpacityView"  />
+
+    <com.android.gallery3d.filtershow.colorpicker.ColorOpacityView
+        android:id="@+id/colorOpacityView"
+       android:layout_width="match_parent"
+        android:layout_height="90dp"
+         android:layout_above="@+id/btnSelect" />
+
+    <Button
+        android:id="@+id/btnSelect"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/color_pick_select"
+        android:layout_alignParentBottom = "true"
+        />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_control_action_slider.xml b/res/layout/filtershow_control_action_slider.xml
new file mode 100644
index 0000000..a3ef3ed
--- /dev/null
+++ b/res/layout/filtershow_control_action_slider.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+    xmlns:app="http://schemas.android.com/apk/res/com.example.imagefilterharness"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal" >
+
+    <ImageButton
+        android:id="@+id/leftActionButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="left|center_vertical"
+        android:scaleType="centerInside"
+        android:layout_weight="0"
+        android:background="@drawable/filtershow_button_background"
+        android:src="@drawable/filtershow_addpoint"
+        android:paddingBottom="8dp"  />
+
+    <SeekBar
+        android:id="@+id/controlValueSeekBar"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_weight="1"
+        style="@style/FilterShowSlider" />
+
+    <ImageButton
+        android:id="@+id/rightActionButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="left|center_vertical"
+        android:scaleType="centerInside"
+        android:layout_weight="0"
+        android:background="@drawable/filtershow_button_background"
+        android:src="@drawable/ic_menu_trash_holo_light"
+        android:paddingBottom="8dp"  />
+
+</LinearLayout>
+
diff --git a/res/layout/filtershow_control_style_chooser.xml b/res/layout/filtershow_control_style_chooser.xml
new file mode 100644
index 0000000..a5bc984
--- /dev/null
+++ b/res/layout/filtershow_control_style_chooser.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+    xmlns:app="http://schemas.android.com/apk/res/com.example.imagefilterharness"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal" >
+            <HorizontalScrollView
+                android:id="@+id/scrollList"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:scrollbars="none" >
+
+                <LinearLayout
+                    android:id="@+id/listStyles"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:orientation="horizontal" >
+                </LinearLayout>
+            </HorizontalScrollView>
+</LinearLayout>
+
diff --git a/res/layout/filtershow_control_title_slider.xml b/res/layout/filtershow_control_title_slider.xml
new file mode 100644
index 0000000..584e015
--- /dev/null
+++ b/res/layout/filtershow_control_title_slider.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:columnCount="2"
+    android:orientation="horizontal" >
+
+    <TextView
+        android:id="@+id/controlName"
+        android:layout_gravity="left"
+        android:layout_marginLeft="8dip" />
+
+    <TextView
+        android:id="@+id/controlValue"
+        android:layout_gravity="right"
+        android:layout_marginRight="8dip"
+        android:textStyle="bold" />
+
+    <SeekBar
+        android:id="@+id/controlValueSeekBar"
+        android:layout_width="match_parent"
+        android:layout_column="0"
+        android:layout_columnSpan="2"
+        android:layout_gravity="fill_horizontal"
+        style="@style/FilterShowSlider" />
+</GridLayout>
+
diff --git a/res/layout/filtershow_cp_custom_title.xml b/res/layout/filtershow_cp_custom_title.xml
new file mode 100644
index 0000000..cef8b6c
--- /dev/null
+++ b/res/layout/filtershow_cp_custom_title.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/customTitle"
+    android:text="@string/color_pick_title"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:layout_marginLeft="20dp"
+    android:layout_marginRight="20dp" >
+</TextView>
\ No newline at end of file
diff --git a/res/layout/filtershow_crop_button.xml b/res/layout/filtershow_crop_button.xml
new file mode 100644
index 0000000..b42d6b6
--- /dev/null
+++ b/res/layout/filtershow_crop_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<com.android.gallery3d.filtershow.ui.FramedTextButton
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/cropUtilityButton"
+    android:layout_width="84dip"
+    android:layout_height="84dip"
+    android:layout_gravity="center_vertical|left"
+    android:background="@drawable/filtershow_button_background"
+    android:scaleType="centerInside"
+    android:visibility="gone"
+    android:text="@string/aspectNone_effect" />
\ No newline at end of file
diff --git a/res/layout/filtershow_curves_button.xml b/res/layout/filtershow_curves_button.xml
new file mode 100644
index 0000000..31e8aed
--- /dev/null
+++ b/res/layout/filtershow_curves_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<com.android.gallery3d.filtershow.ui.FramedTextButton
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/curvesUtilityButton"
+    android:layout_width="84dip"
+    android:layout_height="84dip"
+    android:layout_gravity="center_vertical|left"
+    android:background="@drawable/filtershow_button_background"
+    android:scaleType="centerInside"
+    android:visibility="gone"
+    android:text="@string/curves_channel_rgb" />
diff --git a/res/layout/filtershow_default_editor.xml b/res/layout/filtershow_default_editor.xml
new file mode 100644
index 0000000..b261ea3
--- /dev/null
+++ b/res/layout/filtershow_default_editor.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+    xmlns:iconbutton="http://schemas.android.com/apk/res/com.android.gallery3d"
+    android:id="@+id/basicEditor"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_weight="1" >
+
+    <com.android.gallery3d.filtershow.imageshow.ImageShow
+        android:id="@+id/imageShow"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+ </FrameLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_draw_button.xml b/res/layout/filtershow_draw_button.xml
new file mode 100644
index 0000000..dba8100
--- /dev/null
+++ b/res/layout/filtershow_draw_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<com.android.gallery3d.filtershow.ui.FramedTextButton
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/drawUtilityButton"
+    android:layout_width="84dip"
+    android:layout_height="84dip"
+    android:layout_gravity="center_vertical|left"
+    android:background="@drawable/filtershow_button_background"
+    android:scaleType="centerInside"
+    android:visibility="gone"
+    android:text="@string/draw_style" />
diff --git a/res/layout/filtershow_draw_size.xml b/res/layout/filtershow_draw_size.xml
new file mode 100644
index 0000000..068493e
--- /dev/null
+++ b/res/layout/filtershow_draw_size.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:dividerPadding="20dp"
+    android:gravity="center_horizontal"
+    android:orientation="vertical" >
+
+    <SeekBar
+        android:id="@+id/sizeSeekBar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <Button
+        android:id="@+id/sizeAcceptButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/draw_size_accept" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_editor_panel.xml b/res/layout/filtershow_editor_panel.xml
new file mode 100644
index 0000000..a6da46a
--- /dev/null
+++ b/res/layout/filtershow_editor_panel.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:id="@+id/top"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:visibility="visible" >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <LinearLayout
+                android:id="@+id/controlArea"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:layout_alignParentBottom="true"
+                android:visibility="visible">
+
+            <SeekBar
+                    android:id="@+id/primarySeekBar"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    style="@style/FilterShowSlider"/>
+
+        </LinearLayout>
+
+        <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="56dip"
+                android:background="@color/background_main_toolbar"
+                android:orientation="horizontal"
+                android:baselineAligned="false"
+                android:visibility="visible">
+
+            <ImageButton
+                    android:id="@+id/cancelFilter"
+                    android:layout_width="wrap_content"
+                    android:layout_height="fill_parent"
+                    android:layout_gravity="left|center_vertical"
+                    android:background="@android:color/transparent"
+                    android:layout_weight=".1"
+                    android:gravity="center"
+                    android:src="@drawable/ic_menu_cancel_holo_light"
+                    android:textSize="18dip"/>
+
+            <ImageView
+                    android:layout_width="2dp"
+                    android:layout_height="fill_parent"
+                    android:src="@drawable/filtershow_vertical_bar"/>
+
+            <LinearLayout
+                    android:id="@+id/panelAccessoryViewList"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:orientation="horizontal"
+                    android:visibility="visible">
+
+                <com.android.gallery3d.filtershow.editors.SwapButton
+                        android:id="@+id/applyEffect"
+                        android:layout_width="fill_parent"
+                        android:layout_height="fill_parent"
+                        android:layout_gravity="center"
+                        android:background="@android:color/transparent"
+                        android:gravity="center"
+                        android:text="@string/apply_effect"
+                        android:textSize="18dip"
+                        android:drawableRight="@drawable/filtershow_menu_marker"
+                        android:textAllCaps="true" />
+
+            </LinearLayout>
+
+            <ImageView
+                    android:layout_width="2dp"
+                    android:layout_height="fill_parent"
+                    android:src="@drawable/filtershow_vertical_bar"/>
+
+            <ImageButton
+                    android:id="@+id/applyFilter"
+                    android:layout_width="wrap_content"
+                    android:layout_height="fill_parent"
+                    android:layout_gravity="right|center_vertical"
+                    android:layout_weight=".1"
+                    android:background="@android:color/transparent"
+                    android:gravity="center"
+                    android:src="@drawable/ic_menu_done_holo_light"
+                    android:textSize="18dip"/>
+        </LinearLayout>
+
+        <FrameLayout android:id="@+id/state_panel_container"
+                     android:layout_width="match_parent"
+                     android:layout_height="wrap_content"
+                     android:visibility="visible" />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/filtershow_export_dialog.xml b/res/layout/filtershow_export_dialog.xml
new file mode 100644
index 0000000..2021075
--- /dev/null
+++ b/res/layout/filtershow_export_dialog.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:divider="?android:dividerVertical"
+              android:showDividers="middle">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal|center_vertical"
+        android:layout_margin="7dp"
+        android:text="@string/select_compression"/>
+
+    <LinearLayout
+            android:orientation="horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+        <SeekBar
+                android:id="@+id/qualitySeekBar"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="3"
+                android:layout_margin="7dp"
+                android:max="100"
+                android:progress="100"/>
+
+        <TextView
+                android:id="@+id/qualityTextView"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:layout_marginLeft="7dp"
+                android:layout_gravity="center_vertical|center_horizontal"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+            android:orientation="horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="48dp"
+            style="?android:attr/buttonBarStyle">
+
+        <Button
+                android:id="@+id/cancel"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="@string/cancel"
+                style="?android:attr/buttonBarButtonStyle" />
+
+        <Button
+                android:id="@+id/done"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="@string/done"
+                style="?android:attr/buttonBarButtonStyle"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/filtershow_grad_editor.xml b/res/layout/filtershow_grad_editor.xml
new file mode 100644
index 0000000..6c4721e
--- /dev/null
+++ b/res/layout/filtershow_grad_editor.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:id="@+id/gradEditor"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_weight="1" >
+
+    <com.android.gallery3d.filtershow.imageshow.ImageGrad
+        android:id="@+id/imageShow"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+ </FrameLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_history_operation_row.xml b/res/layout/filtershow_history_operation_row.xml
new file mode 100644
index 0000000..25a0d26
--- /dev/null
+++ b/res/layout/filtershow_history_operation_row.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="120dip"
+    android:gravity="center_horizontal"
+    android:orientation="horizontal"
+    android:padding="0dip"
+    android:background="@color/background_main_toolbar">
+
+    <ImageView
+            android:id="@+id/preview"
+            android:layout_width="180dip"
+            android:layout_height="120dip"
+            android:scaleType="centerCrop"
+            android:cropToPadding="true"
+            android:visibility="visible"
+            />
+
+    <TextView
+            xmlns:android="http://schemas.android.com/apk/res/android"
+            android:id="@+id/rowTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:gravity="bottom|right"
+            android:padding="10dip"
+            android:textSize="16dip"
+            android:textStyle="bold">
+    </TextView>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_history_panel.xml b/res/layout/filtershow_history_panel.xml
new file mode 100644
index 0000000..392e39c
--- /dev/null
+++ b/res/layout/filtershow_history_panel.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:id="@+id/historyPanel"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:layout_weight="1"
+        android:visibility="gone" >
+
+    <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@android:color/transparent"
+            android:gravity="center"
+            android:padding="2dip"
+            android:text="@string/history"
+            android:textColor="@android:color/white"
+            android:textSize="24sp"
+            android:textStyle="bold" />
+
+    <ListView
+            android:id="@+id/operationsList"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:padding="10dip"
+            android:divider="@android:color/transparent"
+            android:dividerHeight="10dip" />
+
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal" >
+
+        <Button
+                android:id="@+id/resetOperationsButton"
+                style="@style/FilterShowHistoryButton"
+                android:gravity="center"
+                android:text="@string/reset" />
+
+        <Button
+                android:id="@+id/saveOperationsButton"
+                style="@style/FilterShowHistoryButton"
+                android:text="@string/save"
+                android:visibility="gone" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/filtershow_main_panel.xml b/res/layout/filtershow_main_panel.xml
new file mode 100644
index 0000000..53691d3
--- /dev/null
+++ b/res/layout/filtershow_main_panel.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:baselineAligned="false"
+              android:orientation="vertical"
+              android:animateLayoutChanges="false"
+              android:visibility="visible"
+              android:background="@color/background_main_toolbar" >
+
+    <FrameLayout android:id="@+id/state_panel_container"
+                 android:layout_width="match_parent"
+                 android:layout_height="wrap_content"
+                 android:visibility="visible"
+                 android:layout_gravity="top"
+                 android:layout_weight="1" />
+
+    <FrameLayout android:id="@+id/category_panel_container"
+                 android:layout_width="wrap_content"
+                 android:visibility="visible"
+                 android:layout_height="0dip"
+                 android:layout_gravity="center"
+                 android:layout_weight="1"/>
+
+    <View
+            android:background="@color/toolbar_separation_line"
+            android:layout_height="1dip"
+            android:layout_width="match_parent"/>
+
+    <com.android.gallery3d.filtershow.CenteredLinearLayout
+            xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+            android:id="@+id/bottom_panel"
+            android:layout_width="match_parent"
+            android:layout_height="48dip"
+            android:layout_gravity="center|bottom"
+            custom:max_width="400dip"
+            android:orientation="vertical">
+
+        <LinearLayout android:layout_width="wrap_content"
+                      android:layout_height="match_parent"
+                      android:background="@color/background_main_toolbar">
+
+            <ImageButton
+                    android:id="@+id/fxButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_effects"/>
+
+            <ImageButton
+                    android:id="@+id/borderButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:padding="2dip"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_border"/>
+
+            <ImageButton
+                    android:id="@+id/geometryButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:padding="2dip"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_fix"/>
+
+            <ImageButton
+                    android:id="@+id/colorsButton"
+                    android:layout_width="@dimen/thumbnail_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/filtershow_button_background"
+                    android:padding="2dip"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_photoeditor_color"/>
+
+        </LinearLayout>
+
+    </com.android.gallery3d.filtershow.CenteredLinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_presets_management_dialog.xml b/res/layout/filtershow_presets_management_dialog.xml
new file mode 100644
index 0000000..f6c6fb7
--- /dev/null
+++ b/res/layout/filtershow_presets_management_dialog.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:divider="?android:dividerVertical"
+              android:showDividers="middle">
+
+    <ListView
+            android:id="@+id/listItems"
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="8dip"
+            android:divider="@android:color/transparent"
+            android:dividerHeight="8dip"/>
+
+    <LinearLayout
+            android:orientation="horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="48dp"
+            style="?android:attr/buttonBarStyle">
+
+        <Button
+                android:id="@+id/cancel"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="@string/cancel"
+                style="?android:attr/buttonBarButtonStyle" />
+
+        <Button
+                android:id="@+id/addpreset"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="@string/filtershow_save_preset"
+                style="?android:attr/buttonBarButtonStyle"/>
+
+        <Button
+                android:id="@+id/ok"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="@string/ok"
+                style="?android:attr/buttonBarButtonStyle"/>
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_presets_management_row.xml b/res/layout/filtershow_presets_management_row.xml
new file mode 100644
index 0000000..648e874
--- /dev/null
+++ b/res/layout/filtershow_presets_management_row.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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="horizontal"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <ImageView
+            android:id="@+id/imageView"
+            android:layout_weight=".1"
+            android:layout_width="80dip"
+            android:layout_height="80dip"
+            android:scaleType="fitCenter"
+            android:layout_gravity="left|center_vertical"/>
+
+    <EditText
+            android:id="@+id/editView"
+            android:gravity="center"
+            android:textSize="18sp"
+            android:layout_weight="1"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:focusable="true"
+            android:imeOptions="actionDone"
+            android:singleLine="true"/>
+
+    <ImageButton
+            android:id="@+id/deleteUserPreset"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_gravity="right|center_vertical"
+            android:background="@android:color/transparent"
+            android:layout_weight=".1"
+            android:gravity="center"
+            android:src="@drawable/ic_menu_trash_holo_light"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_seekbar.xml b/res/layout/filtershow_seekbar.xml
new file mode 100644
index 0000000..6463ca8
--- /dev/null
+++ b/res/layout/filtershow_seekbar.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:id="@+id/top"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:visibility="visible" >
+
+    <SeekBar
+        android:id="@+id/primarySeekBar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        style="@style/FilterShowSlider" />
+
+</LinearLayout>
diff --git a/res/layout/filtershow_state_panel.xml b/res/layout/filtershow_state_panel.xml
new file mode 100644
index 0000000..1f9f970
--- /dev/null
+++ b/res/layout/filtershow_state_panel.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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:id="@+id/imageStatePanel"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_weight="1"
+        android:visibility="visible" >
+
+    <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@android:color/transparent"
+            android:gravity="center"
+            android:padding="2dip"
+            android:text="@string/imageState"
+            android:textColor="@android:color/white"
+            android:textSize="24sp"
+            android:textStyle="bold" />
+
+    <ListView
+            android:id="@+id/imageStateList"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" >
+    </ListView>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_state_panel_new.xml b/res/layout/filtershow_state_panel_new.xml
new file mode 100644
index 0000000..d2d59ab
--- /dev/null
+++ b/res/layout/filtershow_state_panel_new.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:background="@color/background_main_toolbar">
+
+    <View
+            android:background="@color/toolbar_separation_line"
+            android:layout_height="1dip"
+            android:layout_width="match_parent"/>
+
+    <HorizontalScrollView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:scrollbars="none">
+
+        <com.android.gallery3d.filtershow.state.StatePanelTrack
+                android:id="@+id/listStates"
+                android:orientation="horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="48dip"
+                custom:elemEndSize="128dip"
+                custom:elemSize="128dip"
+                android:layout_margin="8dip"
+                android:animateLayoutChanges="true" />
+
+    </HorizontalScrollView>
+
+</LinearLayout>
diff --git a/res/layout/filtershow_tiny_planet_editor.xml b/res/layout/filtershow_tiny_planet_editor.xml
new file mode 100644
index 0000000..fd89f99
--- /dev/null
+++ b/res/layout/filtershow_tiny_planet_editor.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+    xmlns:iconbutton="http://schemas.android.com/apk/res/com.android.gallery3d"
+    android:id="@+id/tinyPlanetEditor"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_weight="1" >
+
+    <com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet
+        android:id="@+id/imageTinyPlanet"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+ </FrameLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_vignette_editor.xml b/res/layout/filtershow_vignette_editor.xml
new file mode 100644
index 0000000..9c9b4cb
--- /dev/null
+++ b/res/layout/filtershow_vignette_editor.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+    xmlns:iconbutton="http://schemas.android.com/apk/res/com.android.gallery3d"
+    android:id="@+id/vignetteEditor"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_weight="1" >
+
+    <com.android.gallery3d.filtershow.imageshow.ImageVignette
+        android:id="@+id/imageVignette"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+ </FrameLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_zoom_editor.xml b/res/layout/filtershow_zoom_editor.xml
new file mode 100644
index 0000000..9813a28
--- /dev/null
+++ b/res/layout/filtershow_zoom_editor.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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"
+    xmlns:iconbutton="http://schemas.android.com/apk/res/com.android.gallery3d"
+    android:id="@+id/basicEditor"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_weight="1" >
+
+    <com.android.gallery3d.filtershow.imageshow.ImageShow
+        android:id="@+id/imageZoom"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+ </FrameLayout>
diff --git a/res/layout/gl_root_group.xml b/res/layout/gl_root_group.xml
new file mode 100644
index 0000000..76ff33b
--- /dev/null
+++ b/res/layout/gl_root_group.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <com.android.gallery3d.ui.GLRootView
+            android:id="@+id/gl_root_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+    <View android:id="@+id/gl_root_cover"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@android:color/black"/>
+</merge>
diff --git a/res/layout/in_line_setting_check_box.xml b/res/layout/in_line_setting_check_box.xml
new file mode 100644
index 0000000..a4d9bba
--- /dev/null
+++ b/res/layout/in_line_setting_check_box.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<com.android.camera.ui.InLineSettingCheckBox xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SettingRow">
+    <TextView android:id="@+id/title"
+            style="@style/SettingItemTitle" />
+
+    <!-- The Switch widget always aligns to the right, so we have to wrap it in a frame layout. -->
+    <FrameLayout
+            android:layout_width="@dimen/setting_item_text_width"
+            android:layout_height="match_parent">
+        <CheckBox android:id="@+id/setting_check_box"
+                android:layout_gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent" />
+    </FrameLayout>
+</com.android.camera.ui.InLineSettingCheckBox>
diff --git a/res/layout/in_line_setting_menu.xml b/res/layout/in_line_setting_menu.xml
new file mode 100644
index 0000000..f45f10f
--- /dev/null
+++ b/res/layout/in_line_setting_menu.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<com.android.camera.ui.InLineSettingMenu xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SettingRow"
+        android:background="@drawable/bg_pressed_exit_fading">
+    <TextView android:id="@+id/title"
+            style="@style/SettingItemTitle" />
+
+    <TextView android:id="@+id/current_setting"
+            style="@style/SettingItemText" />
+
+</com.android.camera.ui.InLineSettingMenu>
+
diff --git a/res/layout/ingest_activity_item_list.xml b/res/layout/ingest_activity_item_list.xml
new file mode 100644
index 0000000..f0e91e8
--- /dev/null
+++ b/res/layout/ingest_activity_item_list.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <com.android.gallery3d.ingest.ui.IngestGridView
+        android:id="@+id/ingest_gridview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:columnWidth="120dip"
+        android:numColumns="auto_fit"
+        android:fastScrollEnabled="true"
+        android:background="@android:color/background_dark"
+        android:choiceMode="multipleChoiceModal"
+        android:stretchMode="columnWidth"  />
+
+    <android.support.v4.view.ViewPager
+        android:id="@+id/ingest_view_pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/background_dark"
+        android:visibility="invisible" />
+
+    <LinearLayout
+        android:id="@+id/ingest_warning_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="20dip"
+        android:gravity="center"
+        android:orientation="horizontal"
+        android:visibility="invisible" >
+
+        <ImageView
+            android:id="@+id/ingest_warning_view_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:src="@android:drawable/ic_dialog_alert" />
+
+        <TextView
+            android:id="@+id/ingest_warning_view_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="10dip"
+            android:textAppearance="?android:attr/textAppearanceSmall" />
+    </LinearLayout>
+</merge>
+
diff --git a/res/layout/ingest_date_tile.xml b/res/layout/ingest_date_tile.xml
new file mode 100644
index 0000000..6b5e934
--- /dev/null
+++ b/res/layout/ingest_date_tile.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<com.android.gallery3d.ingest.ui.DateTileView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/black" >
+    <GridLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center" >
+        <TextView
+            android:id="@+id/date_tile_month"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_column="0"
+            android:layout_row="0"
+            android:layout_gravity="bottom|right"
+            android:layout_marginTop="7sp"
+            android:includeFontPadding="false"
+            android:textSize="16sp"
+            android:textAllCaps="true"
+            android:fontFamily="sans-serif"
+            android:textColor="@color/ingest_date_tile_text" />
+        <TextView
+            android:id="@+id/date_tile_year"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_column="0"
+            android:layout_row="1"
+            android:layout_gravity="top|right"
+            android:includeFontPadding="false"
+            android:textSize="13sp"
+            android:fontFamily="sans-serif-light"
+            android:textColor="@color/ingest_date_tile_text" />
+        <TextView
+            android:id="@+id/date_tile_day"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_column="1"
+            android:layout_row="0"
+            android:layout_rowSpan="2"
+            android:layout_gravity="top|left"
+            android:layout_marginLeft="5sp"
+            android:includeFontPadding="false"
+            android:textSize="44sp"
+            android:textStyle="bold"
+            android:fontFamily="sans-serif"
+            android:textColor="@color/ingest_date_tile_text" />
+    </GridLayout>
+</com.android.gallery3d.ingest.ui.DateTileView>
\ No newline at end of file
diff --git a/res/layout/ingest_fullsize.xml b/res/layout/ingest_fullsize.xml
new file mode 100644
index 0000000..fad596c
--- /dev/null
+++ b/res/layout/ingest_fullsize.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<com.android.gallery3d.ingest.ui.MtpFullscreenView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <ProgressBar
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_centerVertical="true"
+        android:progress="1"
+        android:indeterminate="true"
+        android:indeterminateOnly="true" />
+
+    <com.android.gallery3d.ingest.ui.MtpImageView
+        android:id="@+id/ingest_fullsize_image"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:scaleType="matrix" />
+
+    <CheckBox
+        android:id="@+id/ingest_fullsize_image_checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentRight="true"
+        android:text="@string/Import" />
+
+</com.android.gallery3d.ingest.ui.MtpFullscreenView>
\ No newline at end of file
diff --git a/res/layout/ingest_thumbnail.xml b/res/layout/ingest_thumbnail.xml
new file mode 100644
index 0000000..6907149
--- /dev/null
+++ b/res/layout/ingest_thumbnail.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<com.android.gallery3d.ingest.ui.MtpThumbnailTileView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:scaleType="centerCrop"
+    android:background="@drawable/ingest_item_list_selector">
+</com.android.gallery3d.ingest.ui.MtpThumbnailTileView>
\ No newline at end of file
diff --git a/res/layout/list_pref_setting_popup.xml b/res/layout/list_pref_setting_popup.xml
new file mode 100644
index 0000000..5bfaa52
--- /dev/null
+++ b/res/layout/list_pref_setting_popup.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011, 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.
+-->
+<com.android.camera.ui.ListPrefSettingPopup xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SettingPopupWindow">
+
+    <LinearLayout android:orientation="vertical"
+            android:background="@color/popup_background"
+            android:layout_height="wrap_content"
+            android:layout_width="@dimen/setting_popup_window_width">
+
+        <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="@dimen/popup_title_frame_min_height">
+            <TextView android:id="@+id/title"
+                    style="@style/PopupTitleText" />
+        </FrameLayout>
+
+        <View style="@style/PopupTitleSeparator" />
+
+        <FrameLayout android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+            <ListView android:id="@+id/settingList"
+                    style="@style/SettingItemList"
+                    android:choiceMode="singleChoice" />
+        </FrameLayout>
+    </LinearLayout>
+</com.android.camera.ui.ListPrefSettingPopup>
diff --git a/res/layout/main.xml b/res/layout/main.xml
new file mode 100644
index 0000000..08e5959
--- /dev/null
+++ b/res/layout/main.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/gallery_root"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <include layout="@layout/gl_root_group"/>
+    <FrameLayout android:id="@+id/header"
+            android:visibility="gone"
+            android:layout_alignParentTop="true"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+    <FrameLayout android:id="@+id/footer"
+            android:visibility="gone"
+            android:layout_alignParentBottom="true"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentRight="true"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+</RelativeLayout>
diff --git a/res/layout/manage_offline_bar.xml b/res/layout/manage_offline_bar.xml
new file mode 100644
index 0000000..5c71613
--- /dev/null
+++ b/res/layout/manage_offline_bar.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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:layout_width="match_parent"
+        android:layout_height="wrap_content">
+    <RelativeLayout
+            android:layout_width="fill_parent"
+            android:layout_height="40dp">
+        <TextView android:id="@+id/status"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:layout_centerHorizontal="true" />
+        <ProgressBar android:id="@+id/progress"
+            style="?android:attr/progressBarStyleHorizontal"
+            android:max="100"
+            android:progress="30"
+            android:secondaryProgress="65"
+            android:layout_marginTop="2dp"
+            android:layout_marginBottom="2dp"
+            android:layout_width="130dp"
+            android:layout_height="4dp"
+            android:layout_below="@id/status"
+            android:layout_centerHorizontal="true"/>
+    </RelativeLayout>
+    <RelativeLayout android:layout_width="fill_parent"
+                android:layout_height="@dimen/manage_cache_bottom_height"
+                android:paddingLeft="16dp"
+                android:paddingRight="16dp"
+                android:background="#1f1f1f">
+        <TextView android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/make_available_offline"
+            android:textSize="14sp"
+            android:layout_alignParentLeft="true"
+            android:layout_centerVertical="true"
+            android:gravity="center_vertical"
+            android:drawableLeft="@drawable/ic_menu_make_offline"
+            android:drawablePadding="3dp"/>
+        <Button android:id="@+id/done"
+            android:layout_width="74dp"
+            android:layout_height="match_parent"
+            android:text="@string/done"
+            android:textSize="14sp"
+            android:layout_alignParentRight="true"/>
+    </RelativeLayout>
+</LinearLayout>
diff --git a/res/layout/menu_indicators.xml b/res/layout/menu_indicators.xml
new file mode 100644
index 0000000..0377003
--- /dev/null
+++ b/res/layout/menu_indicators.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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:id="@+id/on_screen_indicators"
+    android:layout_width="64dip"
+    android:layout_height="64dip" >
+
+    <ImageView
+        android:id="@+id/menu_scenemode_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="left|top"
+        android:src="@drawable/ic_indicator_sce_off" />
+
+    <ImageView
+        android:id="@+id/menu_timer_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="center_horizontal|top"
+        android:src="@drawable/ic_indicator_timer_off" />
+
+    <ImageView
+        android:id="@+id/menu_flash_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="right|top"
+        android:src="@drawable/ic_indicator_flash_off" />
+
+    <ImageView
+        android:id="@+id/menu_exposure_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="left|bottom"
+        android:src="@drawable/ic_indicator_ev_0" />
+
+    <ImageView
+        android:id="@+id/menu_location_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="center_horizontal|bottom"
+        android:src="@drawable/ic_indicator_loc_on" />
+
+    <ImageView
+        android:id="@+id/menu_wb_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="right|bottom"
+        android:src="@drawable/ic_indicator_wb_off" />
+
+</FrameLayout>
diff --git a/res/layout/menu_indicators_keyguard.xml b/res/layout/menu_indicators_keyguard.xml
new file mode 100644
index 0000000..7a8795d
--- /dev/null
+++ b/res/layout/menu_indicators_keyguard.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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:id="@+id/on_screen_indicators"
+    android:layout_width="64dip"
+    android:layout_height="64dip" >
+
+    <ImageView
+        android:id="@+id/menu_scenemode_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="left|top"
+        android:src="@drawable/ic_indicator_sce_off" />
+
+    <ImageView
+        android:id="@+id/menu_timer_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="center_horizontal|top"
+        android:src="@drawable/ic_indicator_timer_off" />
+
+    <ImageView
+        android:id="@+id/menu_flash_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="right|top"
+        android:src="@drawable/ic_indicator_flash_auto" />
+
+    <ImageView
+        android:id="@+id/menu_exposure_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="left|bottom"
+        android:src="@drawable/ic_indicator_ev_0" />
+
+    <ImageView
+        android:id="@+id/menu_location_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="center_horizontal|bottom"
+        android:src="@drawable/ic_indicator_loc_on" />
+
+    <ImageView
+        android:id="@+id/menu_wb_indicator"
+        style="@style/MenuIndicator"
+        android:layout_gravity="right|bottom"
+        android:src="@drawable/ic_indicator_wb_off" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/more_setting_popup.xml b/res/layout/more_setting_popup.xml
new file mode 100644
index 0000000..3ccde85
--- /dev/null
+++ b/res/layout/more_setting_popup.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2010, 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.
+-->
+<com.android.camera.ui.MoreSettingPopup xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SettingPopupWindow">
+
+    <FrameLayout
+            android:background="@color/popup_background"
+            android:layout_width="@dimen/big_setting_popup_window_width"
+            android:layout_height="wrap_content">
+        <ListView android:id="@+id/settingList"
+                style="@style/SettingItemList" />
+    </FrameLayout>
+</com.android.camera.ui.MoreSettingPopup>
diff --git a/res/layout/movie_view.xml b/res/layout/movie_view.xml
new file mode 100644
index 0000000..75b8dfd
--- /dev/null
+++ b/res/layout/movie_view.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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/movie_view_root"
+        android:background="@android:color/black"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <VideoView android:id="@+id/surface_view"
+            android:visibility="invisible"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_centerInParent="true" />
+</RelativeLayout>
diff --git a/res/layout/multigrid_content.xml b/res/layout/multigrid_content.xml
new file mode 100644
index 0000000..b1cb145
--- /dev/null
+++ b/res/layout/multigrid_content.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <LinearLayout android:id="@+id/progressContainer"
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone"
+            android:gravity="center">
+
+        <ProgressBar style="?android:attr/progressBarStyleLarge"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        <TextView android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:text="@string/loading"
+                android:paddingTop="4dip"
+                android:singleLine="true" />
+
+    </LinearLayout>
+
+    <FrameLayout android:id="@+id/gridContainer"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+        <GridView android:id="@android:id/list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:choiceMode="multipleChoiceModal"
+                android:numColumns="auto_fit"
+                android:stretchMode="columnWidth"
+                android:drawSelectorOnTop="true" />
+        <TextView android:id="@android:id/empty"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:textAppearance="?android:attr/textAppearanceMedium" />
+    </FrameLayout>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/photo_frame.xml b/res/layout/photo_frame.xml
new file mode 100755
index 0000000..deadaeb
--- /dev/null
+++ b/res/layout/photo_frame.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingTop="4dp"
+        android:paddingBottom="23dp"
+        android:paddingLeft="12dp"
+        android:paddingRight="12dp">
+    <ImageView android:id="@+id/photo"
+            android:layout_gravity="center"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:scaleType="fitCenter"
+            android:cropToPadding="true"
+            android:background="@drawable/border_photo_frame_widget"/>
+</FrameLayout>
diff --git a/res/layout/photo_module.xml b/res/layout/photo_module.xml
new file mode 100644
index 0000000..390863a
--- /dev/null
+++ b/res/layout/photo_module.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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 layout is shared by phone and tablet in both landscape and portrait
+ orientation. The purpose of having this layout is to eventually not manually
+ recreate views when the orientation changes, by migrating the views that do not
+ need to be recreated in onConfigurationChanged from old photo_module to this
+ layout. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center">
+    <TextureView
+        android:id="@+id/preview_content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+    <View
+        android:id="@+id/flash_overlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/white"
+        android:visibility="gone"
+        android:alpha="0" />
+    <ViewStub android:id="@+id/face_view_stub"
+        android:inflatedId="@+id/face_view"
+        android:layout="@layout/face_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"/>
+    <com.android.camera.ui.RenderOverlay
+        android:id="@+id/render_overlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+    <include layout="@layout/camera_controls"
+        android:layout_gravity="center"
+        style="@style/CameraControls"/>
+</merge>
\ No newline at end of file
diff --git a/res/layout/photo_set_item.xml b/res/layout/photo_set_item.xml
new file mode 100644
index 0000000..0f740fa
--- /dev/null
+++ b/res/layout/photo_set_item.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/activatedBackgroundIndicator"
+    android:padding="2dip">
+
+    <com.android.photos.views.SquareImageView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/thumbnail" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/photopage_bottom_controls.xml b/res/layout/photopage_bottom_controls.xml
new file mode 100644
index 0000000..f3226e6
--- /dev/null
+++ b/res/layout/photopage_bottom_controls.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/photopage_bottom_controls"
+        android:padding="10dp"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentLeft="true"
+        android:orientation="horizontal"
+        android:visibility="gone">
+        <ImageButton
+                android:id="@+id/photopage_bottom_control_edit"
+                android:src="@drawable/ic_menu_edit_holo_dark"
+                android:background="@drawable/photopage_bottom_button_background"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentBottom="true"
+                android:paddingTop="5dp"
+                android:paddingBottom="5dp"
+                android:paddingLeft="15dp"
+                android:paddingRight="15dp"
+                android:visibility="gone"/>
+        <ImageButton
+                android:id="@+id/photopage_bottom_control_panorama"
+                android:src="@drawable/ic_view_photosphere"
+                android:background="@drawable/transparent_button_background"
+                android:layout_width="70dp"
+                android:layout_height="70dp"
+                android:layout_centerHorizontal="true"
+                android:layout_alignParentBottom="true"
+                android:paddingTop="5dp"
+                android:paddingBottom="5dp"
+                android:paddingLeft="5dp"
+                android:paddingRight="5dp"
+                android:visibility="gone"/>
+        <ImageButton
+                android:id="@+id/photopage_bottom_control_tiny_planet"
+                android:src="@drawable/ic_menu_tiny_planet"
+                android:background="@drawable/photopage_bottom_button_background"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:layout_alignParentBottom="true"
+                android:paddingTop="5dp"
+                android:paddingBottom="5dp"
+                android:paddingLeft="15dp"
+                android:paddingRight="15dp"
+                android:visibility="gone"/>
+</RelativeLayout>
diff --git a/res/layout/photopage_progress_bar.xml b/res/layout/photopage_progress_bar.xml
new file mode 100644
index 0000000..778feb3
--- /dev/null
+++ b/res/layout/photopage_progress_bar.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/photopage_progress_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentLeft="true"
+        android:padding="25dp"
+        android:visibility="invisible">
+        <View
+                android:id="@+id/photopage_progress_background"
+                android:background="#ff000000"
+                android:layout_width="match_parent"
+                android:layout_height="8dp"
+                android:layout_alignParentBottom="true"
+                android:visibility="visible"/>
+        <View
+                android:id="@+id/photopage_progress_foreground"
+                android:background="#ff33b5e5"
+                android:layout_width="10dp"
+                android:layout_height="8dp"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentBottom="true"
+                android:visibility="visible"/>
+        <TextView
+                android:id="@+id/photopage_progress_bar_text"
+                android:text="@string/pano_progress_text"
+                android:textColor="#ffffffff"
+                android:textSize="14dp"
+                android:shadowColor="#ff000000"
+                android:shadowDx="0"
+                android:shadowDy="0"
+                android:shadowRadius="2"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:layout_above="@id/photopage_progress_background"
+                android:paddingBottom="8dp"
+                android:visibility="visible"/>
+</RelativeLayout>
diff --git a/res/layout/popup_list_item.xml b/res/layout/popup_list_item.xml
new file mode 100644
index 0000000..5a87af7
--- /dev/null
+++ b/res/layout/popup_list_item.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/text1"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceLargePopupMenu"
+    android:singleLine="true"
+    android:gravity="center_vertical"
+    android:paddingLeft="16dp"
+    android:paddingRight="16dp"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:minWidth="196dp"
+/>
diff --git a/res/layout/rotate_dialog.xml b/res/layout/rotate_dialog.xml
new file mode 100644
index 0000000..c62ce91
--- /dev/null
+++ b/res/layout/rotate_dialog.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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:id="@+id/rotate_dialog_root_layout"
+        android:clickable="true"
+        android:gravity="center"
+        android:visibility="gone"
+        android:background="#55000000"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <com.android.camera.ui.RotateLayout
+            android:id="@+id/rotate_dialog_layout"
+            android:gravity="center"
+            android:layout_gravity="center"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" >
+
+        <LinearLayout
+                android:orientation="vertical"
+                android:layout_gravity="center"
+                android:background="@color/popup_background"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content">
+
+            <LinearLayout android:id="@+id/rotate_dialog_title_layout"
+                    android:orientation="vertical"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+
+                <TextView android:id="@+id/rotate_dialog_title"
+                        style="@style/TextAppearance.DialogWindowTitle"
+                        android:gravity="center_vertical"
+                        android:layout_marginLeft="16dip"
+                        android:layout_marginRight="16dip"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:minHeight="64dp"/>
+                <View style="@style/PopupTitleSeparator" />
+            </LinearLayout>
+
+            <LinearLayout
+                    android:orientation="horizontal"
+                    android:background="@color/popup_background"
+                    android:padding="9dp"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content">
+
+                <ProgressBar
+                        android:id="@+id/rotate_dialog_spinner"
+                        android:layout_gravity="center_vertical"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content" />
+                <TextView
+                        style="@style/TextAppearance.Medium"
+                        android:id="@+id/rotate_dialog_text"
+                        android:layout_gravity="center_vertical"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content" />
+            </LinearLayout>
+
+            <ImageView android:background="@drawable/list_divider"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content" />
+
+            <LinearLayout android:id="@+id/rotate_dialog_button_layout"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:minHeight="48dp"
+                    android:orientation="horizontal">
+
+                <Button android:id="@+id/rotate_dialog_button2"
+                        style="@style/Widget.Button.Borderless"
+                        android:gravity="center"
+                        android:maxLines="2"
+                        android:minHeight="48dp"
+                        android:textSize="14sp"
+                        android:layout_weight="1"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content" />
+                <ImageView android:background="@drawable/list_divider"
+                        android:layout_width="wrap_content"
+                        android:layout_height="match_parent" />
+                <Button android:id="@+id/rotate_dialog_button1"
+                        style="@style/Widget.Button.Borderless"
+                        android:gravity="center"
+                        android:maxLines="2"
+                        android:minHeight="48dp"
+                        android:textSize="14sp"
+                        android:layout_weight="1"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content" />
+            </LinearLayout>
+        </LinearLayout>
+    </com.android.camera.ui.RotateLayout>
+</FrameLayout>
diff --git a/res/layout/rotate_text_toast.xml b/res/layout/rotate_text_toast.xml
new file mode 100644
index 0000000..2c89b6f
--- /dev/null
+++ b/res/layout/rotate_text_toast.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<com.android.camera.ui.RotateLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/rotate_toast"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:visibility="gone">
+
+    <FrameLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/toast_frame_holo">
+        <TextView
+            android:id="@+id/message"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:textAppearanceMedium"
+            android:textColor="@android:color/white"
+            android:shadowColor="#BB000000"
+            android:shadowRadius="2.75" />
+    </FrameLayout>
+</com.android.camera.ui.RotateLayout>
+
+
diff --git a/res/layout/secure_album_placeholder.xml b/res/layout/secure_album_placeholder.xml
new file mode 100644
index 0000000..8d9a229
--- /dev/null
+++ b/res/layout/secure_album_placeholder.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:background="@color/photo_placeholder"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:scaleType="center"
+    android:src="@drawable/placeholder_locked"
+/>
diff --git a/res/layout/setting_item.xml b/res/layout/setting_item.xml
new file mode 100644
index 0000000..8571003
--- /dev/null
+++ b/res/layout/setting_item.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011, 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.
+-->
+<com.android.camera.ui.CheckedLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools"
+        tools:ignore="UseCompoundDrawables"
+        style="@style/SettingRow">
+    <TextView android:id="@+id/text"
+            style="@style/SettingItemTitle" />
+    <ImageView android:id="@+id/image"
+            android:layout_height="@dimen/setting_item_icon_width"
+            android:layout_width="@dimen/setting_item_icon_width"
+            android:scaleType="fitCenter"
+            android:adjustViewBounds="true" />
+</com.android.camera.ui.CheckedLinearLayout>
diff --git a/res/layout/time_interval_picker.xml b/res/layout/time_interval_picker.xml
new file mode 100644
index 0000000..d2a9462
--- /dev/null
+++ b/res/layout/time_interval_picker.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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.
+-->
+
+<!-- Layout of time interval picker -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/time_interval_picker"
+        android:orientation="vertical"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent">
+
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+        <TextView
+                android:id="@+id/set_time_interval_title"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:paddingTop="5dip"
+                android:gravity="center"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:text="@string/set_time_interval"/>
+    </LinearLayout>
+
+    <LinearLayout
+            android:orientation="horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingLeft="16dip"
+            android:paddingRight="16dip" >
+
+        <!-- time interval duration -->
+        <NumberPicker
+                android:id="@+id/duration"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:focusable="false" />
+
+        <!-- time interval duration units (seconds/minutes/hours) -->
+        <NumberPicker
+                android:id="@+id/duration_unit"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="2"
+                android:layout_marginLeft="20dip"
+                android:focusable="false" />
+
+    </LinearLayout>
+</LinearLayout>
+
diff --git a/res/layout/time_interval_popup.xml b/res/layout/time_interval_popup.xml
new file mode 100644
index 0000000..9cf224a
--- /dev/null
+++ b/res/layout/time_interval_popup.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011, 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.
+-->
+
+<com.android.camera.ui.TimeIntervalPopup xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SettingPopupWindow">
+
+    <LinearLayout android:orientation="vertical"
+            android:background="@color/popup_background"
+            android:layout_height="wrap_content"
+            android:layout_width="@dimen/big_setting_popup_window_width">
+
+        <LinearLayout android:orientation="horizontal"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent">
+            <TextView android:id="@+id/title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center_vertical"
+                    android:ellipsize="end"
+                    android:layout_weight="1"
+                    android:minHeight="@dimen/popup_title_frame_min_height"
+                    style="@style/PopupTitleText" />
+            <Switch
+                    android:id="@+id/time_lapse_switch"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:layout_weight="0"
+                    android:layout_marginRight="8dp"
+                    android:layout_gravity="right|center_vertical" />
+        </LinearLayout>
+
+        <View style="@style/PopupTitleSeparator" />
+
+        <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+
+            <TextView
+                    android:id="@+id/set_time_interval_help_text"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:paddingTop="16dip"
+                    android:paddingLeft="16dip"
+                    android:paddingRight="16dip"
+                    android:paddingBottom="16dip"
+                    android:textAppearance="?android:attr/textAppearanceMedium"
+                    android:text="@string/set_time_interval_help"/>
+        </LinearLayout>
+
+        <LinearLayout android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal" >
+                <include layout="@layout/time_interval_picker"/>
+        </LinearLayout>
+
+        <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:divider="?android:attr/dividerHorizontal"
+                android:showDividers="beginning"
+                android:dividerPadding="0dip">
+            <Button android:id="@+id/time_lapse_interval_set_button"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"
+                    android:textAppearance="?android:attr/textAppearanceMedium"
+                    style="?android:attr/buttonBarButtonStyle"
+                    android:text="@string/time_lapse_interval_set" />
+        </LinearLayout>
+    </LinearLayout>
+
+</com.android.camera.ui.TimeIntervalPopup>
diff --git a/res/layout/trim_menu.xml b/res/layout/trim_menu.xml
new file mode 100644
index 0000000..e233392
--- /dev/null
+++ b/res/layout/trim_menu.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_centerVertical="true">
+    <TextView
+        android:id="@+id/start_trim"
+        android:layout_marginLeft="8dp"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:text="@string/save"
+        android:textAllCaps="true"
+        android:textSize="14sp"
+        android:gravity="left|center_vertical"
+        android:drawableLeft="@drawable/menu_save_photo"
+        android:drawablePadding="8dp" />
+</FrameLayout>
diff --git a/res/layout/trim_view.xml b/res/layout/trim_view.xml
new file mode 100644
index 0000000..c95c719
--- /dev/null
+++ b/res/layout/trim_view.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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/trim_view_root"
+        android:background="@android:color/black"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <VideoView android:id="@+id/surface_view"
+            android:visibility="visible"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_centerInParent="true" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout/undo_bar.xml b/res/layout/undo_bar.xml
new file mode 100644
index 0000000..33ec91d
--- /dev/null
+++ b/res/layout/undo_bar.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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 layout is shared by phone and tablet in portrait or landscape orientation. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="horizontal"
+        style="@style/UndoBar">
+    <TextView android:text="@string/deleted"
+            style="@style/UndoBarTextAppearance"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:gravity="left|center_vertical" />
+    <View style="@style/UndoBarSeparator" />
+    <TextView android:id="@+id/undo_button"
+            style="@style/UndoButton"
+            android:text="@string/undo"
+            android:drawableLeft="@drawable/ic_menu_revert_holo_dark"/>
+</LinearLayout>
diff --git a/res/layout/video_module.xml b/res/layout/video_module.xml
new file mode 100644
index 0000000..9eb3e84
--- /dev/null
+++ b/res/layout/video_module.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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 layout is shared by phone and tablet in landscape orientation. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent">
+    <TextureView
+        android:id="@+id/preview_content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+    <FrameLayout android:id="@+id/preview_border"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone"
+            android:background="@drawable/ic_snapshot_border" />
+    <com.android.camera.ui.RenderOverlay
+        android:id="@+id/render_overlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+    <com.android.camera.ui.RotateLayout android:id="@+id/recording_time_rect"
+            style="@style/ViewfinderLabelLayout">
+        <include layout="@layout/viewfinder_labels_video" android:id="@+id/labels" />
+    </com.android.camera.ui.RotateLayout>
+    <ImageView android:id="@+id/review_image"
+            android:layout_height="match_parent"
+            android:layout_width="match_parent"
+            android:visibility="gone"
+            android:background="@android:color/black"/>
+    <ImageView
+            android:id="@+id/btn_play"
+            style="@style/ReviewControlIcon"
+            android:layout_centerInParent="true"
+            android:src="@drawable/ic_gallery_play_big"
+            android:visibility="gone"
+            android:onClick="onReviewPlayClicked"/>
+
+    <include layout="@layout/camera_controls"
+        android:layout_gravity="center"
+        style="@style/CameraControls"/>
+</merge>
diff --git a/res/layout/viewfinder_labels_video.xml b/res/layout/viewfinder_labels_video.xml
new file mode 100644
index 0000000..cfe3b02
--- /dev/null
+++ b/res/layout/viewfinder_labels_video.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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 layout is shared by phone and tablet in portrait or landscape orientation. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent">
+    <TextView android:id="@+id/recording_time"
+            style="@style/OnViewfinderLabel"
+            android:gravity="center"
+            android:drawableLeft="@drawable/ic_recording_indicator"
+            android:drawablePadding="5dp"
+            android:visibility="gone" />
+    <TextView android:id="@+id/time_lapse_label"
+            android:text="@string/time_lapse_title"
+            style="@style/OnViewfinderLabel"
+            android:visibility="gone" />
+</LinearLayout>
diff --git a/res/menu/album.xml b/res/menu/album.xml
new file mode 100644
index 0000000..4db0e51
--- /dev/null
+++ b/res/menu/album.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_camera"
+            android:icon="@drawable/ic_menu_camera_holo_light"
+            android:title="@string/switch_to_camera"
+            android:showAsAction="ifRoom" />
+    <item android:id="@+id/action_slideshow"
+            android:icon="@drawable/ic_menu_slideshow_holo_light"
+            android:title="@string/slideshow"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_select"
+            android:title="@string/select_item"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_group_by"
+            android:title="@string/group_by"
+            android:showAsAction="never"/>
+</menu>
diff --git a/res/menu/albumset.xml b/res/menu/albumset.xml
new file mode 100644
index 0000000..8ac8cbb
--- /dev/null
+++ b/res/menu/albumset.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_camera"
+            android:icon="@drawable/ic_menu_camera_holo_light"
+            android:title="@string/switch_to_camera"
+            android:showAsAction="ifRoom" />
+    <item android:id="@+id/action_select"
+            android:title="@string/select_album"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_manage_offline"
+            android:title="@string/make_available_offline"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_sync_picasa_albums"
+            android:title="@string/sync_picasa_albums"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_settings"
+            android:title="@string/settings"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_general_help"
+            android:title="@string/help"
+            android:visible="false"
+            android:showAsAction="never" />
+</menu>
diff --git a/res/menu/crop.xml b/res/menu/crop.xml
new file mode 100644
index 0000000..aa0e035
--- /dev/null
+++ b/res/menu/crop.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/cancel"
+            android:title="@android:string/cancel"
+            android:showAsAction="always|withText">
+    </item>
+    <item android:id="@+id/save"
+            android:title="@string/crop_save_text"
+            android:showAsAction="always|withText">
+    </item>
+</menu>
diff --git a/res/menu/filterby.xml b/res/menu/filterby.xml
new file mode 100644
index 0000000..3a72c57
--- /dev/null
+++ b/res/menu/filterby.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_filter_all"
+            android:title="@string/show_all" />
+    <item android:id="@+id/action_filter_image"
+            android:title="@string/show_images_only" />
+    <item android:id="@+id/action_filter_video"
+            android:title="@string/show_videos_only" />
+</menu>
diff --git a/res/menu/filtershow_activity_menu.xml b/res/menu/filtershow_activity_menu.xml
new file mode 100644
index 0000000..09c6ffe
--- /dev/null
+++ b/res/menu/filtershow_activity_menu.xml
@@ -0,0 +1,37 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item
+        android:id="@+id/menu_share"
+        android:actionProviderClass="android.widget.ShareActionProvider"
+        android:showAsAction="never"
+        android:enabled="false"
+        android:visible="false"
+        android:title="@string/share"/>
+    <item
+        android:id="@+id/undoButton"
+        android:icon="@drawable/filtershow_button_undo"
+        android:showAsAction="always"
+        android:title="@string/filtershow_undo"/>
+    <item
+        android:id="@+id/redoButton"
+        android:icon="@drawable/filtershow_button_redo"
+        android:showAsAction="always"
+        android:title="@string/filtershow_redo"/>
+    <item
+        android:id="@+id/resetHistoryButton"
+        android:title="@string/reset"/>
+    <item
+        android:id="@+id/showImageStateButton"
+        android:showAsAction="never"
+        android:visible="true"
+        android:title="@string/show_imagestate_panel" />
+    <item
+        android:id="@+id/manageUserPresets"
+        android:showAsAction="never"
+        android:visible="true"
+        android:title="@string/filtershow_manage_preset" />
+    <item
+        android:id="@+id/exportFlattenButton"
+        android:showAsAction="never"
+        android:visible="true"
+        android:title="@string/export_flattened" />
+</menu>
diff --git a/res/menu/filtershow_menu_chan_sat.xml b/res/menu/filtershow_menu_chan_sat.xml
new file mode 100644
index 0000000..eae559d
--- /dev/null
+++ b/res/menu/filtershow_menu_chan_sat.xml
@@ -0,0 +1,43 @@
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <group android:id="@+id/grunge_popupmenu" >
+        <item
+            android:id="@+id/editor_chan_sat_main"
+            android:title="@string/editor_chan_sat_main"/>
+        <item
+            android:id="@+id/editor_chan_sat_red"
+            android:title="@string/editor_chan_sat_red"/>
+        <item
+            android:id="@+id/editor_chan_sat_yellow"
+            android:title="@string/editor_chan_sat_yellow"/>
+        <item
+            android:id="@+id/editor_chan_sat_green"
+            android:title="@string/editor_chan_sat_green"/>
+       <item
+            android:id="@+id/editor_chan_sat_cyan"
+            android:title="@string/editor_chan_sat_cyan"/>
+        <item
+            android:id="@+id/editor_chan_sat_blue"
+            android:title="@string/editor_chan_sat_blue"/>
+        <item
+            android:id="@+id/editor_chan_sat_magenta"
+            android:title="@string/editor_chan_sat_magenta"/>
+    </group>
+
+</menu>
\ No newline at end of file
diff --git a/res/menu/filtershow_menu_crop.xml b/res/menu/filtershow_menu_crop.xml
new file mode 100644
index 0000000..f8ba3df
--- /dev/null
+++ b/res/menu/filtershow_menu_crop.xml
@@ -0,0 +1,35 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <group android:id="@+id/crop_popupmenu" >
+        <item
+            android:id="@+id/crop_menu_1to1"
+            android:title="@string/aspect1to1_effect"/>
+        <item
+            android:id="@+id/crop_menu_4to6"
+            android:visible="false"
+            android:title="@string/aspect4to6_effect"/>
+        <item
+            android:id="@+id/crop_menu_4to3"
+            android:title="@string/aspect4to3_effect"/>
+        <item
+            android:id="@+id/crop_menu_3to4"
+            android:title="@string/aspect3to4_effect"/>
+        <item
+            android:id="@+id/crop_menu_5to7"
+            android:title="@string/aspect5to7_effect"/>
+        <item
+            android:id="@+id/crop_menu_7to5"
+            android:title="@string/aspect7to5_effect"/>
+        <item
+            android:id="@+id/crop_menu_9to16"
+            android:visible="false"
+            android:title="@string/aspect9to16_effect"/>
+        <item
+            android:id="@+id/crop_menu_none"
+            android:title="@string/aspectNone_effect"/>
+        <item
+            android:id="@+id/crop_menu_original"
+            android:title="@string/aspectOriginal_effect"/>
+    </group>
+
+</menu>
diff --git a/res/menu/filtershow_menu_curves.xml b/res/menu/filtershow_menu_curves.xml
new file mode 100644
index 0000000..326df45
--- /dev/null
+++ b/res/menu/filtershow_menu_curves.xml
@@ -0,0 +1,18 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <group android:id="@+id/curves_popupmenu" >
+        <item
+            android:id="@+id/curve_menu_rgb"
+            android:title="@string/curves_channel_rgb"/>
+        <item
+            android:id="@+id/curve_menu_red"
+            android:title="@string/curves_channel_red"/>
+        <item
+            android:id="@+id/curve_menu_green"
+            android:title="@string/curves_channel_green"/>
+        <item
+            android:id="@+id/curve_menu_blue"
+            android:title="@string/curves_channel_blue"/>
+    </group>
+
+</menu>
\ No newline at end of file
diff --git a/res/menu/filtershow_menu_draw.xml b/res/menu/filtershow_menu_draw.xml
new file mode 100644
index 0000000..2960c1f
--- /dev/null
+++ b/res/menu/filtershow_menu_draw.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <group android:id="@+id/curves_popupmenu" >
+        <item
+            android:id="@+id/draw_menu_style_line"
+            android:title="@string/draw_style_line" />
+         <item
+            android:id="@+id/draw_menu_style_brush_marker"
+            android:title="@string/draw_style_brush_marker"/>
+         <item
+            android:id="@+id/draw_menu_style_brush_spatter"
+            android:title="@string/draw_style_brush_spatter"/>
+         <item
+            android:id="@+id/draw_menu_size"
+            android:title="@string/draw_size" />
+        <item
+            android:id="@+id/draw_menu_color"
+            android:title="@string/draw_color"/>
+        <item
+            android:id="@+id/draw_menu_clear"
+            android:title="@string/draw_clear"/>
+    </group>
+
+</menu>
\ No newline at end of file
diff --git a/res/menu/filtershow_menu_grad.xml b/res/menu/filtershow_menu_grad.xml
new file mode 100644
index 0000000..1dee7e0
--- /dev/null
+++ b/res/menu/filtershow_menu_grad.xml
@@ -0,0 +1,27 @@
+<!--
+     Copyright (C) 2013 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+     <item
+          android:id="@+id/editor_grad_brightness"
+          android:title="@string/editor_grad_brightness"/>
+     <item
+          android:id="@+id/editor_grad_saturation"
+          android:title="@string/editor_grad_saturation"/>
+     <item
+          android:id="@+id/editor_grad_contrast"
+          android:title="@string/editor_grad_contrast"/>
+</menu>
diff --git a/res/menu/gallery.xml b/res/menu/gallery.xml
new file mode 100644
index 0000000..dc36787
--- /dev/null
+++ b/res/menu/gallery.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item
+        android:id="@+id/menu_camera"
+        android:icon="@android:drawable/ic_menu_camera"
+        android:showAsAction="ifRoom"
+        android:title="@string/menu_camera"/>
+    <item
+        android:id="@+id/menu_search"
+        android:icon="@android:drawable/ic_menu_search"
+        android:showAsAction="ifRoom"
+        android:title="@string/menu_search"/>
+    <item
+        android:id="@+id/menu_settings"
+        android:icon="@android:drawable/ic_menu_preferences"
+        android:showAsAction="never"
+        android:title="@string/settings"/>
+    <item
+        android:id="@+id/menu_help"
+        android:icon="@android:drawable/ic_menu_help"
+        android:showAsAction="never"
+        android:title="@string/help"/>
+</menu>
\ No newline at end of file
diff --git a/res/menu/gallery_multiselect.xml b/res/menu/gallery_multiselect.xml
new file mode 100644
index 0000000..d9365c1
--- /dev/null
+++ b/res/menu/gallery_multiselect.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:id="@+id/menu_edit"
+            android:title="@string/edit"
+            android:visible="false"
+            android:showAsAction="ifRoom" />
+    <item android:id="@+id/menu_delete"
+            android:icon="@android:drawable/ic_menu_delete"
+            android:title="@string/delete"
+            android:visible="false"
+            android:showAsAction="ifRoom" />
+    <item android:id="@+id/menu_share"
+          android:title="@string/share"
+          android:showAsAction="ifRoom"
+          android:visible="false"
+          android:actionProviderClass="android.widget.ShareActionProvider" />
+    <item android:id="@+id/menu_crop"
+            android:title="@string/crop_action"
+            android:visible="false"
+            android:showAsAction="never" />
+    <item android:id="@+id/menu_trim"
+            android:title="@string/trim_action"
+            android:visible="false"
+            android:showAsAction="never" />
+    <item android:id="@+id/menu_mute"
+            android:title="@string/mute_action"
+            android:visible="false"
+            android:showAsAction="never" />
+    <item android:id="@+id/menu_set_as"
+            android:title="@string/set_as"
+            android:visible="false"
+            android:showAsAction="never" />
+</menu>
\ No newline at end of file
diff --git a/res/menu/groupby.xml b/res/menu/groupby.xml
new file mode 100644
index 0000000..b2c2b8d
--- /dev/null
+++ b/res/menu/groupby.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_cluster_album"
+            android:title="@string/group_by_album" />
+    <item android:id="@+id/action_cluster_time"
+            android:title="@string/group_by_time" />
+    <item android:id="@+id/action_cluster_location"
+            android:title="@string/group_by_location" />
+    <item android:id="@+id/action_cluster_tags"
+            android:title="@string/group_by_tags" />
+    <item android:id="@+id/action_cluster_size"
+            android:title="@string/group_by_size" />
+    <item android:id="@+id/action_cluster_faces"
+            android:title="@string/group_by_faces" />
+</menu>
diff --git a/res/menu/ingest_menu_item_list_selection.xml b/res/menu/ingest_menu_item_list_selection.xml
new file mode 100644
index 0000000..2f020b6
--- /dev/null
+++ b/res/menu/ingest_menu_item_list_selection.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/ingest_switch_view"
+          android:showAsAction="always" />
+    <item android:id="@+id/import_items"
+          android:showAsAction="always|withText"
+          android:title="@string/Import" />
+</menu>
\ No newline at end of file
diff --git a/res/menu/movie.xml b/res/menu/movie.xml
new file mode 100644
index 0000000..fde235c
--- /dev/null
+++ b/res/menu/movie.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_share"
+            android:icon="@drawable/ic_menu_share_holo_light"
+            android:title="@string/share"
+            android:enabled="true"
+            android:actionProviderClass="android.widget.ShareActionProvider"
+            android:showAsAction="ifRoom" />
+</menu>
diff --git a/res/menu/operation.xml b/res/menu/operation.xml
new file mode 100644
index 0000000..d1791e2
--- /dev/null
+++ b/res/menu/operation.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_import"
+            android:title="@string/Import"
+            android:icon="@drawable/ic_menu_ptp_holo_light"
+            android:showAsAction="always|withText"
+            android:visible="false" />
+    <!-- Ideally, showAsAction for share_panorama and share should be reversed.
+         But, if share_panorama is set to never, it doesn't seem to get promoted
+         to the action bar and stays on the overflow menu. -->
+    <item android:id="@+id/action_share_panorama"
+            android:icon="@drawable/ic_menu_share_holo_light"
+            android:title="@string/share_panorama"
+            android:visible="false"
+            android:actionProviderClass="android.widget.ShareActionProvider"
+            android:showAsAction="ifRoom">
+    </item>
+    <item android:id="@+id/action_share"
+            android:icon="@drawable/ic_menu_share_holo_light"
+            android:title="@string/share"
+            android:visible="false"
+            android:actionProviderClass="android.widget.ShareActionProvider"
+            android:showAsAction="never">
+    </item>
+    <item android:id="@+id/action_delete"
+            android:icon="@drawable/ic_menu_trash_holo_light"
+            android:title="@string/delete"
+            android:visible="false"
+            android:showAsAction="ifRoom" />
+    <item android:id="@+id/action_edit"
+            android:title="@string/edit"
+            android:showAsAction="never"
+            android:visible="false" />
+    <item android:id="@+id/action_rotate_ccw"
+            android:showAsAction="never"
+            android:visible="false"
+            android:title="@string/rotate_left" />
+    <item android:id="@+id/action_rotate_cw"
+            android:showAsAction="never"
+            android:visible="false"
+            android:title="@string/rotate_right" />
+    <item android:id="@+id/action_crop"
+            android:title="@string/crop_action"
+            android:showAsAction="never"
+            android:visible="false" />
+    <item android:id="@+id/action_setas"
+            android:title="@string/set_image"
+            android:showAsAction="never"
+            android:visible="false" />
+    <item android:id="@+id/action_details"
+            android:icon="@drawable/ic_menu_info_details"
+            android:title="@string/details"
+            android:visible="false"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_show_on_map"
+            android:title="@string/show_on_map"
+            android:showAsAction="never"
+            android:visible="false" />
+</menu>
diff --git a/res/menu/photo.xml b/res/menu/photo.xml
new file mode 100644
index 0000000..48742d1
--- /dev/null
+++ b/res/menu/photo.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_import"
+            android:title="@string/Import"
+            android:icon="@drawable/ic_menu_ptp_holo_light"
+            android:showAsAction="always|withText"
+            android:visible="false" />
+    <!-- Ideally, showAsAction for share_panorama and share should be reversed.
+         But, if share_panorama is set to never, it doesn't seem to get promoted
+         to the action bar and stays on the overflow menu. -->
+    <item android:id="@+id/action_share_panorama"
+            android:icon="@drawable/ic_menu_share_holo_light"
+            android:title="@string/share_panorama"
+            android:visible="false"
+            android:actionProviderClass="android.widget.ShareActionProvider"
+            android:showAsAction="ifRoom" />
+    <item android:id="@+id/action_share"
+            android:icon="@drawable/ic_menu_share_holo_light"
+            android:title="@string/share"
+            android:visible="false"
+            android:actionProviderClass="android.widget.ShareActionProvider"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_delete"
+            android:icon="@drawable/ic_menu_trash_holo_light"
+            android:title="@string/delete"
+            android:visible="false"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_slideshow"
+            android:icon="@drawable/ic_menu_slideshow_holo_light"
+            android:title="@string/slideshow"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_edit"
+            android:title="@string/edit"
+            android:showAsAction="never"
+            android:visible="false" />
+    <item android:id="@+id/action_simple_edit"
+          android:title="@string/simple_edit"
+          android:showAsAction="never"
+          android:visible="false" />
+    <item android:id="@+id/action_rotate_ccw"
+            android:showAsAction="never"
+            android:title="@string/rotate_left" />
+    <item android:id="@+id/action_rotate_cw"
+            android:showAsAction="never"
+            android:title="@string/rotate_right" />
+    <item android:id="@+id/action_crop"
+            android:title="@string/crop_action"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_trim"
+            android:title="@string/trim_action"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_mute"
+            android:title="@string/mute_action"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_setas"
+            android:title="@string/set_image"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_details"
+            android:title="@string/details"
+            android:showAsAction="never" />
+    <item android:id="@+id/action_show_on_map"
+            android:title="@string/show_on_map"
+            android:showAsAction="never" />
+</menu>
diff --git a/res/menu/pickup.xml b/res/menu/pickup.xml
new file mode 100644
index 0000000..44de9b1
--- /dev/null
+++ b/res/menu/pickup.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_cancel"
+            android:title="@string/cancel"
+            android:showAsAction="always|withText" />
+</menu>
diff --git a/res/menu/settings.xml b/res/menu/settings.xml
new file mode 100644
index 0000000..f91f1ba
--- /dev/null
+++ b/res/menu/settings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/add_account"
+            android:title="@string/add_account"
+            android:showAsAction="always|withText">
+    </item>
+</menu>
diff --git a/res/mipmap-hdpi/ic_launcher_camera.png b/res/mipmap-hdpi/ic_launcher_camera.png
new file mode 100644
index 0000000..7b9d090
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_camera.png
Binary files differ
diff --git a/res/mipmap-hdpi/ic_launcher_gallery.png b/res/mipmap-hdpi/ic_launcher_gallery.png
new file mode 100644
index 0000000..23ea998
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_gallery.png
Binary files differ
diff --git a/res/mipmap-hdpi/ic_launcher_video_camera.png b/res/mipmap-hdpi/ic_launcher_video_camera.png
new file mode 100644
index 0000000..d242657
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_video_camera.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_camera.png b/res/mipmap-mdpi/ic_launcher_camera.png
new file mode 100644
index 0000000..9d24f4e
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_camera.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_gallery.png b/res/mipmap-mdpi/ic_launcher_gallery.png
new file mode 100644
index 0000000..e1a9949
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_gallery.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_video_camera.png b/res/mipmap-mdpi/ic_launcher_video_camera.png
new file mode 100644
index 0000000..19f0e64
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_video_camera.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_camera.png b/res/mipmap-xhdpi/ic_launcher_camera.png
new file mode 100644
index 0000000..824161a
--- /dev/null
+++ b/res/mipmap-xhdpi/ic_launcher_camera.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_gallery.png b/res/mipmap-xhdpi/ic_launcher_gallery.png
new file mode 100644
index 0000000..79544a2
--- /dev/null
+++ b/res/mipmap-xhdpi/ic_launcher_gallery.png
Binary files differ
diff --git a/res/mipmap-xxhdpi/ic_launcher_camera.png b/res/mipmap-xxhdpi/ic_launcher_camera.png
new file mode 100644
index 0000000..1e09a6b
--- /dev/null
+++ b/res/mipmap-xxhdpi/ic_launcher_camera.png
Binary files differ
diff --git a/res/raw/backdropper.graph b/res/raw/backdropper.graph
new file mode 100644
index 0000000..1dff2df
--- /dev/null
+++ b/res/raw/backdropper.graph
@@ -0,0 +1,91 @@
+//
+// Copyright (C) 2011 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.
+//
+
+// Imports ---------------------------------------------------
+@import android.filterpacks.base;
+@import android.filterpacks.ui;
+@import android.filterpacks.videosrc;
+@import android.filterpacks.videoproc;
+@import android.filterpacks.videosink;
+
+@setting autoBranch = "synced";
+
+// Externals -------------------------------------------------
+
+@external textureSourceCallback;
+@external recordingWidth;
+@external recordingHeight;
+@external recordingProfile;
+@external recordingDoneListener;
+
+@external previewSurfaceTexture;
+@external previewWidth;
+@external previewHeight;
+
+@external orientation;
+
+@external learningDoneListener;
+
+// Filters ---------------------------------------------------
+
+// Camera input
+@filter SurfaceTextureSource source {
+  sourceListener = $textureSourceCallback;
+  width = $recordingWidth;
+  height = $recordingHeight;
+  closeOnTimeout = true;
+}
+
+// Background video input
+@filter MediaSource background {
+  sourceUrl = "no_file_specified";
+  waitForNewFrame = false;
+  sourceIsUrl = true;
+  orientation = $orientation;
+}
+
+// Background replacer
+@filter BackDropperFilter replacer {
+  autowbToggle = 1;
+  learningDoneListener = $learningDoneListener;
+  orientation = $orientation;
+}
+
+// Display output
+@filter SurfaceTextureTarget display {
+  surfaceTexture = $previewSurfaceTexture;
+  width = $previewWidth;
+  height = $previewHeight;
+}
+
+// Recording output
+@filter MediaEncoderFilter recorder {
+  recordingProfile = $recordingProfile;
+  recordingDoneListener = $recordingDoneListener;
+  recording = false;
+  width = $recordingWidth;
+  height = $recordingHeight;
+  // outputFile, orientationHint, inputRegion,
+  // audioSource, listeners, captureRate
+  // will be set when recording starts
+}
+
+// Connections -----------------------------------------------
+@connect source[video] => replacer[video];
+@connect background[video] => replacer[background];
+@connect replacer[video] => display[frame];
+@connect replacer[video] => recorder[videoframe];
+
diff --git a/res/raw/beep_once.ogg b/res/raw/beep_once.ogg
new file mode 100644
index 0000000..06e8be8
--- /dev/null
+++ b/res/raw/beep_once.ogg
Binary files differ
diff --git a/res/raw/beep_twice.ogg b/res/raw/beep_twice.ogg
new file mode 100644
index 0000000..94a7c14
--- /dev/null
+++ b/res/raw/beep_twice.ogg
Binary files differ
diff --git a/res/raw/blank.jpg b/res/raw/blank.jpg
new file mode 100644
index 0000000..509b5ad
--- /dev/null
+++ b/res/raw/blank.jpg
Binary files differ
diff --git a/res/raw/focus_complete.ogg b/res/raw/focus_complete.ogg
new file mode 100644
index 0000000..0db2683
--- /dev/null
+++ b/res/raw/focus_complete.ogg
Binary files differ
diff --git a/res/raw/goofy_face.graph b/res/raw/goofy_face.graph
new file mode 100644
index 0000000..90e0f3a
--- /dev/null
+++ b/res/raw/goofy_face.graph
@@ -0,0 +1,123 @@
+//
+// Copyright (C) 2011 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.
+//
+
+// Imports ---------------------------------------------------
+@import android.filterpacks.videosrc;
+@import android.filterpacks.videosink;
+@import android.filterpacks.ui;
+@import android.filterpacks.base;
+@import android.filterpacks.imageproc;
+
+@import com.google.android.filterpacks.facedetect;
+
+@setting autoBranch = "synced";
+
+// Externals -------------------------------------------------
+
+@external textureSourceCallback;
+@external recordingWidth;
+@external recordingHeight;
+@external recordingProfile;
+@external recordingDoneListener;
+
+@external previewSurfaceTexture;
+@external previewWidth;
+@external previewHeight;
+
+// Not used by this graph, but simplifies higher-level
+// graph initialization code.
+@external orientation;
+
+// Filters ---------------------------------------------------
+
+// Camera input
+@filter SurfaceTextureSource source {
+  sourceListener = $textureSourceCallback;
+  width = $recordingWidth;
+  height = $recordingHeight;
+  closeOnTimeout = true;
+}
+
+// Face detection
+@filter ToPackedGrayFilter toPackedGray {
+  owidth = 320;
+  oheight = 240;
+  keepAspectRatio = true;
+}
+
+@filter MultiFaceTrackerFilter faceTracker {
+  numChannelsDetector = 3;
+  quality = 0.0f;
+  smoothness = 0.2f;
+  minEyeDist = 25.0f;
+  rollRange = 45.0f;
+  numSkipFrames = 9;
+  trackingError = 1.0;
+  mouthOnlySmoothing = 0;
+  useAffineCorrection = 1;
+  patchSize = 15;
+}
+
+// Goofyface
+@filter GoofyFastRenderFilter goofyrenderer {
+  distortionAmount = 1.0;
+}
+
+// Display output
+@filter SurfaceTextureTarget display {
+  surfaceTexture = $previewSurfaceTexture;
+  width = $previewWidth;
+  height = $previewHeight;
+  renderMode = "stretch";
+}
+
+// Orientation rotation filter
+@filter FixedRotationFilter rotate {
+    rotation = 0;
+}
+
+// Orientation rotation filter for facemeta data
+@filter FaceMetaFixedRotationFilter metarotate {
+    rotation = 0;
+}
+
+
+// Recording output
+@filter MediaEncoderFilter recorder {
+  recordingProfile = $recordingProfile;
+  recordingDoneListener = $recordingDoneListener;
+  recording = false;
+  width = $recordingWidth;
+  height = $recordingHeight;
+  // outputFile, orientationHint, inputRegion,
+  // audioSource, listeners, captureRate
+  // will be set when recording starts
+}
+
+// Connections -----------------------------------------------
+// camera -> faceTracker
+@connect source[video] => rotate[image];
+@connect rotate[image] => toPackedGray[image];
+@connect toPackedGray[image] => faceTracker[image];
+// camera -> goofy
+@connect source[video] => goofyrenderer[image];
+// faceTracker -> metarotate -> goofy
+@connect faceTracker[faces] => metarotate[faces];
+@connect metarotate[faces] => goofyrenderer[faces];
+// goofy -> display out
+@connect goofyrenderer[outimage] => display[frame];
+// goofy -> record
+@connect goofyrenderer[outimage] => recorder[videoframe];
diff --git a/res/raw/video_record.ogg b/res/raw/video_record.ogg
new file mode 100644
index 0000000..d2dee03
--- /dev/null
+++ b/res/raw/video_record.ogg
Binary files differ
diff --git a/res/values-af/filtershow_strings.xml b/res/values-af/filtershow_strings.xml
new file mode 100644
index 0000000..65aa0d2
--- /dev/null
+++ b/res/values-af/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fotoredigeerder"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Kan die beeld nie laai nie!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Stel muurpapier"</string>
+    <string name="original" msgid="3524493791230430897">"Oorspronklike"</string>
+    <string name="borders" msgid="2067345080568684614">"Grense"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Ontdoen"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Herdoen"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Wys geskiedenis"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Versteek geskiedenis"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Wys prenttoestand"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Versteek prenttoestand"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Instellings"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Daar is ongestoorde veranderinge aan hierdie prent."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Wil jy stoor voor jy uitgaan?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Stoor en gaan uit"</string>
+    <string name="exit" msgid="242642957038770113">"Gaan uit"</string>
+    <string name="history" msgid="455767361472692409">"Geskiedenis"</string>
+    <string name="reset" msgid="9013181350779592937">"Stel terug"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Toegepaste uitwerkings"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Vergelyk"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Pas toe"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Stel terug"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspek"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Geen"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Vasgestel"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Klein planeet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Beligting"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Skerpheid"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontras"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Kleurhelderheid"</string>
+    <string name="saturation" msgid="7026791551032438585">"Versadiging"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"SW-filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Outokleur"</string>
+    <string name="hue" msgid="6231252147971086030">"Kleur"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Skaduwees"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Ligstrepe"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kurwes"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinjet"</string>
+    <string name="redeye" msgid="4508883127049472069">"Rooi oog"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Skets"</string>
+    <string name="straighten" msgid="26025591664983528">"Maak reguit"</string>
+    <string name="crop" msgid="5781263790107850771">"Snoei"</string>
+    <string name="rotate" msgid="2796802553793795371">"Draai"</string>
+    <string name="mirror" msgid="5482518108154883096">"Spieël"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatief"</string>
+    <string name="none" msgid="6633966646410296520">"Geen"</string>
+    <string name="edge" msgid="7036064886242147551">"Kante"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Verklein prent"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rooi"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Groen"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blou"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Styl"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Grootte"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Kleur"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Lyne"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Merker"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Spatsels"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Vee uit"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Kies gepasmaakte kleur"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Kies kleur"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Kies grootte"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
new file mode 100644
index 0000000..bf72c67
--- /dev/null
+++ b/res/values-af/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galery"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Prentraam"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videospeler"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Laai tans video…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Laai tans prent…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Laai tans rekening…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Hervat video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Hervat speel vanaf %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Speel verder"</string>
+    <string name="loading" msgid="7038208555304563571">"Laai tans…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Kon nie laai nie"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kon nie die prent laai nie"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Geen kleinkiekie nie"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Begin van voor af"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Raak aan \'n gesig om te begin."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Stoor tans prent…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Stoor foto tans in <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="save_error" msgid="6857408774183654970">"Kon nie gesnoeide prent stoor nie."</string>
+    <string name="crop_label" msgid="521114301871349328">"Snoei prent"</string>
+    <string name="trim_label" msgid="274203231381209979">"Snoei video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Kies foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Kies video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Kies item"</string>
+    <string name="select_album" msgid="1557063764849434077">"Kies album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Kies groep"</string>
+    <string name="set_image" msgid="2331476809308010401">"Stel prent as"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Stel muurpapier"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Stel tans muurpapier in..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Muurpapier"</string>
+    <string name="delete" msgid="2839695998251824487">"Vee uit"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vee gekose item uit?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vee gekose items uit?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bevestig"</string>
+    <string name="cancel" msgid="3637516880917356226">"Kanselleer"</string>
+    <string name="share" msgid="3619042788254195341">"Deling"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Deel panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Deel as foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Uitgevee"</string>
+    <string name="undo" msgid="2930873956446586313">"ONTDOEN"</string>
+    <string name="select_all" msgid="3403283025220282175">"Kies almal"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Ontkies almal"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Skyfievertoning"</string>
+    <string name="details" msgid="8415120088556445230">"Besonderhede"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d van %2$d items:"</string>
+    <string name="close" msgid="5585646033158453043">"Maak toe"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Skakel oor na kamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d gekies"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d gekies"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d gekies"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d gekies"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d gekies"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d gekies"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d gekies"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d gekies"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d gekies"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Wys op kaart"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Draai na links"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Draai na regs"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Kon nie die item vind nie."</string>
+    <string name="edit" msgid="1502273844748580847">"Redigeer"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Verwerk kasversoeke"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Kas tans..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Snoei"</string>
+    <string name="trim_action" msgid="703098114452883524">"Snoei"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Demp"</string>
+    <string name="set_as" msgid="3636764710790507868">"Stel as"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Kan nie video demp nie."</string>
+    <string name="video_err" msgid="7003051631792271009">"Kan nie video speel nie."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Volgens ligging"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Volgens tyd"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Volgens merkers"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Deur mense"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Volgens album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Volgens grootte"</string>
+    <string name="untagged" msgid="7281481064509590402">"Ongemerk"</string>
+    <string name="no_location" msgid="4043624857489331676">"Geen ligging nie"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Sommige liggings kon nie geïdentifiseer word nie weens netwerkprobleme."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Kon nie die foto\'s in hierdie album aflaai nie. Probeer later weer."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Net prente"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Net video\'s"</string>
+    <string name="show_all" msgid="6963292714584735149">"Prente en video\'s"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalery"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Geen foto\'s nie."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Gesnoeide prent gestoor in <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Geen albums beskikbaar nie."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O prente/video\'s beskikbaar."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Plasings"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Maak vanlyn beskikbaar"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Herlaai"</string>
+    <string name="done" msgid="217672440064436595">"Klaar"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d van %2$d items:"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="description" msgid="3016729318096557520">"Beskrywing"</string>
+    <string name="time" msgid="1367953006052876956">"Tyd"</string>
+    <string name="location" msgid="3432705876921618314">"Ligging"</string>
+    <string name="path" msgid="4725740395885105824">"Pad"</string>
+    <string name="width" msgid="9215847239714321097">"Wydte"</string>
+    <string name="height" msgid="3648885449443787772">"Hoogte"</string>
+    <string name="orientation" msgid="4958327983165245513">"Oriëntasie"</string>
+    <string name="duration" msgid="8160058911218541616">"Tydsduur"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-tipe"</string>
+    <string name="file_size" msgid="8486169301588318915">"Lêergrootte"</string>
+    <string name="maker" msgid="7921835498034236197">"Maker"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flits"</string>
+    <string name="aperture" msgid="5920657630303915195">"Apertuur"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Fokuslengte"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Witbalans"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Beligtingstyd"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Handmatig"</string>
+    <string name="auto" msgid="4296941368722892821">"Outo"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flits gevuur"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Geen flits"</string>
+    <string name="unknown" msgid="3506693015896912952">"Onbekend"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Oorspronklike"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Kits"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleik"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blou"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"S/W"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Pons"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X-proses"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Lito"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Maak album vanlyn beskikbaar."</item>
+    <item quantity="other" msgid="4948604338155959389">"Maak albums vanlyn beskikbaar."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Hierdie item is plaaslik gestoor en vanlyn beskikbaar."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Alle albums"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Plaaslike albums"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-toestelle"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa-albums"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> beskikbaar"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> of minder"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> of hoër"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> tot <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Voer in"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Invoer klaar"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Invoer onsuksesvol"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera gekoppel."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera ontkoppel."</string>
+    <string name="click_import" msgid="6407959065464291972">"Raak hier om in te voer"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Kies \'n album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Skommel alle prente"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Kies \'n prent"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Kies prente"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Skyfievertoning"</string>
+    <string name="albums" msgid="7320787705180057947">"Albums"</string>
+    <string name="times" msgid="2023033894889499219">"Tye"</string>
+    <string name="locations" msgid="6649297994083130305">"Liggings"</string>
+    <string name="people" msgid="4114003823747292747">"Mense"</string>
+    <string name="tags" msgid="5539648765482935955">"Merkers"</string>
+    <string name="group_by" msgid="4308299657902209357">"Groepeer volgens"</string>
+    <string name="settings" msgid="1534847740615665736">"Instellings"</string>
+    <string name="add_account" msgid="4271217504968243974">"Voeg rekening by"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Laai af"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Geredigeerde aanlyn foto\'s"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Ingevoer"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skermkiekie"</string>
+    <string name="help" msgid="7368960711153618354">"Hulp"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Geen berging nie"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Geen eksterne berging beskikbaar nie"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmstrook-aansig"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Roosteraansig"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Volskerm-aansig"</string>
+    <string name="trimming" msgid="9122385768369143997">"Snoei tans"</string>
+    <string name="muting" msgid="5094925919589915324">"Demp tans"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Wag asseblief"</string>
+    <string name="save_into" msgid="9155488424829609229">"Stoor video na <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Kan nie snoei nie: teikenvideo is te kort"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Weergewing van panorama"</string>
+    <string name="save" msgid="613976532235060516">"Stoor"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Skandeer tans inhoud..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d items geskandeer"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d item geskandeer"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d items geskandeer"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sorteer tans..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Klaar geskandeer"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Voer tans in..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Daar is geen inhoud beskikbaar om op hierdie toestel in te voer nie."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Daar is geen MTP-toestel gekoppel nie"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kamerafout"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Kan nie aan die kamera koppel nie."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kamera is gedeaktiveer weens sekuriteitsbeleide."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Wag asseblief…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Heg USB-berging voordat kamera gebruik word."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Sit \'n SD-kaart in voor jy die kamera gebruik."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Berei tans USB-berging voor…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Berei tans SD-kaart voor..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Kon nie toegang tot USB-berging kry nie."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Kon nie toegang tot SD-kaart kry."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"KANSELLEER"</string>
+    <string name="review_ok" msgid="1156261588693116433">"KLAAR"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Tydsverloop-opname"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Kies kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Terug"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Voorkant"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Stoor ligging"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Aftel-tydhouer"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sekonde"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d sekondes"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Biep tydens aftelling"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Af"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Aan"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videogehalte"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Hoog"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Laag"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Tydsverloop"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kamera-instellings"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Videokamera-instellings"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Prentgrootte"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M pixels"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M pixels"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M pixels"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M pixels"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fokus"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Outo"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Oneindig"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Flits-modus"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Outo"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Aan"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Af"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Witbalans"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Outo"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Gloeiend"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Daglig"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluoresserend"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Bewolk"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Toneel-modus"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Outo"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Handeling"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nag"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Sonsondergang"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Partytjie"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Kan nie in toneelmodus gekies word nie."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Beligting"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Jou USB-berging se stoorspasie raak min. Verander die gehalte-instelling of vee \'n paar prente of ander lêers uit."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Jou SD-kaart raak vol. Verander die gehalte-instelling of vee \'n paar prente of ander lêers uit."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Groottebeperking bereik."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Te vinnig"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Berei panorama voor"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Kon nie panorama stoor nie."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Lê panorama vas"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Wag vir vorige panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Stoor tans…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Weergewing van panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Raak om te fokus."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effekte"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Geen"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Druk"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Groot oë"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Groot mond"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Klein mond"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Groot neus"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Klein oë"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"In die ruimte"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Sonsondergang"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Jou video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Sit jou toestel neer."\n"Stap vir \'n oomblik buite sig."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Raak om foto tydens opname te neem."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Video-opname het begin."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Video-opname het gestop."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Video-momentopname is gedeaktiveer wanneer spesiale effekte aan is."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Vee effekte uit"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"LAWWE GESIGTE"</string>
+    <string name="effect_background" msgid="6579360207378171022">"AGTERGROND"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Sluiterknoppie"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Kieslysknoppie"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Mees onlangse foto"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Skakelaar vir voorste of agterste kamera"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kamera-, video- of panoramakieser"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Meer instellingkontroles"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Maak instellingkontroles toe"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoembeheer"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Verminder %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Vermeerder %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s merkblokkie"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Skakel oor na foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Skakel oor na video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Skakel oor na panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Skakel oor na nuwe panorama"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Kanselleer hersiening"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Voltooi hersiening"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Hersien neem weer"</string>
+    <string name="capital_on" msgid="5491353494964003567">"AAN"</string>
+    <string name="capital_off" msgid="7231052688467970897">"AF"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Af"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekonde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuut"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ure"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekondes"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minute"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ure"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Klaar"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Stel tydinterval"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Tydsverloop-kenmerk is af. Skakel dit aan om tydstussenpose te stel."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Aftel-tydhouer is af. Skakel dit aan om af te tel voor jy \'n foto neem."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Stel tydsduur in sekondes"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Tel af om \'n foto te neem"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Moet foto-liggings onthou word?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Merk jou foto\'s en video\'s met die liggings waar hulle geneem is."\n\n"Ander programme kan toegang kry tot hierdie inligting saam met jou gestoorde prente."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nee dankie"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ja"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Soek"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Foto\'s"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albums"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d foto\'s"</item>
+  </plurals>
+</resources>
diff --git a/res/values-am/filtershow_strings.xml b/res/values-am/filtershow_strings.xml
new file mode 100644
index 0000000..f644d1f
--- /dev/null
+++ b/res/values-am/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"ፎቶ አርታዒ"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"ምስሉን መጫን አልተቻለም!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"ልጥፍ በማዘጋጀት ላይ"</string>
+    <string name="original" msgid="3524493791230430897">"የመጀመሪያው"</string>
+    <string name="borders" msgid="2067345080568684614">"ድንበሮች"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"ቀልብስ"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"ድገም"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"ታሪክ አሳይ"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"ታሪክ ደብቅ"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"የምስል ሁኔታን አሳይ"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"የምስል ሁኔታን ደብቅ"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"ቅንብሮች"</string>
+    <string name="unsaved" msgid="8704442449002374375">"በዚህ ምስል ላይ"</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"ከመውጣትዎ በፊት ማስቀመጥ ይፈልጋሉ?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"አስቀምጥና ውጣ"</string>
+    <string name="exit" msgid="242642957038770113">"ውጣ"</string>
+    <string name="history" msgid="455767361472692409">"ታሪክ"</string>
+    <string name="reset" msgid="9013181350779592937">"ዳግም አስጀምር"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"የተተገበሩ ተጽዕኖዎች"</string>
+    <string name="compare_original" msgid="8140838959007796977">"አወዳድር"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"ተግብር"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"ዳግም አስጀምር"</string>
+    <string name="aspect" msgid="4025244950820813059">"ገጽታ"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1.1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"ምንም"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"ቋሚ"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"ተጋላጭነት"</string>
+    <string name="sharpness" msgid="6463103068318055412">"ሹልነት"</string>
+    <string name="contrast" msgid="2310908487756769019">"ንፅፅር"</string>
+    <string name="vibrance" msgid="3326744578577835915">"ድምቀት"</string>
+    <string name="saturation" msgid="7026791551032438585">"የቀለም ሙሌት"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"የጥቁርና ነጭ ማጣሪያ"</string>
+    <string name="wbalance" msgid="6346581563387083613">"ራስ-ቀለም መሙላት"</string>
+    <string name="hue" msgid="6231252147971086030">"የቀለም ድባብ"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"ጥላዎች"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"ድምቀቶች"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"ጥምዞች"</string>
+    <string name="vignette" msgid="934721068851885390">"ቪኜት"</string>
+    <string name="redeye" msgid="4508883127049472069">"ቀይ አይን"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"መሳል"</string>
+    <string name="straighten" msgid="26025591664983528">"ቀጥ አርግ"</string>
+    <string name="crop" msgid="5781263790107850771">"ከርክም"</string>
+    <string name="rotate" msgid="2796802553793795371">"አሽከርክር"</string>
+    <string name="mirror" msgid="5482518108154883096">"መስታወት"</string>
+    <string name="negative" msgid="6998313764388022201">"ኔጌቲቭ"</string>
+    <string name="none" msgid="6633966646410296520">"ምንም"</string>
+    <string name="edge" msgid="7036064886242147551">"ጠርዞች"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"ናሙና ማውረድ"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"ቀ አ ሰ"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"ቀይ"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"አረንጓዴ"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"ሰማያዊ"</string>
+    <string name="draw_style" msgid="2036125061987325389">"ቅጥ"</string>
+    <string name="draw_size" msgid="4360005386104151209">"መጠን"</string>
+    <string name="draw_color" msgid="2119030386987211193">"ቀለም"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"መስመሮች"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"አመልካች"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"ነጠብጣብ"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"አጽዳ"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"ብጁ ቀለም ይምረጡ"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"ቀለም ይምረጡ"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"መጠን ይምረጡ"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"እሺ"</string>
+</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
new file mode 100644
index 0000000..68f44c3
--- /dev/null
+++ b/res/values-am/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"የሥነ ጥበብ ማዕከል"</string>
+    <string name="gadget_title" msgid="259405922673466798">"የምስል ክፈፍ"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"የቪዲዮ ማጫወቻ"</string>
+    <string name="loading_video" msgid="4013492720121891585">"ቪዲዮ በማስገባት ላይ"</string>
+    <string name="loading_image" msgid="1200894415793838191">"ምስል በመስቀል ላይ...."</string>
+    <string name="loading_account" msgid="928195413034552034">"መለያ በመስቀል ላይ..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"ቪዲዮ ቀጥል"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"ከ%s  ማጫወት ይቀጥል?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"ማጫወት ቀጥል"</string>
+    <string name="loading" msgid="7038208555304563571">"በመስቀል ላይ…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"መስቀል አልተቻለም"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"ምስሉን መጫን አልተቻለም"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"ምንም ጥፍርአከል የለም"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"እንደገና ጀምር"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"እሺ"</string>
+    <string name="ok" msgid="5296833083983263293">"እሺ"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"ለመጀመር ፊት ንካ::"</string>
+    <string name="saving_image" msgid="7270334453636349407">"ምስል በማስቀመጥ ላይ..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"ስዕል ወደ <xliff:g id="ALBUM_NAME">%1$s</xliff:g> በማስቀመጥ ላይ …"</string>
+    <string name="save_error" msgid="6857408774183654970">"የተቀመቀመውን ምስል ማስቀመጥ አልተቻለም::"</string>
+    <string name="crop_label" msgid="521114301871349328">"ፎቶ ክፈፍ"</string>
+    <string name="trim_label" msgid="274203231381209979">"ቪዲዮን ከርክም"</string>
+    <string name="select_image" msgid="7841406150484742140">"ፎቶዎች ምረጥ"</string>
+    <string name="select_video" msgid="4859510992798615076">"ቪዲዮ ምረጥ"</string>
+    <string name="select_item" msgid="2816923896202086390">"አይነት ምረጥ"</string>
+    <string name="select_album" msgid="1557063764849434077">"አልበም ምረጥ"</string>
+    <string name="select_group" msgid="6744208543323307114">"ቡድን ምረጥ"</string>
+    <string name="set_image" msgid="2331476809308010401">"ምስል እንደ አዘጋጅ"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"ልጣፍ አዘጋጅ"</string>
+    <string name="wallpaper" msgid="140165383777262070">"ልጣፍ በማቀናበር ላይ...."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"ልጣፍ"</string>
+    <string name="delete" msgid="2839695998251824487">"ሰርዝ"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"የተመረጠው ንጥል ይሰረዝ?"</item>
+    <item quantity="other" msgid="5874316486520635333">"የተመረጡት ንጥሎች ይሰረዙ?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"አረጋግጥ"</string>
+    <string name="cancel" msgid="3637516880917356226">"ይቅር"</string>
+    <string name="share" msgid="3619042788254195341">"አጋራ"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"ፓኖራማ አጋራ"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"እንደ ፎቶ አጋራ"</string>
+    <string name="deleted" msgid="6795433049119073871">"ጠፍቷል"</string>
+    <string name="undo" msgid="2930873956446586313">"ቀልብስ"</string>
+    <string name="select_all" msgid="3403283025220282175">"ሁሉንም ምረጥ"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"ሁሉንም አትምረጥ"</string>
+    <string name="slideshow" msgid="4355906903247112975">"ስላይድ አሳይ"</string>
+    <string name="details" msgid="8415120088556445230">"ዝርዝሮች"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d ከ%2$d ንጥሎች፤"</string>
+    <string name="close" msgid="5585646033158453043">"ዝጋ"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"ወደ ካሜራ ቀይር"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d ተመርጠዋል"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d ተመርጠዋል"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d ተመርጠዋል"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d ተመርጠዋል"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d ተመርጠዋል"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d ተመርጠዋል"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d ተመርጠዋል"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d ተመርጠዋል"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d ተመርጠዋል"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"ካርታ ላይ  አሳይ"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"ወደ ግራ አሽከርክር"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"ወደ ቀኝ አሽከርክር"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"ሊገኙ አልተቻለም::"</string>
+    <string name="edit" msgid="1502273844748580847">"አርትዕ"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"መሸጎጫ ጥየቃዎች ሂደት"</string>
+    <string name="caching_label" msgid="4521059045896269095">"በመሸጎጥ ላይ..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"ከርክም"</string>
+    <string name="trim_action" msgid="703098114452883524">"አሳጥር"</string>
+    <string name="mute_action" msgid="5296241754753306251">"ድምጽ-ከል አድርግ"</string>
+    <string name="set_as" msgid="3636764710790507868">"እንደ"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"ቪዲዮ ላይ ድምጸ-ከል ማድረግ አልተቻለም።"</string>
+    <string name="video_err" msgid="7003051631792271009">"ቪዲዮ ማጫወት አይቻልም።"</string>
+    <string name="group_by_location" msgid="316641628989023253">"በስፍራ"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"በጊዜ"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"በመለያዎች"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"በሰዎች"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"በ አልበም"</string>
+    <string name="group_by_size" msgid="153766174950394155">"በመጠን"</string>
+    <string name="untagged" msgid="7281481064509590402">"ያልተለጠፈ"</string>
+    <string name="no_location" msgid="4043624857489331676">"ምንም ስፍራ የለም"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"አንዳንድ ስፍራዎች በአውታረ መረብ  ችግር ምክንያት ለየቶ ማወቅ አልተቻለም::"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"በዚህ አልበም ላይ ያሉ ፎቶዎችን ለማውረድ አልተቻለም፡፡ በኋላ  ደግመህ ሞክር፡፡"</string>
+    <string name="show_images_only" msgid="7263218480867672653">"ምስሎች ብቻ"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"ቪዲዮዎች ብቻ"</string>
+    <string name="show_all" msgid="6963292714584735149">"ምስሎች &amp; ቪዲዮዎች"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"የፎቶ ሥነ ጥበብ ማዕከል"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"ምንም ፎቶዎች የሉም፡፡"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"የተከረከመ ምስል <xliff:g id="FOLDER_NAME">%s</xliff:g> ላይ ተቀምጧል።"</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"ምንም አልበሞች አልተገኙም::"</string>
+    <string name="empty_album" msgid="4542880442593595494">"O ምስሎች/ ቪዲዮዎች ማግኘት ይቻላል::"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"ልጥፎች"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"ከመስመር ውጪ እንዲገኝአድርግ"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"አድስ"</string>
+    <string name="done" msgid="217672440064436595">"ተከናውኗል"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d  ከ%2$d  አይነቶች:"</string>
+    <string name="title" msgid="7622928349908052569">"አርዕስት"</string>
+    <string name="description" msgid="3016729318096557520">"መግለጫ"</string>
+    <string name="time" msgid="1367953006052876956">"ጊዜ"</string>
+    <string name="location" msgid="3432705876921618314">"ስፍራ"</string>
+    <string name="path" msgid="4725740395885105824">"ዱካ"</string>
+    <string name="width" msgid="9215847239714321097">"ስፋት"</string>
+    <string name="height" msgid="3648885449443787772">"ቁመት"</string>
+    <string name="orientation" msgid="4958327983165245513">"አቀማመጠ ገፅ"</string>
+    <string name="duration" msgid="8160058911218541616">"የጊዜ መጠን፡"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME ዓይነት"</string>
+    <string name="file_size" msgid="8486169301588318915">"የፋይል መጠን፡"</string>
+    <string name="maker" msgid="7921835498034236197">"ሰሪ"</string>
+    <string name="model" msgid="8240207064064337366">"ሞዴል"</string>
+    <string name="flash" msgid="2816779031261147723">"ፍላሽ"</string>
+    <string name="aperture" msgid="5920657630303915195">"የካሜራ ሌንስ ማስገቢያ"</string>
+    <string name="focal_length" msgid="1291383769749877010">"የትኩረት ርዝመት"</string>
+    <string name="white_balance" msgid="1582509289994216078">"ነጭ ምጥጥን"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"የብርሃነ መጠን ጊዜ"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"መመሪያ"</string>
+    <string name="auto" msgid="4296941368722892821">"ራስ ሰር"</string>
+    <string name="flash_on" msgid="7891556231891837284">"ብልጭ ብሏል"</string>
+    <string name="flash_off" msgid="1445443413822680010">"ምንም ብልጭታ"</string>
+    <string name="unknown" msgid="3506693015896912952">"አይታወቅም"</string>
+    <string name="ffx_original" msgid="372686331501281474">"የመጀመሪያው"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"ወይን"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"ፈጣን"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"ቀለም አፍዝዝ"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"ሰማያዊ"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"ጥቁር/ነጭ"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"ቡጢ"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"አልበም ከመስመር ውጪ እንዲገኝ ማድረግ"</item>
+    <item quantity="other" msgid="4948604338155959389">"አልበሞች ከመስመር ውጪ እንዲገኙ ማድረግ::"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"ይህ አይነት በአካባቢው ተቀምጧልእና ከመስመር ውጪ አለ።"</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"ሁሉም አልበሞች"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"አካባቢያዊ አልበሞች"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"የMTP መሣሪያዎች"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa አልበሞች"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ነፃ"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ወይም በታች"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ወይም በላይ"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g>ለ <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"አስገባ"</string>
+    <string name="import_complete" msgid="3875040287486199999">"ማስመጣት አጠናቅ"</string>
+    <string name="import_fail" msgid="8497942380703298808">"ማስመጣት ስኬታማ አልነበረም"</string>
+    <string name="camera_connected" msgid="916021826223448591">"ካሜራ ተገናኝቷል::"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"ካሜራ አልተገናኘም::"</string>
+    <string name="click_import" msgid="6407959065464291972">"ለማስገባት እዚህ ንካ"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"አልበም ምረጥ"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"ሁሉንም  ምስሎች  በውዝ"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"ምስል ይምረጡ"</string>
+    <string name="widget_type" msgid="1364653978966343448">"ምስሎችን ምረጥ"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"ስላይድ ትዕይንት"</string>
+    <string name="albums" msgid="7320787705180057947">"አልበሞች"</string>
+    <string name="times" msgid="2023033894889499219">"ጊዜ"</string>
+    <string name="locations" msgid="6649297994083130305">"ሥፍራዎች"</string>
+    <string name="people" msgid="4114003823747292747">"ሰዎች"</string>
+    <string name="tags" msgid="5539648765482935955">"መለያዎች"</string>
+    <string name="group_by" msgid="4308299657902209357">"በቡድን አስቀምጥ"</string>
+    <string name="settings" msgid="1534847740615665736">"ቅንብሮች"</string>
+    <string name="add_account" msgid="4271217504968243974">"መለያ አክል"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"ካሜራ"</string>
+    <string name="folder_download" msgid="7186215137642323932">"አውርድ"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"አርትዖት የተደረጉ የመስመር ላይ ፎቶዎች"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"ከውጭ የገባ"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"ቅጽበታዊ ገጽ እይታ"</string>
+    <string name="help" msgid="7368960711153618354">"እገዛ"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"ምንም ማከማቻ የለም"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"ምንም ውጫዊ ማከማቻ የለም"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"የድርድር ፊልም እይታ"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"የፍርግርግ ዕይታ"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"የሙሉ ማያገጽ እይታ"</string>
+    <string name="trimming" msgid="9122385768369143997">"ማሳጠር"</string>
+    <string name="muting" msgid="5094925919589915324">"ድምጸ-ከል በማድረግ ላይ"</string>
+    <string name="please_wait" msgid="7296066089146487366">"እባክዎ ይጠብቁ"</string>
+    <string name="save_into" msgid="9155488424829609229">"ቪዲዮ ለ<xliff:g id="ALBUM_NAME">%1$s</xliff:g> በማጋራት ላይ …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"ማሳጠር አይቻልም፤ ዒላማው በጣም አጭር ነው"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"ፓኖራማን በማሳየት ላይ"</string>
+    <string name="save" msgid="613976532235060516">"አስቀምጥ"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"ይዘትን በመቃኘት ላይ..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d ንጥሎች ተቃኝተዋል"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d ንጥል ተቃኝቷል"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d ንጥሎች ተቃኝተዋል"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"በመደርደር ላይ..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"ቅኝት ተጠናቅቋል"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"በማስመጣት ላይ…"</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"ወደዚህ መሳሪያ ሊመጣ የሚችል ምንም ሊገኝ የሚችል ይዘት የለም።"</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"ምንም የተገናኘ የMTP መሳሪያ የለም"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"ካሜራ ስህተት"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"ከካሜራ ጋር ማገናኘት አልተቻለም።"</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"በደህንነት ፖሊሲዎች ምክንያት ካሜራ ቦዝኗል።"</string>
+    <string name="camera_label" msgid="6346560772074764302">"ካሜራ"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"ካምኮርድ"</string>
+    <string name="wait" msgid="8600187532323801552">"እባክዎ ይጠብቁ…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"እባክህ ካሜራ ከመጠቀምህ በፊት USB ሰካ።"</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"እባክህ ካሜራውን ከመጠቀምህ በፊት የSD ካርድ አስገባ።"</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"የUSB ማከማቻ በማዘጋጀት ላይ..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"የ SD  ካርድ በማዘጋጀት ላይ..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"USB ማከማችን መድረስ አልተቻለም፡፡"</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"SD ካርድን መድረስ አልተቻለም፡፡"</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ይቅር"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ተከናውኗል"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"በመቅዳት የሚፈጀውን ጊዜ"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"ካሜራ ምረጥ"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"ተመለስ"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"የፊት"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"ሥፍራ"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"ሰዓት ቆጣሪ"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 ሰከንድ"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d ሰከንዶች"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"ጊዜ በሚቆጠርበት ጊዜ ድምጽ"</string>
+    <string name="setting_off" msgid="4480039384202951946">"ጠፍቷል"</string>
+    <string name="setting_on" msgid="8602246224465348901">"በርቷል"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"ቪዲዮ ጥራት"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"ከፍ ያለ"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"ዝቅ ያለ"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"አላፊ ጊዜ"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"የካሜራ ቅንብሮች"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"የካምኮርድ ቅንብሮች"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"የምስል መጠን"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 ሜጋፒክሰል"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 ሜጋ ፒክሴል"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 ሜጋ ፒክሴል"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 ሜጋ ፒክሴል"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3 ሜጋ ፒክሴል"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 ሜጋ ፒክሴል"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"የማተኮር ሁነታ"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"ራስ"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"ወሰን የሌለው"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"ማክሮ"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"የብልጭታ ሁነታ"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"ራስ"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"በ"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"ውጪ"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"ዝግጁ ምስል"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"ራስ"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"ያለፈበት"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"የቀን ብርሃን"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"ፍሎረሰንት"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"ደመናማ"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"የእይታ ሁነታ"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"ራስ"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"ተግባር"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"ማታ"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"ፀሀይ ስትጠልቅ"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"ፓርቲ"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">" በትዕይንት ሁኔታ መመረጥ የሚችል አይደለም።"</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"የተጋለጠ"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"እሺ"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"የUSB  ማከማቻዎቦታ እየሞላበት ነው።የጥራት ቅንብር ይለውጡ ወይም አንዳንድ ምስሎችን ወይም ሌላ ፋይሎች ይሰርዙ።"</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"የSD ካርድዎ ቦታ እያለቀበት ነው። የጥራት ቅንብሩን ይልወጡ ወይም አንዳንድ ምስሎችንወይም ሌሎች ፋይሎችን ይሰርዙ።"</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"መጠኑ ላይ ደርሷል፡፡"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"በጣም ፈጥኖዋል"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"ፓናሮማ በማዘጋጀት ላይ"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"ፓኖራማ  ማስቀመጥ አልተቻለም::"</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"ፓኖራማ"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"ፓኖራማ በማንሳት ላይ"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"ቀዳሚ ፓኖራማ በመጠበቅ ላይ"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"በማስቀመጥ ላይ&amp;hellip;"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"ፓኖራማን በማሳየት ላይ"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"ዳሰስ ለማተኮር፡፡"</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"ማሳመሪያዎች"</string>
+    <string name="effect_none" msgid="3601545724573307541">"ምንም የለም"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"ጭመቅ"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"ትላልቅ ዓይኖች"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"ትልቅ አፍ"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"ትንሽ አፍ"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"ትልቅ አፍንጫ"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"ትናንሽ ዓይኖች"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"ቦታ ውስጥ"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"ፀሀይ ስትጠልቅ"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"ቪዲዮህ"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"መሳሪያህን ወደታች አኑር።"\n"  ለትንሽ ቆይታ ከዕይታ ውጪ ሁን።"</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"እየቀዳህ ፎቶ ለማንሳት ንካ።"</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"የቪዲዮ ቀረጻ ተጀምሯል።"</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"የቪዲዮ ቀረጻ ቆሟል።"</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"ቪዲዮ ማንሻ ልዩ ማሳመሪያዎች ሲበሩ ይቦዝናል::"</string>
+    <string name="clear_effects" msgid="5485339175014139481">"ማሳመሪያዎች አጽዳ"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"ሞኛሞኝ ፊቶች"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ዳራ"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"የካሜራ ሌንስ መከለያ አዝራር።"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"የምናሌ አዝራር"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"በጣም የቅርብ ፎቶ"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"የፊትና ኋላ ካሜራ ማብሪያና ማጥፊያ"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"ካሜራ፣ቪድዮ ወይም ፓናሮማ መምረጫ"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"ተጨማሪ ቅንብሮች  መቈጣጠሪያ"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"የቅንጅት መቆጣጠሪያዎች ዝጋ"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"ኣጕላ  መቆጣጠሪያ"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"ቀንስ %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"ጨምር %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s ምልክት ማድረጊያ ሳጥን"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"ወደ ፎቶ ቀይር"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"ወደ ቪዲዮ ቀይር"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"ወደ ፓኖራማ ቀይር"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"ወደ አዲስ ፓኖራማ ይቀይሩ"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"ግምገማ፣ ሰርዝ"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"ግምገማ፣ ተጠናቅቋል"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"ዳግም የተነሳውን ይገምግሙ"</string>
+    <string name="capital_on" msgid="5491353494964003567">"በርቷል"</string>
+    <string name="capital_off" msgid="7231052688467970897">"ጠፍቷል"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"ጠፍቷል"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 ሰከንድ"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 ሰከንዶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 ደቂቃ"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 ደቂቃዎች"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 ሰዓት"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 ሰዓት"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ሰዓቶች"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ሰዓቶች"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"ሰኮንዶች"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"ደቂቃዎች"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ሰዓቶች"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"ተከናውኗል"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"የጊዜ ክፍተት ያዘጋጁ"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"የአላፊ ጊዜ ባህሪ ጠፍቷል። የጊዜ ክፍተቱን ለማዘጋጀት ያብሩት።"</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"የሰዓት ቆጣሪ ጠፍቷል። ስዕል ከማንሳትዎ በፊት ለመቁጠር ያብሩት።"</string>
+    <string name="set_duration" msgid="5578035312407161304">"ቆይታን በሰከንዶች ያዘጋጁ"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"ፎቶ ለመውሰድ ጊዜ በመቁጠር ላይ"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"የፎቶ አካባቢዎች ይታወሱ?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"ፎቶዎችዎን እና ቪዲዮዎችዎን በተነሱበት አካባቢዎች መለያ ይስጧቸው።"\n\n"ሌሎች መተግበሪያዎች ይህንን መረጃ ከተቀመጡ ምስሎችዎ ጋር ሊደርሱበት ይችላሉ።"</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"አይ፣ አመሰግናለሁ"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"አዎ"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"ካሜራ"</string>
+    <string name="menu_search" msgid="7580008232297437190">"ፍለጋ"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"ፎቶዎች"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"አልበሞች"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d ፎቶ"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d ፎቶዎች"</item>
+  </plurals>
+</resources>
diff --git a/res/values-ar/filtershow_strings.xml b/res/values-ar/filtershow_strings.xml
new file mode 100644
index 0000000..7ccf388
--- /dev/null
+++ b/res/values-ar/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"محرر الصور"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"لا يمكن تحميل الصورة!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"جارٍ تعيين الخلفية"</string>
+    <string name="original" msgid="3524493791230430897">"أصلية"</string>
+    <string name="borders" msgid="2067345080568684614">"حدود"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"تراجع"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"إعادة"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"عرض السجل"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"إخفاء السجل"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"عرض حالة الصورة"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"إخفاء حالة الصورة"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"إعدادات"</string>
+    <string name="unsaved" msgid="8704442449002374375">"هناك تغييرات في هذه الصورة لم يتم حفظها."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"هل تريد الحفظ قبل الخروج؟"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"حفظ وخروج"</string>
+    <string name="exit" msgid="242642957038770113">"خروج"</string>
+    <string name="history" msgid="455767361472692409">"السجل"</string>
+    <string name="reset" msgid="9013181350779592937">"إعادة تعيين"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"التأثيرات المطبقة"</string>
+    <string name="compare_original" msgid="8140838959007796977">"مقارنة"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"تطبيق"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"إعادة تعيين"</string>
+    <string name="aspect" msgid="4025244950820813059">"جانب"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"لا شيء"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"ثابتة"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"كوكب صغير"</string>
+    <string name="exposure" msgid="6526397045949374905">"التعرض للضوء"</string>
+    <string name="sharpness" msgid="6463103068318055412">"حدة"</string>
+    <string name="contrast" msgid="2310908487756769019">"تباين"</string>
+    <string name="vibrance" msgid="3326744578577835915">"حيوية"</string>
+    <string name="saturation" msgid="7026791551032438585">"تشبع اللون"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"فلتر أبيض وأسود"</string>
+    <string name="wbalance" msgid="6346581563387083613">"لون تلقائي"</string>
+    <string name="hue" msgid="6231252147971086030">"تدرج اللون"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"ظلال"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"تسليط الضوء"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"المنحنيات"</string>
+    <string name="vignette" msgid="934721068851885390">"نقوش صورة نصفية"</string>
+    <string name="redeye" msgid="4508883127049472069">"العين الحمراء"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"رسم"</string>
+    <string name="straighten" msgid="26025591664983528">"تسوية"</string>
+    <string name="crop" msgid="5781263790107850771">"اقتصاص"</string>
+    <string name="rotate" msgid="2796802553793795371">"تدوير"</string>
+    <string name="mirror" msgid="5482518108154883096">"انعكاس"</string>
+    <string name="negative" msgid="6998313764388022201">"سالب"</string>
+    <string name="none" msgid="6633966646410296520">"لا شيء"</string>
+    <string name="edge" msgid="7036064886242147551">"الحواف"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"تصغير حجم"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"حمراء"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"خضراء"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"زرقاء"</string>
+    <string name="draw_style" msgid="2036125061987325389">"النمط"</string>
+    <string name="draw_size" msgid="4360005386104151209">"الحجم"</string>
+    <string name="draw_color" msgid="2119030386987211193">"اللون"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"الأسطر"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"محدد"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"رشاش"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"محو"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"اختيار لون مخصص"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"تحديد اللون"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"تحديد الحجم"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"موافق"</string>
+</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
new file mode 100644
index 0000000..d4f6d6b
--- /dev/null
+++ b/res/values-ar/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"المعرض"</string>
+    <string name="gadget_title" msgid="259405922673466798">"إطار الصورة"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"مشغّل الفيديو"</string>
+    <string name="loading_video" msgid="4013492720121891585">"جارٍ تحميل الفيديو…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"جارٍ تحميل الصورة…"</string>
+    <string name="loading_account" msgid="928195413034552034">"جارٍ تحميل الحساب..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"استئناف الفيديو"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"هل تريد استئناف التشغيل من %s ؟"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"استئناف التشغيل"</string>
+    <string name="loading" msgid="7038208555304563571">"جارٍ التحميل…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"تعذر التحميل"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"تعذر تحميل الصورة"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"بلا صورة مصغرة"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"البدء من جديد"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"موافق"</string>
+    <string name="ok" msgid="5296833083983263293">"موافق"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"المس وجهًا للبدء."</string>
+    <string name="saving_image" msgid="7270334453636349407">"جارٍ حفظ الصورة..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"جارٍ حفظ الصورة في <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"تعذر حفظ الصورة التي تم اقتصاصها."</string>
+    <string name="crop_label" msgid="521114301871349328">"اقتصاص الصورة"</string>
+    <string name="trim_label" msgid="274203231381209979">"اقتصاص الفيديو"</string>
+    <string name="select_image" msgid="7841406150484742140">"تحديد صورة"</string>
+    <string name="select_video" msgid="4859510992798615076">"تحديد فيديو"</string>
+    <string name="select_item" msgid="2816923896202086390">"تحديد عنصر"</string>
+    <string name="select_album" msgid="1557063764849434077">"تحديد ألبوم"</string>
+    <string name="select_group" msgid="6744208543323307114">"تحديد مجموعة"</string>
+    <string name="set_image" msgid="2331476809308010401">"تعيين الصورة كـ"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"تعيين خلفية"</string>
+    <string name="wallpaper" msgid="140165383777262070">"جارٍ تعيين الخلفية..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"الخلفية"</string>
+    <string name="delete" msgid="2839695998251824487">"حذف"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"هل تريد حذف العنصر المحدد؟"</item>
+    <item quantity="other" msgid="5874316486520635333">"هل تريد حذف العناصر المحددة؟"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"تأكيد"</string>
+    <string name="cancel" msgid="3637516880917356226">"إلغاء"</string>
+    <string name="share" msgid="3619042788254195341">"مشاركة"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"مشاركة بانوراما"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"مشاركة كصورة"</string>
+    <string name="deleted" msgid="6795433049119073871">"تم حذفها"</string>
+    <string name="undo" msgid="2930873956446586313">"تراجع"</string>
+    <string name="select_all" msgid="3403283025220282175">"تحديد الكل"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"إلغاء تحديد الكل"</string>
+    <string name="slideshow" msgid="4355906903247112975">"عرض الشرائح"</string>
+    <string name="details" msgid="8415120088556445230">"التفاصيل"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d من %2$d من العناصر:"</string>
+    <string name="close" msgid="5585646033158453043">"إغلاق"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"تبديل إلى الكاميرا"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"تم تحديد %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"تم تحديد %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"تم تحديد %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"تم تحديد %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"تم تحديد %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"تم تحديد %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"تم تحديد %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"تم تحديد %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"تم تحديد %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"عرض على الخريطة"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"تدوير لليسار"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"تدوير لليمين"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"تعذر العثور على العنصر."</string>
+    <string name="edit" msgid="1502273844748580847">"تعديل"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"معالجة طلبات التخزين المؤقت"</string>
+    <string name="caching_label" msgid="4521059045896269095">"تخزين مؤقت..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"اقتصاص"</string>
+    <string name="trim_action" msgid="703098114452883524">"اقتطاع"</string>
+    <string name="mute_action" msgid="5296241754753306251">"كتم الصوت"</string>
+    <string name="set_as" msgid="3636764710790507868">"تعيين كـ"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"لا يمكن كتم صوت الفيديو."</string>
+    <string name="video_err" msgid="7003051631792271009">"لا يمكن تشغيل الفيديو."</string>
+    <string name="group_by_location" msgid="316641628989023253">"بحسب الموقع"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"بحسب الوقت"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"بحسب العلامات"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"بحسب الأشخاص"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"بحسب الألبوم"</string>
+    <string name="group_by_size" msgid="153766174950394155">"بحسب الحجم"</string>
+    <string name="untagged" msgid="7281481064509590402">"بلا علامات"</string>
+    <string name="no_location" msgid="4043624857489331676">"لا موقع"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"تعذر تحديد بعض المواقع بسبب مشاكل في الشبكة."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"تعذر تحميل الصور في هذا الألبوم. أعد المحاولة لاحقًا."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"الصور فقط"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"مقاطع الفيديو فقط"</string>
+    <string name="show_all" msgid="6963292714584735149">"الصور ومقاطع الفيديو"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"معرض الصور"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"ليست هناك أية صور."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"تم حفظ الصورة التي تم اقتصاصها في <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"ليست هناك أية ألبومات متاحة."</string>
+    <string name="empty_album" msgid="4542880442593595494">"لا تتوفر أية صور/مقاطع فيديو."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"المشاركات"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"جعلها متاحة في وضع عدم الاتصال"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"تحديث"</string>
+    <string name="done" msgid="217672440064436595">"تم"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d من %2$d من العناصر:"</string>
+    <string name="title" msgid="7622928349908052569">"العنوان"</string>
+    <string name="description" msgid="3016729318096557520">"الوصف"</string>
+    <string name="time" msgid="1367953006052876956">"الوقت"</string>
+    <string name="location" msgid="3432705876921618314">"الموقع"</string>
+    <string name="path" msgid="4725740395885105824">"المسار"</string>
+    <string name="width" msgid="9215847239714321097">"العرض"</string>
+    <string name="height" msgid="3648885449443787772">"الارتفاع"</string>
+    <string name="orientation" msgid="4958327983165245513">"الاتجاه"</string>
+    <string name="duration" msgid="8160058911218541616">"المدة"</string>
+    <string name="mimetype" msgid="8024168704337990470">"نوع MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"حجم الملف"</string>
+    <string name="maker" msgid="7921835498034236197">"منشئ الوسائط"</string>
+    <string name="model" msgid="8240207064064337366">"الطراز"</string>
+    <string name="flash" msgid="2816779031261147723">"الفلاش"</string>
+    <string name="aperture" msgid="5920657630303915195">"فتحة العدسة"</string>
+    <string name="focal_length" msgid="1291383769749877010">"البعد البؤري"</string>
+    <string name="white_balance" msgid="1582509289994216078">"موازنة اللون الأبيض"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"مدة التعرض للضوء"</string>
+    <string name="iso" msgid="5028296664327335940">"سرعة ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"ملليمتر"</string>
+    <string name="manual" msgid="6608905477477607865">"يدوية"</string>
+    <string name="auto" msgid="4296941368722892821">"تلقائية"</string>
+    <string name="flash_on" msgid="7891556231891837284">"تم تشغيل الفلاش"</string>
+    <string name="flash_off" msgid="1445443413822680010">"بلا فلاش"</string>
+    <string name="unknown" msgid="3506693015896912952">"غير معروف"</string>
+    <string name="ffx_original" msgid="372686331501281474">"أصلية"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"فوري"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"أزرق"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"أبيض/أسود"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"تبييض"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"صخري"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"جارٍ جعل الألبوم متاحًا في وضع عدم الاتصال."</item>
+    <item quantity="other" msgid="4948604338155959389">"جارٍ جعل الألبومات متاحة في وضع عدم الاتصال."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"هذا العنصر مخزن محليًا ومتاح في وضع عدم الاتصال."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"كل الألبومات"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"الألبومات المحلية"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"أجهزة MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"ألبومات الويب بيكاسا"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> متوفرة"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> أو أقل"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> أو أكثر"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> إلى <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"استيراد"</string>
+    <string name="import_complete" msgid="3875040287486199999">"انتهى الاستيراد"</string>
+    <string name="import_fail" msgid="8497942380703298808">"أخفق الاستيراد"</string>
+    <string name="camera_connected" msgid="916021826223448591">"الكاميرا متصلة."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"الكاميرا غير متصلة."</string>
+    <string name="click_import" msgid="6407959065464291972">"المس هنا للاستيراد"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"اختيار ألبوم"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"ترتيب عشوائي لجميع الصور"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"اختيار صورة"</string>
+    <string name="widget_type" msgid="1364653978966343448">"اختيار صور"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"عرض الشرائح"</string>
+    <string name="albums" msgid="7320787705180057947">"ألبومات"</string>
+    <string name="times" msgid="2023033894889499219">"التواريخ"</string>
+    <string name="locations" msgid="6649297994083130305">"المواقع"</string>
+    <string name="people" msgid="4114003823747292747">"الأشخاص"</string>
+    <string name="tags" msgid="5539648765482935955">"العلامات"</string>
+    <string name="group_by" msgid="4308299657902209357">"تجميع بحسب"</string>
+    <string name="settings" msgid="1534847740615665736">"الإعدادات"</string>
+    <string name="add_account" msgid="4271217504968243974">"إضافة حساب"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"الكاميرا"</string>
+    <string name="folder_download" msgid="7186215137642323932">"التنزيل"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"الصور المعدلة عبر الإنترنت"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"المستوردة"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"لقطة شاشة"</string>
+    <string name="help" msgid="7368960711153618354">"المساعدة"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"لا تتوفر سعة تخزين"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"لا تتوفر سعة تخزين خارجية"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"عرض شريط الفيلم"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"عرض الشبكة"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"عرض ملء الشاشة"</string>
+    <string name="trimming" msgid="9122385768369143997">"جارٍ الاقتطاع"</string>
+    <string name="muting" msgid="5094925919589915324">"جارِ كتم الصوت"</string>
+    <string name="please_wait" msgid="7296066089146487366">"الرجاء الانتظار"</string>
+    <string name="save_into" msgid="9155488424829609229">"جارٍ حفظ الفيديو <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"لا يمكن الاقتطاع: الفيديو المستهدف قصير جدًا"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"جارٍ عرض البانوراما"</string>
+    <string name="save" msgid="613976532235060516">"حفظ"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"جارٍ مسح المحتوى ضوئيًا..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"تم مسح %1$d من العناصر ضوئيًا"</item>
+    <item quantity="one" msgid="4340019444460561648">"تم مسح %1$d عنصر ضوئيًا"</item>
+    <item quantity="other" msgid="3138021473860555499">"تم مسح %1$d من العناصر ضوئيًا"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"جارٍ التصنيف..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"تم المسح الضوئي"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"جارٍ الاستيراد..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"لا يتوفر أي محتوى لاستيراده على هذا الجهاز."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"ليس هناك جهاز MTP متصل"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"خطأ في الكاميرا"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"يتعذر الاتصال بالكاميرا."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"تم تعطيل الكاميرا بسبب سياسات الأمان."</string>
+    <string name="camera_label" msgid="6346560772074764302">"الكاميرا"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"كاميرا فيديو"</string>
+    <string name="wait" msgid="8600187532323801552">"يرجى الانتظار…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"حمِّل وحدة تخزين USB قبل استخدام الكاميرا."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"أدرج بطاقة SD قبل استخدام الكاميرا."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"جارٍ تحضير وحدة تخزين USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"جارٍ تحضير بطاقة SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"تعذر الدخول إلى وحدة تخزين USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"تعذر الدخول إلى بطاقة SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"إلغاء"</string>
+    <string name="review_ok" msgid="1156261588693116433">"تم"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"تسجيل اللقطات المتتابعة"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"اختيار كاميرا"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"رجوع"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"الأمام"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"تخزين الموقع"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"مؤقت العد التنازلي"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"ثانية واحدة"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d ثانية"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"صفير أثناء العد التنازلي"</string>
+    <string name="setting_off" msgid="4480039384202951946">"إيقاف"</string>
+    <string name="setting_on" msgid="8602246224465348901">"تشغيل"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"جودة الفيديو"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"مرتفعة"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"منخفضة"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"لقطات متتابعة"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"إعدادات الكاميرا"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"إعدادات كاميرا الفيديو"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"حجم الصورة"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 ميغا بكسل"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 ميغا بكسل"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 ميغا بكسل"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 ميغا بكسل"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3 ميغا بكسل"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 ميغا بكسل"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"وضع التركيز"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"تلقائي"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"بلا نهاية"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"ماكرو"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"وضع الفلاش"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"تلقائي"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"تشغيل"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"إيقاف"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"موازنة اللون الأبيض"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"تلقائي"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"براق"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"نهاري"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"فلورسنت"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"غائم"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"وضع المشهد"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"تلقائي"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"النطاق الديناميكي العالي"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"الإجراء"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"ليلي"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"الغروب"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"مجموعة"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"لا يمكن تحديده في وضع المشهد."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"التعرض"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"موافق"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"مساحة وحدة تخزين USB منخفضة. غيّر إعداد الجودة أو احذف بعض الصور أو الملفات الأخرى."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"مساحة بطاقة SD منخفضة. غيّر إعداد الجودة أو احذف بعض الصور أو الملفات الأخرى."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"تم بلوغ الحد الأقصى للحجم."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"سريع للغاية"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"جارٍ تحضير العرض البانورامي"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"تعذر حفظ بانوراما."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"عرض بانورامي"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"جارٍ التقاط عرض بانورامي"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"في انتظار العرض البانورامي السابق"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"جارٍ الحفظ..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"جارٍ عرض البانوراما"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"المس للتركيز."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"تأثيرات"</string>
+    <string name="effect_none" msgid="3601545724573307541">"بلا"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"ضغط"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"عيون كبيرة"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"فم كبير"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"فم صغير"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"أنف كبير"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"عيون صغيرة"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"في الفضاء"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"الغروب"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"مقطع فيديو"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"ضع جهازك أسفل"\n"اخرج من العرض للحظة."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"المس لالتقاط صورة أثناء التسجيل."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"بدأ تسجيل الفيديو."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"تم إيقاف تسجيل الفيديو."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"يتم تعطيل لقطة الفيديو عند تشغيل التأثيرات الخاصة."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"محو التأثيرات"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"وجوه مضحكة"</string>
+    <string name="effect_background" msgid="6579360207378171022">"الخلفية"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"زر المصراع"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"زر القائمة"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"أحدث صورة"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"مفتاح التبديل بين الأمام والخلف"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"محدّد الكاميرا أو الفيديو أو البانوراما"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"مزيد من عناصر التحكم في الإعدادات"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"إغلاق عناصر التحكم في الإعدادات"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"التحكم في التكبير/التصغير"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"خفض %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"زيادة %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"مربع اختيار %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"تبديل إلى وضع الصور"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"تبديل إلى الفيديو"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"تبديل إلى بانوراما"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"تبديل إلى بانوراما جديدة"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"مراجعة الإلغاء"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"تمت المراجعة"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"مراجعة: إعادة الالتقاط"</string>
+    <string name="capital_on" msgid="5491353494964003567">"تشغيل"</string>
+    <string name="capital_off" msgid="7231052688467970897">"إيقاف"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"تم الإيقاف"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"نصف ثانية"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"ثانية واحدة"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"ثانية ونصف"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"ثانيتان"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"ثانيتان ونصف"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 ثوانٍ"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 ثوانٍ"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 ثوانٍ"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 ثوانٍ"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 ثوانٍ"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 ثانية"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 ثانية"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 ثانية"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"نصف دقيقة"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"دقيقة واحدة"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"دقيقة ونصف"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"دقيقتان"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"دقيقتان ونصف"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 دقائق"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 دقائق"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 دقائق"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 دقائق"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 دقائق"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 دقيقة"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 دقيقة"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 دقيقة"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"نصف ساعة"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"ساعة واحدة"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"ساعة ونصف"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"ساعتان"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"ساعتان ونصف"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ساعات"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ساعات"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ساعات"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ساعات"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ساعات"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ساعة"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ساعة"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ساعة"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"ثوانٍ"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"دقائق"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ساعات"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"تم"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"تعيين المهلة الزمنية"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"ميزة اللقطات المتتابعة مغلقة. يمكنك تشغيلها لتعيين المهلة الزمنية."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"مؤقت العد التنازلي متوقف. يُمكنك تشغيله لإجراء العد التنازلي قبل التقاط صورة."</string>
+    <string name="set_duration" msgid="5578035312407161304">"تعيين المدة بالثواني"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"العد التنازلي لالتقاط صورة"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"هل تتذكر مواقع الصور؟"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"ضع علامة على الصور ومقاطع الفيديو التابعة لك تشير إلى المواقع التي تم التقاطها منها."\n\n"يمكن لتطبيقات أخرى الدخول إلى هذه المعلومات إلى جانب صورك المحفوظة."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"لا، شكرًا"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"نعم"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"الكاميرا"</string>
+    <string name="menu_search" msgid="7580008232297437190">"البحث"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"الصور"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"الألبومات"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d صورة"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d من الصور"</item>
+  </plurals>
+</resources>
diff --git a/res/values-be/filtershow_strings.xml b/res/values-be/filtershow_strings.xml
new file mode 100644
index 0000000..66f4506
--- /dev/null
+++ b/res/values-be/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Фотарэдактар"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Не атрымлiваецца загрузіць малюнак"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Усталёўка шпалер..."</string>
+    <string name="original" msgid="3524493791230430897">"Арыгiнал"</string>
+    <string name="borders" msgid="2067345080568684614">"Межы"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Вярнуць"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Паўтарыць"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Паказаць гісторыю"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Схаваць гісторыю"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Паказаць статус малюнка"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Схаваць статус малюнка"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Налады"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Існуюць незахаваныя змяненні ў гэтай выяве."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Жадаеце захавацца перад выхадам?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Захаваць і выйсці"</string>
+    <string name="exit" msgid="242642957038770113">"Выйсці"</string>
+    <string name="history" msgid="455767361472692409">"Гісторыя"</string>
+    <string name="reset" msgid="9013181350779592937">"Скінуць"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Прымененыя эфекты"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Параўнаць"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Паспрабаваць"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Скінуць"</string>
+    <string name="aspect" msgid="4025244950820813059">"Аспект"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Няма"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Выпраўленыя"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Маленькая планета"</string>
+    <string name="exposure" msgid="6526397045949374905">"Экспазіцыя"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Выразнасць"</string>
+    <string name="contrast" msgid="2310908487756769019">"Кантраст"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Вібрацыя"</string>
+    <string name="saturation" msgid="7026791551032438585">"Насычанасць"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Чорна-белы фiльтр"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Aўтаколер"</string>
+    <string name="hue" msgid="6231252147971086030">"Тон"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Цені"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Блікі"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Пэндзаль"</string>
+    <string name="vignette" msgid="934721068851885390">"Віньетка"</string>
+    <string name="redeye" msgid="4508883127049472069">"Чырвонае вока"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Намаляваць"</string>
+    <string name="straighten" msgid="26025591664983528">"Выраўнаваць"</string>
+    <string name="crop" msgid="5781263790107850771">"Абрэзаць"</string>
+    <string name="rotate" msgid="2796802553793795371">"Павярнуць"</string>
+    <string name="mirror" msgid="5482518108154883096">"Люстра"</string>
+    <string name="negative" msgid="6998313764388022201">"Негатыў"</string>
+    <string name="none" msgid="6633966646410296520">"Няма"</string>
+    <string name="edge" msgid="7036064886242147551">"Краi"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Уорхал"</string>
+    <string name="downsample" msgid="3552938534146980104">"Паменшыць"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Чырвоны"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Зялёны"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Сiнi"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Стыль"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Памер"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Колер"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Лiнii"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Маркёр"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Пырскi"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Ачысціць"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Выбраць іншы колер"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Выберыце колер"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Выберыце памер"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
new file mode 100644
index 0000000..a8670de
--- /dev/null
+++ b/res/values-be/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Галерэя"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Фотарамка"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Відэапрайгравальнік"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Загрузка відэа..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Загрузка выявы..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Загрузка ўліковага запісу..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Працягнуць відэа"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Аднавіць прайграванне з %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Працягнуць прайграванне"</string>
+    <string name="loading" msgid="7038208555304563571">"Загрузка..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Не атрымалася загрузiць"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Не атрымалася загрузіць малюнак"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Няма паменшанай выявы"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Пачаць зноў"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ОК"</string>
+    <string name="ok" msgid="5296833083983263293">"ОК"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Дакранiцеся да твару, каб пачаць."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Захаванне выявы..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Захаванне фатаграфіі ў <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="save_error" msgid="6857408774183654970">"Немагчыма захаваць абрэзаную выяву."</string>
+    <string name="crop_label" msgid="521114301871349328">"Абрэзаць малюнак"</string>
+    <string name="trim_label" msgid="274203231381209979">"Абрэзаць відэа"</string>
+    <string name="select_image" msgid="7841406150484742140">"Абраць фота"</string>
+    <string name="select_video" msgid="4859510992798615076">"Выберыце відэа"</string>
+    <string name="select_item" msgid="2816923896202086390">"Выбраць элемент"</string>
+    <string name="select_album" msgid="1557063764849434077">"Выбраць альбом"</string>
+    <string name="select_group" msgid="6744208543323307114">"Выбар групы"</string>
+    <string name="set_image" msgid="2331476809308010401">"Усталяваць малюнак як"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Усталяваць шпалеры"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Усталяванне шпалер..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Шпалеры"</string>
+    <string name="delete" msgid="2839695998251824487">"Выдаліць"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Выдаліць выбраны элемент?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Выдаліць выбраныя элементы?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Пацвердзіць"</string>
+    <string name="cancel" msgid="3637516880917356226">"Адмяніць"</string>
+    <string name="share" msgid="3619042788254195341">"Апублікаваць"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Адправiць панараму"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Адправiць як фота"</string>
+    <string name="deleted" msgid="6795433049119073871">"Выдаленая"</string>
+    <string name="undo" msgid="2930873956446586313">"ВЯРНУЦЬ"</string>
+    <string name="select_all" msgid="3403283025220282175">"Выбраць усё"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Адмяніць выбар усяго"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Слайд-шоу"</string>
+    <string name="details" msgid="8415120088556445230">"Падрабязнасці"</string>
+    <string name="details_title" msgid="2611396603977441273">"Элементаў: %1$d з %2$d"</string>
+    <string name="close" msgid="5585646033158453043">"Закрыць"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Пераключэнне ў рэжым камеры"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d абрана"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d абрана"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d абрана"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d абрана"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d абрана"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d абрана"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d абрана"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d абрана"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d абрана"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Паказаць на карце"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Павярнуць налева"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Павярнуць направа"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Элемент не знойдзены."</string>
+    <string name="edit" msgid="1502273844748580847">"Рэдагаваць"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Запыты на кэшаванне працэсу"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Кэшаванне..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Абрэзаць"</string>
+    <string name="trim_action" msgid="703098114452883524">"Рамка"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Адключыць гук"</string>
+    <string name="set_as" msgid="3636764710790507868">"Ўсталяваць у якасці"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Немагчыма адключыць вiдэа."</string>
+    <string name="video_err" msgid="7003051631792271009">"Не атрымліваецца прайграць відэа."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Па месцазнаходжанні"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Па часе"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Па тэгах"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Па людзях"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Па альбомах"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Паводле памеру"</string>
+    <string name="untagged" msgid="7281481064509590402">"Неазначаныя"</string>
+    <string name="no_location" msgid="4043624857489331676">"Няма месцазнаходж."</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Некаторыя месцы не могуць быць вызначаны з-за праблем сеткі."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Немагчыма спампаваць фатаграфіі ў гэтым альбоме. Паўтарыце спробу пазней."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Толькі малюнкі"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Толькі відэа"</string>
+    <string name="show_all" msgid="6963292714584735149">"Выявы і відэа"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Фотагалерэя"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Фатаграфій няма"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Абрэзаная выява захавана ў тэчцы <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Няма альбомаў."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Выяў/вiдэа: 0."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Паведамленнi"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Адкрыць доступ у аўтан. рэжыме"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Абнавіць"</string>
+    <string name="done" msgid="217672440064436595">"Гатова"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"Элементаў: %1$d з %2$d"</string>
+    <string name="title" msgid="7622928349908052569">"Назва"</string>
+    <string name="description" msgid="3016729318096557520">"Апісанне"</string>
+    <string name="time" msgid="1367953006052876956">"Час"</string>
+    <string name="location" msgid="3432705876921618314">"Месцазнаходж."</string>
+    <string name="path" msgid="4725740395885105824">"Шлях"</string>
+    <string name="width" msgid="9215847239714321097">"Шырыня"</string>
+    <string name="height" msgid="3648885449443787772">"Вышыня"</string>
+    <string name="orientation" msgid="4958327983165245513">"Арыентацыя"</string>
+    <string name="duration" msgid="8160058911218541616">"Працягласць"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Тып MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Памер файла"</string>
+    <string name="maker" msgid="7921835498034236197">"Фатограф"</string>
+    <string name="model" msgid="8240207064064337366">"Мадэль"</string>
+    <string name="flash" msgid="2816779031261147723">"Успышка"</string>
+    <string name="aperture" msgid="5920657630303915195">"Апертура"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Фокусная адл."</string>
+    <string name="white_balance" msgid="1582509289994216078">"Баланс белага"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Вытрымка"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"мм"</string>
+    <string name="manual" msgid="6608905477477607865">"Ручны"</string>
+    <string name="auto" msgid="4296941368722892821">"Аўта"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Са ўспышкай"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Без успышкі"</string>
+    <string name="unknown" msgid="3506693015896912952">"Невядома"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Арыгiнал"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Сiнi"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Чорна-белы"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Латэ"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Афсетны друк"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Адкрыць да альбома доступ у аўтаномным рэжыме"</item>
+    <item quantity="other" msgid="4948604338155959389">"Стварэнне альбомаў даступна ў аўтаномным рэжыме."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Гэты файл захоўваецца лакальна, таму ён даступны ў аўтаномным рэжыме."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Усе альбомы"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Лакальныя альбомы"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Прылады MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Альбомы Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> вольна"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ці ніжэй"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> або вышэй"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> - <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Імпартаваць"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Імпарт завершаны"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Няўдалы iмпарт"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Камера падлучаная."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Камера адключаная."</string>
+    <string name="click_import" msgid="6407959065464291972">"Націсніце тут, каб імпартаваць"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Выбраць альбом"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Змяшаць усе малюнкі"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Выберыце выяву"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Выбар выяў"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Слайд-шоу"</string>
+    <string name="albums" msgid="7320787705180057947">"Альбомы"</string>
+    <string name="times" msgid="2023033894889499219">"Разы"</string>
+    <string name="locations" msgid="6649297994083130305">"Месцы"</string>
+    <string name="people" msgid="4114003823747292747">"Людзі"</string>
+    <string name="tags" msgid="5539648765482935955">"Тэгі"</string>
+    <string name="group_by" msgid="4308299657902209357">"Групаваць па"</string>
+    <string name="settings" msgid="1534847740615665736">"Налады"</string>
+    <string name="add_account" msgid="4271217504968243974">"Дадаць уліковы запіс"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Спампаваць"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Абноўлены фота ў анлайне"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Імпартаваныя"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Скрыншот"</string>
+    <string name="help" msgid="7368960711153618354">"Даведка"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Няма захавальнiкаў"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Няма даступных знешнiх захавальнiкаў"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Дыястужка"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"У выглядзе табліцы"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Поўнаэкранны прагляд"</string>
+    <string name="trimming" msgid="9122385768369143997">"Абрэзка"</string>
+    <string name="muting" msgid="5094925919589915324">"Бязгучна"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Пачакайце"</string>
+    <string name="save_into" msgid="9155488424829609229">"Захаванне відэа ў альбоме <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Немагчыма абрэзаць: мэтавае вiдэа занадта кароткае"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Атрыманне панарамы"</string>
+    <string name="save" msgid="613976532235060516">"Захаваць"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Сканіраванне змесціва..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Сканіравана элементаў: %1$d"</item>
+    <item quantity="one" msgid="4340019444460561648">"Сканіраваны %1$d элемент"</item>
+    <item quantity="other" msgid="3138021473860555499">"Сканiравана элементаў: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Сартаванне..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Сканіраванне скончана"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Імпарт..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Няма кантэнту, даступнага для імпарту на гэтай прыладзе."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Няма MTP, падключанага да прылады"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Памылка камеры"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Не атрымлiваецца падключыцца да камеры."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Камера адключана з-за палітыкі бяспекі."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Камера"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Відэакамера"</string>
+    <string name="wait" msgid="8600187532323801552">"Чакайце..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Перш чым выкарыстоўваць камеру, падключыце USB-назапашвальнiк."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Устаўце SD-карту перад выкарыстаннем камеры."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Падрыхтоўка USB-назапашвальнiка..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Падрыхтоўка SD-карты..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Немагчыма атрымаць доступ да USB-назапашвальніка."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Немагчыма атрымаць доступ да SD-карты."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"АДМЯНІЦЬ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ГАТОВА"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Запіс на працягу доўгага часу"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Выбраць камеру"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Назад"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Перад"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Месцазнаходжанне крамы"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Таймер зваротнага адлiку"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 секунда"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d секунд"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Гукавы сігнал падчас зваротнага адлiку"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Адключана"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Уключана"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Якасць відэа"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Высокая якасць"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Нізкая якасць"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Часавы інтэрвал"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Налады камеры"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Налады відэакамеры"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Памер малюнка"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 мегапікселяў"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 мегапiкселяў"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 мегапiкселя"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 мегапiкселя"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 мегапікселя"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 мегапiксель"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Рэжым факусоўкі"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Аўтаматычна"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Бясконцасць"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Макра"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Рэжым успышкі"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Аўтаматычна"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Уключана"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Адключана"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Баланс белага"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Аўтаматычна"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Белы напал"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Дзённае святло"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Флюарэсцэнтны"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Пахмурна"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Рэжым здымкаў"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Аўтаматычна"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Дзеянне"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Ноч"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Заход"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Вечарына"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Немагчыма выбраць у рэжыме здымкi."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Экспазіцыя"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"ОК"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Месца на вашым USB-назапашвальнiку заканчваецца. Змяніце параметры якасці або выдаліце некаторыя выявы цi іншыя файлы."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Месца на вашай SD-карце заканчваецца. Змяніце параметры якасці або выдаліце некаторыя выявы цi іншыя файлы."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Дасягнуты максімальны памер."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Занад. хутка"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Падрыхтоўка панарамы"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Немагчыма захаваць панараму."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Панарама"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Захоп панарамы"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Чаканне папярэдняй панарамы"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Захаванне..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Рэндэрынг панарамы"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Націсніце, каб наладзiць фокус."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Эфекты"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Няма"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Сцicнуць"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Вялiкiя вочы"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Вялiкi рот"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Маленькi рот"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Вялікі нос"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Маленькiя вочы"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"У космасе"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Заход"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Вашы відэа"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Наладзьце прыладу."\n"На хвіліну выйдзіце з поля зроку."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Націсніце, каб зрабiць здымак падчас запісу."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Відэазапіс пачаўся."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Вiдэазапiс спыніўся."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Капiяванне кадра з відэа адключаецца, калі ўключаны спецэфекты."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Выдалiць эфекты"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"ПАЦЕШНЫЯ СМАЙЛIКI"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ФОНАВЫЯ"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Кнопка затвора"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Кнопка меню"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Апошнія фатаграфii"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Пераключэнне пярэдняй i задняй камеры"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Камера, вiдэа або панарама"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Іншыя налады"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Закрыць налады"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Кiраванне маштабаваннем..."</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Памяншэнне на %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Павелічэнне %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Поле %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Пераключыцца на фота"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Пераключыцца ў рэжым відэа"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Пераключыцца ў рэжым панарамы"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Пераключыцца на новую панараму"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Адмена агляда"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Агляд зроблены"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Пераробка агляда"</string>
+    <string name="capital_on" msgid="5491353494964003567">"УКЛ."</string>
+    <string name="capital_off" msgid="7231052688467970897">"АДКЛ."</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Адключана"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 секунды"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 секунда"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 секунды"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 секунды"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 секунды"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 секунды"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 секунды"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 секунды"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 хвіліны"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 хвіліна"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 хвіліны"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 хвіліны"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 хвіліны"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 хвіліны"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 хвіліны"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 хвілін"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 хвілін"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 хвілін"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 хвілін"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 хвілін"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 хвіліны"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 гадзіны"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 гадзіна"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 гадзіны"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 гадзіны"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 гадзіны"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 гадзіны"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 гадзіны"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 гадзін"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 гадзін"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 гадзін"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 гадзін"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 гадзін"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 гадзіны"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"секунды"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"хвілін"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"г."</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Гатова"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Задаць iнтэрвал часу"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Функцыя \"Часавы інтэрвал\" выключана. Уключыце яе."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Таймер выключаны. Уключыце на iм зваротны адлік перад здымкай."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Усталяваць час у секундах"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Зваротны адлiк, каб зрабіць здымак"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Запамiнаць месца, дзе зроблена фота?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Адзначце на вашых фатаграфіях і відэа месцы, у якіх яны былі створаныя."\n\n"Іншыя праграмы могуць атрымаць доступ да гэтай інфармацыі разам з захаванымі выявамі."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Не, дзякуй"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Так"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Камера"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Пошук"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Фатаграфіі"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Альбомы"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d фота"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d фота"</item>
+  </plurals>
+</resources>
diff --git a/res/values-bg/filtershow_strings.xml b/res/values-bg/filtershow_strings.xml
new file mode 100644
index 0000000..715692b
--- /dev/null
+++ b/res/values-bg/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Фоторедактор"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Изображението не може да се зареди!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Тапетът се задава"</string>
+    <string name="original" msgid="3524493791230430897">"Оригинал"</string>
+    <string name="borders" msgid="2067345080568684614">"Контури"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Отмяна"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Възстановяване"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"История: Показване"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"История: Скриване"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Съст. на изобр.: Показване"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Съст. на изобр.: Скриване"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Настройки"</string>
+    <string name="unsaved" msgid="8704442449002374375">"В това изображение има незапазени промени."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Искате ли да запазите преди изход?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Запазване и изход"</string>
+    <string name="exit" msgid="242642957038770113">"Изход"</string>
+    <string name="history" msgid="455767361472692409">"История"</string>
+    <string name="reset" msgid="9013181350779592937">"Повторно задаване"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Приложени ефекти"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Сравняване"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Прилагане"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Нулиране"</string>
+    <string name="aspect" msgid="4025244950820813059">"Съотношение"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Без"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Фиксирано"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Мини планета"</string>
+    <string name="exposure" msgid="6526397045949374905">"Експониране"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Отчетливост"</string>
+    <string name="contrast" msgid="2310908487756769019">"Контраст"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Живи цветове"</string>
+    <string name="saturation" msgid="7026791551032438585">"Насищане"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Филтър за Ч/Б"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Авт. цвят"</string>
+    <string name="hue" msgid="6231252147971086030">"Нюанс"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Засенчване"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Просветляване"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Извивки"</string>
+    <string name="vignette" msgid="934721068851885390">"Винетиране"</string>
+    <string name="redeye" msgid="4508883127049472069">"Червени очи"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Рисуване"</string>
+    <string name="straighten" msgid="26025591664983528">"Изправяне"</string>
+    <string name="crop" msgid="5781263790107850771">"Подрязване"</string>
+    <string name="rotate" msgid="2796802553793795371">"Завъртане"</string>
+    <string name="mirror" msgid="5482518108154883096">"Огледало"</string>
+    <string name="negative" msgid="6998313764388022201">"Негатив"</string>
+    <string name="none" msgid="6633966646410296520">"Без"</string>
+    <string name="edge" msgid="7036064886242147551">"Контури"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Уорхол"</string>
+    <string name="downsample" msgid="3552938534146980104">"Децимация"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"червено"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"зелено"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"синьо"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Стил"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Размер"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Цвят"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Линии"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Маркер"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Пръски боя"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Изчистване"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Избор на персонализиран цвят"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Избиране на цвят"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Избиране на размер"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
new file mode 100644
index 0000000..cf0a879
--- /dev/null
+++ b/res/values-bg/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Галерия"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Рамка на снимка"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Видеоплейър"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Видеоклипът се зарежда..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Изображението се зарежда..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Профилът се зарежда…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Възобновяване на видеоклип"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Да продължи ли възпроизвеждането от %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Продължаване"</string>
+    <string name="loading" msgid="7038208555304563571">"Зарежда се…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Не можаха да се заредят"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Изображението не можа да бъде заредено"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Няма миниизображение"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Стартиране отначало"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Докоснете лице за начало."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Снимката се запазва..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Снимката се запазва в/ъв „<xliff:g id="ALBUM_NAME">%1$s</xliff:g>“…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Подрязаното изобр. не можа да се запази."</string>
+    <string name="crop_label" msgid="521114301871349328">"Подрязване на снимка"</string>
+    <string name="trim_label" msgid="274203231381209979">"Отрязване на видеоклипа"</string>
+    <string name="select_image" msgid="7841406150484742140">"Избиране на снимка"</string>
+    <string name="select_video" msgid="4859510992798615076">"Избиране на видеоклип"</string>
+    <string name="select_item" msgid="2816923896202086390">"Избиране на елемент"</string>
+    <string name="select_album" msgid="1557063764849434077">"Изберете албум"</string>
+    <string name="select_group" msgid="6744208543323307114">"Изберете група"</string>
+    <string name="set_image" msgid="2331476809308010401">"Задаване на снимката като"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Задаване на тапет"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Тапетът се задава..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Тапет"</string>
+    <string name="delete" msgid="2839695998251824487">"Изтриване"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Да се изтрие ли избраното?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Да се изтрият ли избраните?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Потвърждаване"</string>
+    <string name="cancel" msgid="3637516880917356226">"Отказ"</string>
+    <string name="share" msgid="3619042788254195341">"Споделяне"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Споделяне на панорамата"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Споделяне като снимка"</string>
+    <string name="deleted" msgid="6795433049119073871">"Изтрито"</string>
+    <string name="undo" msgid="2930873956446586313">"ОТМЯНА"</string>
+    <string name="select_all" msgid="3403283025220282175">"Избиране на всички"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Премахване на избора"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Слайдшоу"</string>
+    <string name="details" msgid="8415120088556445230">"Подробности"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d от %2$d елемента:"</string>
+    <string name="close" msgid="5585646033158453043">"Затваряне"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Превключване към камера"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Избрани: %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Избрани: %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Избрани: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Избрани: %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Избрани: %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Избрани: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Избрани: %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Избрани: %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Избрани: %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Показване на карта"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Завъртане наляво"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Завъртане надясно"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Елементът не можа да бъде намерен."</string>
+    <string name="edit" msgid="1502273844748580847">"Редактиране"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Заявките за кеширане се обработват"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Кешира се..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Подрязване"</string>
+    <string name="trim_action" msgid="703098114452883524">"Отрязване"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Спиране"</string>
+    <string name="set_as" msgid="3636764710790507868">"Задаване като"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Клипът не може да се спре."</string>
+    <string name="video_err" msgid="7003051631792271009">"Видеоклипът не може да се възпроизведе."</string>
+    <string name="group_by_location" msgid="316641628989023253">"По местоположение"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"По време"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"По маркери"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"По хора"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"По албум"</string>
+    <string name="group_by_size" msgid="153766174950394155">"По размер"</string>
+    <string name="untagged" msgid="7281481064509590402">"Немаркирани"</string>
+    <string name="no_location" msgid="4043624857489331676">"Няма местоположение"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Някои местоположения не можаха да бъдат идентифицирани поради проблеми с мрежата."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Снимките в този албум не можаха да се изтеглят. Опитайте отново по-късно."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Само изображения"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Само видеоклипове"</string>
+    <string name="show_all" msgid="6963292714584735149">"Изображения и видеоклипове"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерия"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Няма снимки."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Подрязаното изображение е запазено в/ъв „<xliff:g id="FOLDER_NAME">%s</xliff:g>“."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Няма налични албуми."</string>
+    <string name="empty_album" msgid="4542880442593595494">"0 изображения/видеоклипове са налице."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Публикации"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Налице офлайн"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Опресняване"</string>
+    <string name="done" msgid="217672440064436595">"Готово"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d от %2$d елемента:"</string>
+    <string name="title" msgid="7622928349908052569">"Заглавие"</string>
+    <string name="description" msgid="3016729318096557520">"Описание"</string>
+    <string name="time" msgid="1367953006052876956">"Час"</string>
+    <string name="location" msgid="3432705876921618314">"Местоположение"</string>
+    <string name="path" msgid="4725740395885105824">"Път"</string>
+    <string name="width" msgid="9215847239714321097">"Ширина"</string>
+    <string name="height" msgid="3648885449443787772">"Височина"</string>
+    <string name="orientation" msgid="4958327983165245513">"Ориентация"</string>
+    <string name="duration" msgid="8160058911218541616">"Времетраене"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Тип MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Файл: Размер"</string>
+    <string name="maker" msgid="7921835498034236197">"Автор"</string>
+    <string name="model" msgid="8240207064064337366">"Модел"</string>
+    <string name="flash" msgid="2816779031261147723">"Светкавица"</string>
+    <string name="aperture" msgid="5920657630303915195">"Бленда"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Дълж. на фокус"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Бяло: Баланс"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Експонация"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"мм"</string>
+    <string name="manual" msgid="6608905477477607865">"Ръчно"</string>
+    <string name="auto" msgid="4296941368722892821">"Авт."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Със светкавица"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Без светкавица"</string>
+    <string name="unknown" msgid="3506693015896912952">"Неизвестно"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Оригинал"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Ретро"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Моментно фото"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Избелване"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"синьо"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Ч/Б"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Пунш"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Кроспроцес"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Лате"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Литография"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Албумът става достъпен офлайн."</item>
+    <item quantity="other" msgid="4948604338155959389">"Албумите стават достъпни офлайн."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Този елемент се съхранява локално и е налице офлайн."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Всички албуми"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Местни албуми"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP устройства"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Албуми в Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Свободни: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> или по-малко"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> или повече"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> до <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Импортиране"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Импорт. завърши"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Импортирането не бе успешно"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Камерата е включена."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Камерата е изключена."</string>
+    <string name="click_import" msgid="6407959065464291972">"Докоснете тук, за да импортирате"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Избор на албум"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Разбъркване на всички"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Избор на изображение"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Избор на изображения"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Слайдшоу"</string>
+    <string name="albums" msgid="7320787705180057947">"Албуми"</string>
+    <string name="times" msgid="2023033894889499219">"Час"</string>
+    <string name="locations" msgid="6649297994083130305">"Mестопол."</string>
+    <string name="people" msgid="4114003823747292747">"Хора"</string>
+    <string name="tags" msgid="5539648765482935955">"Маркери"</string>
+    <string name="group_by" msgid="4308299657902209357">"Групиране по"</string>
+    <string name="settings" msgid="1534847740615665736">"Настройки"</string>
+    <string name="add_account" msgid="4271217504968243974">"Добавяне на профил"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Изтегляне"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Редактирани снимки онлайн"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Импортирани"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Екранна снимка"</string>
+    <string name="help" msgid="7368960711153618354">"Помощ"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Няма хранилище"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Няма налично външно хранилище"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Изглед „Филмова лента“"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Табличен изглед"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Изглед на цял екран"</string>
+    <string name="trimming" msgid="9122385768369143997">"Отрязва се"</string>
+    <string name="muting" msgid="5094925919589915324">"Спира се"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Моля, изчакайте"</string>
+    <string name="save_into" msgid="9155488424829609229">"Видеоклипът се запазва в/ъв „<xliff:g id="ALBUM_NAME">%1$s</xliff:g>“…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Не може да се отреже: Целевият видеоклип е твърде кратък"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Панорамата се изобразява"</string>
+    <string name="save" msgid="613976532235060516">"Запазване"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Съдържанието се сканира..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Сканирани са %1$d елемента"</item>
+    <item quantity="one" msgid="4340019444460561648">"Сканиран е %1$d елемент"</item>
+    <item quantity="other" msgid="3138021473860555499">"Сканирани са %1$d елемента"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Сортира се..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Сканирането завърши"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Импортира се..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Няма съдържание, налично за импортиране на това устройство."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Няма свързано устройство, поддържащо MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Грешка в камерата"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Не може да се осъществи връзка с камерата."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Камерата е деактивирана заради правилата за сигурност."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Камера"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Видеокамера"</string>
+    <string name="wait" msgid="8600187532323801552">"Моля, изчакайте…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Свържете USB хранилището, преди да използвате камерата."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Поставете SD карта, преди да използвате камерата."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB хранилището се подготвя..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD картата се подготвя..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Няма достъп до USB хранилището."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Няма достъп до SD картата."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ОТКАЗ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ГОТОВО"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Запис на цайтрафера"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Избор на камера"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Задна"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Предна"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Място за съхранение"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Таймер за обратното отброяване"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 секунда"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d секунди"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Писукане при обратното отбр."</string>
+    <string name="setting_off" msgid="4480039384202951946">"Изключено"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Включено"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Качество на видеоклип"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Високо"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Ниско"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Цайтрафер"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Настройки на камера"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Настройки на видеокамера"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Размер на снимка"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 мегапиксел"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Фокусен режим"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Автоматичен"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Безкрайност"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Макро"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Светкавица"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Автоматичен"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Включена"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Изкл."</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Баланс на бялото"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Автоматичен"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Изкуствена светлина"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Дневна светлина"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Флуоресцентна светлина"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Облачно"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Сценичен режим"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Автоматичен"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Действие"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Нощ"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Залез"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Празненство"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Не може да се избира в сценичен режим."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Eкспониране"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Мястото на USB хранилището ви привършва. Променете настройките за качество или изтрийте някои изображения или други файлове."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Мястото на SD картата ви привършва. Променете настройките за качество или изтрийте някои изображения или други файлове."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Достигнат е макс. размер."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Твърде бързо"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Панорамата се подготвя"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Панорамата не можа да бъде запазена."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Панорама"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Панорамата се заснема"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Изчаква се предишната панорама"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Запазва се..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Панорамата се изобразява"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Докоснете за фокусиране."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Ефекти"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Без ефекти"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Разкривяване"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Големи очи"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Голяма уста"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Малка уста"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Голям нос"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Малки очи"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"В космоса"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Залез"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Видеоклипът ви"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Оставете устройството си."\n"Отдръпнете се от зрителното му поле за момент."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Докоснете, за да направите снимка, докато записвате."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Видеозаписът започна."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Видеозаписът спря."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Моментните снимки в клиповете са деакт. при вкл. спец. ефекти."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Изчистване на ефектите"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"СМЕШНИ ЛИЦА"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ФОН"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Бутон на затвора"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Бутон за меню"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Най-скорошна снимка"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Превключване между предната и задната камера"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Избор между камера, видеокамера или панорама"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Още контроли за настройки"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Затваряне на контролите за настройки"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Контрола за промяна на мащаба"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Намаляване на %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Увеличаване на %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Квадратче за отметка за %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Превключване към фотоапарат"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Превключване към видео"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Превключване към панорама"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Превключване към нова панорама"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Анулиране на прегледа"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Прегледът приключи"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Преглед на презаснетия елемент"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ВКЛЮЧЕНО"</string>
+    <string name="capital_off" msgid="7231052688467970897">"ИЗКЛЮЧЕНО"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Изкл."</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 секунда"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 минути"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 час"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 часа"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 часа"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"секунди"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"минути"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"часа"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Готово"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Задаване на интервал от време"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Функцията за цайтрафер е изключена. Включете я, за да зададете интервал от време."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Таймерът за обратното отброяване е изключен. Включете го, за да отброява, преди да направите снимка."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Задаване на продължителността в секунди"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Обратно отброяване до правенето на снимка"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Да се запомнят ли местоположенията на снимките?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Поставете в снимките и видеоклиповете си маркери с местоположенията, на които са направени."\n\n"Другите приложения могат да осъществяват достъп до тази информация, както и до запазените ви изображения."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Не, благодаря"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Да"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Камера"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Търсене"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Снимки"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Албуми"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d снимка"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d снимки"</item>
+  </plurals>
+</resources>
diff --git a/res/values-ca/filtershow_strings.xml b/res/values-ca/filtershow_strings.xml
new file mode 100644
index 0000000..495947f
--- /dev/null
+++ b/res/values-ca/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor de fotos"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"No es pot carregar la imatge."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"S\'està establint el fons de pantalla"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Vores"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Desfés"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Refés"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Mostra l\'historial"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Amaga l\'historial"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Mostra estat imatge"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Amaga estat d\'imatge"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Configuració"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Hi ha canvis sense desar en aquesta imatge."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Vols desar abans de sortir?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Desa i surt"</string>
+    <string name="exit" msgid="242642957038770113">"Surt"</string>
+    <string name="history" msgid="455767361472692409">"Historial"</string>
+    <string name="reset" msgid="9013181350779592937">"Restableix"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Efectes aplicats"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Compara"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Aplica"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Restableix"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspecte"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Cap"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"S\'ha corregit"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Planeta petit"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposició"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Nitidesa"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vibració"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturació"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtre B/N"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autocolor"</string>
+    <string name="hue" msgid="6231252147971086030">"To de color"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Ombres"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Punts brillants"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Corbes"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinyeta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Ulls vermells"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Dibuixos"</string>
+    <string name="straighten" msgid="26025591664983528">"Redreça"</string>
+    <string name="crop" msgid="5781263790107850771">"Retalla"</string>
+    <string name="rotate" msgid="2796802553793795371">"Gira"</string>
+    <string name="mirror" msgid="5482518108154883096">"Reflex"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatiu"</string>
+    <string name="none" msgid="6633966646410296520">"Cap"</string>
+    <string name="edge" msgid="7036064886242147551">"Vores"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Reduïda"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Vermell"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Verd"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blau"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Estil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Mida"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Color"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Línies"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marcador"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Esquitx"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Esborra"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Tria un color personalitzat"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Selecció del color"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Selecció de la mida"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"D\'acord"</string>
+</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
new file mode 100644
index 0000000..333b3af
--- /dev/null
+++ b/res/values-ca/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Marc de la imatge"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Reproductor de vídeo"</string>
+    <string name="loading_video" msgid="4013492720121891585">"S\'està carregant el vídeo..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"S\'està carregant la imatge…"</string>
+    <string name="loading_account" msgid="928195413034552034">"S\'està carregant el compte…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Reprèn el vídeo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Voleu reprendre la reproducció a partir de %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Reprèn la reproducció"</string>
+    <string name="loading" msgid="7038208555304563571">"S\'està carregant…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"No s\'ha pogut carregar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"No s\'ha pogut carregar la imatge"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"No hi ha cap miniatura"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Torna a començar"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"D\'acord"</string>
+    <string name="ok" msgid="5296833083983263293">"Accepta"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Toca una cara per començar."</string>
+    <string name="saving_image" msgid="7270334453636349407">"S\'està desant la imatge..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"S\'està desant la imatge a <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"No s\'ha pogut desar la imatge retallada."</string>
+    <string name="crop_label" msgid="521114301871349328">"Escapça la imatge"</string>
+    <string name="trim_label" msgid="274203231381209979">"Retalla el vídeo"</string>
+    <string name="select_image" msgid="7841406150484742140">"Selecciona una foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Selecciona un vídeo"</string>
+    <string name="select_item" msgid="2816923896202086390">"Tria un element"</string>
+    <string name="select_album" msgid="1557063764849434077">"Selecciona un àlbum"</string>
+    <string name="select_group" msgid="6744208543323307114">"Selecciona un grup"</string>
+    <string name="set_image" msgid="2331476809308010401">"Defineix la imatge com a"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Defineix el fons de pantalla"</string>
+    <string name="wallpaper" msgid="140165383777262070">"S\'està establint el fons de pantalla..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fons de pantalla"</string>
+    <string name="delete" msgid="2839695998251824487">"Suprimeix"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vols supr. l\'element selecc.?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vols supr. els elements sel.?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirma"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancel·la"</string>
+    <string name="share" msgid="3619042788254195341">"Comparteix"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Comparteix el panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Comparteix com a foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Suprimida"</string>
+    <string name="undo" msgid="2930873956446586313">"DESFÉS"</string>
+    <string name="select_all" msgid="3403283025220282175">"Selecciona-ho tot"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Anul·la la selecció de tot"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Presentació de diapositives"</string>
+    <string name="details" msgid="8415120088556445230">"Detalls"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d de %2$d elements:"</string>
+    <string name="close" msgid="5585646033158453043">"Tanca"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Canvia a la càmera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d seleccionats"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d seleccionat"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d seleccionats"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d seleccionats"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d seleccionat"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d seleccionats"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d seleccionats"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d seleccionat"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d seleccionats"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostra al mapa"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Gira a l\'esquerra"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Gira a la dreta"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"No s\'ha trobat l\'element."</string>
+    <string name="edit" msgid="1502273844748580847">"Edita"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"S\'estan processant les sol·licituds de memòria cau"</string>
+    <string name="caching_label" msgid="4521059045896269095">"S\'està desant a la memòria cau..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Retalla"</string>
+    <string name="trim_action" msgid="703098114452883524">"Retalla"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Silencia"</string>
+    <string name="set_as" msgid="3636764710790507868">"Defineix com a"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"No es pot silenciar el vídeo."</string>
+    <string name="video_err" msgid="7003051631792271009">"No es pot reproduir el vídeo."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Per ubicació"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Per temps"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Per etiquetes"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Per persones"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Per àlbum"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Per mida"</string>
+    <string name="untagged" msgid="7281481064509590402">"Sense etiquetar"</string>
+    <string name="no_location" msgid="4043624857489331676">"Sense ubicació"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Hi ha ubicacions que no s\'han pogut identificar a causa de problemes amb la xarxa."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"No s\'han pogut baixar les fotos en aquest àlbum. Torna-ho a provar més tard."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Només imatges"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Només vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imatges i vídeos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotos"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Sense fotos."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"S\'ha desat la imatge retallada a <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"No hi ha àlbums disponibles."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O imatges/vídeos disponibles."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Publicacions"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Disponible fora de línia"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Actualitza"</string>
+    <string name="done" msgid="217672440064436595">"Fet"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d elements:"</string>
+    <string name="title" msgid="7622928349908052569">"Títol"</string>
+    <string name="description" msgid="3016729318096557520">"Descripció"</string>
+    <string name="time" msgid="1367953006052876956">"Hora"</string>
+    <string name="location" msgid="3432705876921618314">"Ubicació"</string>
+    <string name="path" msgid="4725740395885105824">"Camí"</string>
+    <string name="width" msgid="9215847239714321097">"Amplada"</string>
+    <string name="height" msgid="3648885449443787772">"Alçada"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientació"</string>
+    <string name="duration" msgid="8160058911218541616">"Durada"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Tipus MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Mida del fitxer"</string>
+    <string name="maker" msgid="7921835498034236197">"Creador"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flaix"</string>
+    <string name="aperture" msgid="5920657630303915195">"Obertura"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Longitud focal"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balanç de blancs"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Temps d\'exposició"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Automàtic"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flaix disparat"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Sense flaix"</string>
+    <string name="unknown" msgid="3506693015896912952">"Desconeguda"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instantània"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Destenyeix"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blau"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"B/N"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Cop"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Procés X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Fent que l\'àlbum estigui disponible fora de línia."</item>
+    <item quantity="other" msgid="4948604338155959389">"Fent que àlbums estiguin disponibles fora de línia"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"L\'element s\'ha emmagatzemat localment i està disponible fora de línia."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Tots els àlbums"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Àlbums locals"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Dispositius MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Àlbums de Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Lliure: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o menys"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o més"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importa"</string>
+    <string name="import_complete" msgid="3875040287486199999">"S\'ha completat la importació"</string>
+    <string name="import_fail" msgid="8497942380703298808">"La importació no s\'ha dut a terme correctament"</string>
+    <string name="camera_connected" msgid="916021826223448591">"S\'ha connectat la càmera."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"S\'ha desconnectat la càmera."</string>
+    <string name="click_import" msgid="6407959065464291972">"Toca aquí per importar"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Tria un àlbum"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Barreja totes les imatges"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Selecciona una imatge"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Selecció d\'imatges"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Pres. diapositives"</string>
+    <string name="albums" msgid="7320787705180057947">"Àlbums"</string>
+    <string name="times" msgid="2023033894889499219">"Dates"</string>
+    <string name="locations" msgid="6649297994083130305">"Ubicacions"</string>
+    <string name="people" msgid="4114003823747292747">"Persones"</string>
+    <string name="tags" msgid="5539648765482935955">"Etiquetes"</string>
+    <string name="group_by" msgid="4308299657902209357">"Agrupa per"</string>
+    <string name="settings" msgid="1534847740615665736">"Configuració"</string>
+    <string name="add_account" msgid="4271217504968243974">"Afegeix un compte"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Càmera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Baixades"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Fotos editades en línia"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importades"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captura de pantalla"</string>
+    <string name="help" msgid="7368960711153618354">"Ajuda"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"No hi ha emmagatzem."</string>
+    <string name="no_external_storage" msgid="95726173164068417">"No hi ha emmagatzematge extern disponible"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Vista de tira de pel·lícula"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Vista de quadrícula"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Visual. pant. compl."</string>
+    <string name="trimming" msgid="9122385768369143997">"S\'està retallant"</string>
+    <string name="muting" msgid="5094925919589915324">"S\'està silenciant"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Espera"</string>
+    <string name="save_into" msgid="9155488424829609229">"S\'està desant el vídeo a <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"No es pot retallar: el vídeo de destinació és massa curt"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panorama de representació"</string>
+    <string name="save" msgid="613976532235060516">"Desa"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"S\'està explorant el contingut..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elements explorats"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d element explorat"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d elements explorats"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"S\'està ordenant..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Exploració finalitzada"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"S\'està important..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"No hi ha contingut disponible per importar en aquest dispositiu."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"No hi ha cap dispositiu MTP connectat"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Error de la càmera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"No es pot connectar a la càmera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"La càmera s\'ha desactivat a causa de les polítiques de seguretat."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Càmera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Càmera de vídeo"</string>
+    <string name="wait" msgid="8600187532323801552">"Espereu-vos…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Instal·la l\'emmagatzematge USB abans d\'utilitzar la càmera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Insereix una targeta SD abans d\'utilitzar la càmera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Prep. l\'emmagatzematge USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"S\'està preparant la targeta SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"No s\'ha pogut accedir a l\'emmagatzematge USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"No s\'ha pogut accedir a la targeta SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"CANCEL·LA"</string>
+    <string name="review_ok" msgid="1156261588693116433">"FET"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"S\'està enregistrant lapse de temps"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Tria de la càmera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Enrere"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Frontal"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Emmagatzema la ubicació"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Temporitzador de compte enrere"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 segon"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d segons"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Sona en compte enr."</string>
+    <string name="setting_off" msgid="4480039384202951946">"Desactivat"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Activat"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Qualitat de vídeo"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Alta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Baixa"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Lapse de temps"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Configuració de la càmera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Configuració de la càmera de vídeo"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Mida de la imatge"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapíxels"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapíxels"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapíxels"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapíxels"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapíxels"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapíxel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Mode d\'enfocament"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automàtic"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinit"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Mode de flaix"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automàtic"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"A"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Desactivat"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balanç de blancs"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automàtica"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescent"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Llum de dia"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescent"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Ennuvolat"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Mode d\'escena"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automàtic"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Acció"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nocturn"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Posta del sol"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Festa"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"No es pot seleccionar en mode d\'escena."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exposició"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"D\'acord"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"L\'emmagatzematge USB s\'està quedant sense espai. Canvia la configuració de qualitat o bé suprimeix unes quantes imatges o altres fitxers."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"La targeta SD s\'està quedant sense espai. Canvia la configuració de qualitat o suprimeix unes quantes imatges o altres fitxers."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"S\'ha arribat al límit de mida"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Massa ràpid"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"S\'està preparant el panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"No s\'ha pogut desar el panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"S\'està capturant un panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"S\'està esperant la panoràmica anterior"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"S\'està desant..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panorama de representació"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Toca per enfocar."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efectes"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Cap"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Aixafa"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Ulls grans"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Boca gran"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Boca petita"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Nas gran"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Ulls petits"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"A l\'espai"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Posta del sol"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"El teu vídeo"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Deixa el dispositiu."\n"Surt un moment de la visualització."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Toca per fer una foto mentre enregistres."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"L\'enregistrament de vídeo ha començat."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"L\'enregistrament de vídeo s\'ha aturat."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Les instantànies del vídeo es desactiven quan hi ha els efectes especials activats."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Esborra efectes"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"GANYOTES"</string>
+    <string name="effect_background" msgid="6579360207378171022">"FONS"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Botó de l\'obturador"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Botó de menú"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Foto més recent"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Commutador de càmera anterior i posterior"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Càmera, vídeo o selector de panorames"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Més controls de configuració"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Tanca els controls de configuració"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Control de zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Redueix %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Augmenta %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Casella de selecció %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Canvia a foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Canvia a vídeo"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Canvia a panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Canvia al panorama nou"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Cancel·la en mode de revisió"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Fet en mode de revisió"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Revisa la foto o el vídeo nou"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ACTIVAT"</string>
+    <string name="capital_off" msgid="7231052688467970897">"DESACTIVAT"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Desactivat"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 segon"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 segons"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minuts"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hora"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 hores"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 hores"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"segons"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minuts"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"hores"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Fet"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Definició d\'interval de temps"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"La funció de lapse de temps està desactivada. Activa-la per definir l\'interval de temps."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"El temporitzador del compte enrere està desactivat. Activa el compte enrere abans de fer una foto."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Configuració de la durada en segons"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Compte enrere per fer una foto"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Vols que es recordin les ubicacions de les fotos?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Etiqueta les teves fotos i els teus vídeos amb les ubicacions des de les quals es fan."\n\n"Altres aplicacions podran accedir a aquesta informació juntament amb les imatges desades."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"No, gràcies"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Sí"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Càmera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Cerca"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Àlbums"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotos"</item>
+  </plurals>
+</resources>
diff --git a/res/values-cs/filtershow_strings.xml b/res/values-cs/filtershow_strings.xml
new file mode 100644
index 0000000..e3889d0
--- /dev/null
+++ b/res/values-cs/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor fotografií"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Obrázek nelze načíst!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Nastavování tapety"</string>
+    <string name="original" msgid="3524493791230430897">"Původní"</string>
+    <string name="borders" msgid="2067345080568684614">"Okraje"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Vrátit zpět"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Opakovat"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Zobrazit historii"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Skrýt historii"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Ukázat stav obrázku"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Skrýt stav obrázku"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Nastavení"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Obrázek obsahuje neuložené změny."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Chcete před ukončením uložit změny?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Uložit a ukončit"</string>
+    <string name="exit" msgid="242642957038770113">"Ukončit"</string>
+    <string name="history" msgid="455767361472692409">"Historie"</string>
+    <string name="reset" msgid="9013181350779592937">"Obnovit"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Použité efekty"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Porovnat"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Použít"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Obnovit"</string>
+    <string name="aspect" msgid="4025244950820813059">"Poměr stran"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Žádný"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Pevné"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Malá planeta"</string>
+    <string name="exposure" msgid="6526397045949374905">"Expozice"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ostrost"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Živost"</string>
+    <string name="saturation" msgid="7026791551032438585">"Sytost"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtr ČB"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autom. barva"</string>
+    <string name="hue" msgid="6231252147971086030">"Odstín"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Stíny"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Světlá místa"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Křivky"</string>
+    <string name="vignette" msgid="934721068851885390">"Viněta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Červené oči"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Kresby"</string>
+    <string name="straighten" msgid="26025591664983528">"Vyrovnat"</string>
+    <string name="crop" msgid="5781263790107850771">"Oříznutí"</string>
+    <string name="rotate" msgid="2796802553793795371">"Otočit"</string>
+    <string name="mirror" msgid="5482518108154883096">"Zrcadlit"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Žádný"</string>
+    <string name="edge" msgid="7036064886242147551">"Hrany"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Zmenšit"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Červená"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Zelená"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Modrá"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Styl"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Velikost"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Barva"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Čáry"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Zvýrazňovač"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Cákance"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Vymazat"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Zvolit vlastní barvu"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Vyberte barvu"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Vyberte velikost"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
new file mode 100644
index 0000000..b6265cc
--- /dev/null
+++ b/res/values-cs/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerie"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Rámeček fotografie"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Přehrávač videa"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Načítání videa..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Načítání obrázku..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Načítání účtu…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Pokračovat v přehrávání videa"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Pokračovat v přehrávání od %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Pokračovat v přehrávání"</string>
+    <string name="loading" msgid="7038208555304563571">"Načítání..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nelze načíst"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Obrázek nelze načíst"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Miniatura není dostupná"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Začít znovu"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Začněte klepnutím na obličej."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Ukládání fotografie..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Ukládání obrázku do alba <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Oříznutý obrázek nelze uložit."</string>
+    <string name="crop_label" msgid="521114301871349328">"Oříznout fotografii"</string>
+    <string name="trim_label" msgid="274203231381209979">"Oříznout video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Vybrat fotografii"</string>
+    <string name="select_video" msgid="4859510992798615076">"Vybrat video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Vybrat položku"</string>
+    <string name="select_album" msgid="1557063764849434077">"Vybrat album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Vybrat skupinu"</string>
+    <string name="set_image" msgid="2331476809308010401">"Nastavit fotografii jako"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nastavit tapetu"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Nastavování tapety..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
+    <string name="delete" msgid="2839695998251824487">"Smazat"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Smazat vybranou položku?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Smazat vybrané položky?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potvrdit"</string>
+    <string name="cancel" msgid="3637516880917356226">"Zrušit"</string>
+    <string name="share" msgid="3619042788254195341">"Sdílet"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Sdílet panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Sdílet jako fotku"</string>
+    <string name="deleted" msgid="6795433049119073871">"Smazáno"</string>
+    <string name="undo" msgid="2930873956446586313">"VRÁTIT ZPĚT"</string>
+    <string name="select_all" msgid="3403283025220282175">"Vybrat vše"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Zrušit výběr všech položek"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Prezentace"</string>
+    <string name="details" msgid="8415120088556445230">"Podrobnosti"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d z %2$d položek:"</string>
+    <string name="close" msgid="5585646033158453043">"Zavřít"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Přepnout do režimu Fotoaparát"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Vybráno: %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Vybráno: %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Vybráno: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Vybráno: %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Vybráno: %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Vybráno: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Vybráno: %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Vybráno: %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Vybráno: %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Zobrazit na mapě"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Otočit doleva"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Otočit doprava"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Položku nelze najít."</string>
+    <string name="edit" msgid="1502273844748580847">"Upravit"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Zpracování požadavků na uložení do mezipaměti"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Ukládání do mezipaměti..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Oříznout"</string>
+    <string name="trim_action" msgid="703098114452883524">"Oříznout"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Ztlumit"</string>
+    <string name="set_as" msgid="3636764710790507868">"Nastavit jako"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Video nelze ztlumit."</string>
+    <string name="video_err" msgid="7003051631792271009">"Video nelze přehrát."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Podle místa"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Podle času"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Podle tagů"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Podle lidí"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Podle alba"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Podle velikosti"</string>
+    <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string>
+    <string name="no_location" msgid="4043624857489331676">"Neznámá poloha"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Některá umístění se nepodařilo identifikovat kvůli problémům se sítí."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Fotografie v tomto albu nelze stáhnout. Zkuste to znovu později."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Pouze obrázky"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Pouze videa"</string>
+    <string name="show_all" msgid="6963292714584735149">"Obrázky a videa"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galerie fotografií"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Žádné fotky."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Ořezaný snímek byl uložen do složky <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Žádné album není k dispozici."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Žádné obrázky ani videa nejsou k dispozici."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Příspěvky"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Zpřístupnit offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Aktualizovat"</string>
+    <string name="done" msgid="217672440064436595">"Hotovo"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d z %2$d položek:"</string>
+    <string name="title" msgid="7622928349908052569">"Název"</string>
+    <string name="description" msgid="3016729318096557520">"Popis"</string>
+    <string name="time" msgid="1367953006052876956">"Čas"</string>
+    <string name="location" msgid="3432705876921618314">"Místo"</string>
+    <string name="path" msgid="4725740395885105824">"Cesta"</string>
+    <string name="width" msgid="9215847239714321097">"Šířka"</string>
+    <string name="height" msgid="3648885449443787772">"Výška"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientace"</string>
+    <string name="duration" msgid="8160058911218541616">"Délka"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Typ MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Velikost souboru"</string>
+    <string name="maker" msgid="7921835498034236197">"Tvůrce"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Blesk"</string>
+    <string name="aperture" msgid="5920657630303915195">"Clona"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Ohnisk. vzdálenost"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Vyvážení bílé"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Expoziční čas"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Ručně"</string>
+    <string name="auto" msgid="4296941368722892821">"Autom."</string>
+    <string name="flash_on" msgid="7891556231891837284">"S bleskem"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Bez blesku"</string>
+    <string name="unknown" msgid="3506693015896912952">"Neznámé"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Původní"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Klasika"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instantní"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bělení"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Modré"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Černobílé"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Děrování"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Cross process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latté"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografie"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Zpřístupňování alba offline"</item>
+    <item quantity="other" msgid="4948604338155959389">"Zpřístupnění alb offline"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Tato položka je uložena v místním úložišti a je k dispozici offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Všechna alba"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Místní alba"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Zařízení MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Alba Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Volná paměť: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> nebo menší"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> nebo větší"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> až <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importovat"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Import byl dokončen"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Import se nezdařil"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Fotoaparát byl připojen."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Fotoaparát byl odpojen."</string>
+    <string name="click_import" msgid="6407959065464291972">"Chcete-li zahájit import, dotkněte se zde"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Vybrat album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Náhodně všechny obrázky"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Vybrat obrázek"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Výběr obrázků"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Prezentace"</string>
+    <string name="albums" msgid="7320787705180057947">"Alba"</string>
+    <string name="times" msgid="2023033894889499219">"Časy"</string>
+    <string name="locations" msgid="6649297994083130305">"Lokality"</string>
+    <string name="people" msgid="4114003823747292747">"Lidé"</string>
+    <string name="tags" msgid="5539648765482935955">"Tagy"</string>
+    <string name="group_by" msgid="4308299657902209357">"Seskupit podle..."</string>
+    <string name="settings" msgid="1534847740615665736">"Nastavení"</string>
+    <string name="add_account" msgid="4271217504968243974">"Přidat účet"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparát"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Stahování"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Upravené fotografie online"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importováno"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Snímky obrazovky"</string>
+    <string name="help" msgid="7368960711153618354">"Nápověda"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Žádné úložiště"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Žádné externí úložiště není k dispozici"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Zobrazení filmového pásu"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Mřížkové zobrazení"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Celá obrazovka"</string>
+    <string name="trimming" msgid="9122385768369143997">"Zkracování"</string>
+    <string name="muting" msgid="5094925919589915324">"Probíhá ztlumení"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Čekejte prosím"</string>
+    <string name="save_into" msgid="9155488424829609229">"Ukládání videa do alba <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Zkrácení nelze provést: výsledné video je příliš krátké"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Vykreslování panoramatu"</string>
+    <string name="save" msgid="613976532235060516">"Uložit"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Skenování obsahu..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"počet skenovaných položek: %1$d"</item>
+    <item quantity="one" msgid="4340019444460561648">"počet skenovaných položek: %1$d"</item>
+    <item quantity="other" msgid="3138021473860555499">"počet skenovaných položek: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Řazení..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skenování dokončeno"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Probíhá import..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Není k dispozici žádný obsah pro import do tohoto zařízení."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Není připojeno žádné zařízení MTP."</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Chyba fotoaparátu"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Nelze se připojit k fotoaparátu."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Fotoaparát byl z důvodu zásad zabezpečení deaktivován."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Fotoaparát"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Čekejte prosím..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Před použitím fotoaparátu připojte úložiště USB."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Před použitím fotoaparátu prosím vložte SD kartu."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Příprava úložiště USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Příprava karty SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nelze získat přístup k úložišti USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nelze získat přístup ke kartě SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ZRUŠIT"</string>
+    <string name="review_ok" msgid="1156261588693116433">"HOTOVO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Časosběrný záznam"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Zvolit fotoaparát"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Zadní"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Přední"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Úložiště"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Časovač odpočítávání"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 s"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d s"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Zvuk při odpočtu"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Vypnuto"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Zapnuto"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Kvalita videa"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Vysoká"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Nízká"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Časosběr"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Nastavení fotoaparátu"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Nastavení videokamery"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Velikost fotografií"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapixelů"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 Mpx"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 Mpx"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 Mpx"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 Mpx"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 Mpx"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Režim zaostření"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Nekonečno"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Režim blesku"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automaticky"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Zapnout"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Vypnout"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Vyvážení bílé"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automaticky"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Žárovka"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Denní světlo"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Zářivka"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Zataženo"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scénický režim"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automaticky"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Akce"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Noc"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Západ slunce"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Párty"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Tuto možnost nelze ve scénickém režimu vybrat."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Expozice"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"V úložišti USB je málo místa. Změňte nastavení kvality nebo smažte některé fotografie či jiné soubory."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Na kartě SD je málo místa. Změňte nastavení kvality nebo smažte některé obrázky či jiné soubory."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Bylo dosaženo limitu velikosti."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Moc rychle"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Příprava panoramatu"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panoramatickou fotku nelze uložit."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Snímání panoramatu"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Čeká se na předchozí panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Ukládání..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Vykreslování panoramatu"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Dotykem zaostříte."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efekty"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Žádný"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Zmáčknout"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Velké oči"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Velká ústa"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Malá ústa"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Velký nos"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Malé oči"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Ve vesmíru"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Západ slunce"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Vaše video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Položte zařízení."\n"Vystupte na chvíli ze zorného pole."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Dotykem v průběhu nahrávání pořídíte snímek."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Záznam videa byl zahájen."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Záznam videa byl zastaven."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Jsou-li zapnuty zvláštní efekty, jsou snímky videa zakázány."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Vymazat efekty"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"BLÁZNIVÉ TVÁŘE"</string>
+    <string name="effect_background" msgid="6579360207378171022">"POZADÍ"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Tlačítko závěrky"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Tlačítko Menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Poslední fotografie"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Přepínač mezi předním a zadním fotoaparátem"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Přepínač mezi panoramatickým režimem a režimy fotoaparátu a videokamery"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Další ovládací prvky nastavení"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Zavřít ovládací prvky nastavení"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Ovládání přiblížení/oddálení"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Snížit %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Zvýšit %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s zaškrtávací políčko"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Přepnout na fotoaparát"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Přepnout na video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Přepnout do panoramatického režimu"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Přepnout do nového panoramatického režimu"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Zrušit"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Hotovo"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Pořídit další"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ZAPNUTO"</string>
+    <string name="capital_off" msgid="7231052688467970897">"VYPNUTO"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Vypnuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekunda"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hodina"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 hodin"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 hodin"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 hodin"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 hodin"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 hodin"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 hodin"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekundy"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minuty"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"hodiny"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Hotovo"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Nastavit časový interval"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Funkce časosběr je vypnutá. Zapněte ji a nastavte časový interval."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Časovač odpočítávání je vypnutý. Chcete-li odpočítávat do aktivace spouště fotoaparátu, zapněte jej."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Nastavit dobu v sekundách"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Odpočítávání spouště fotoaparátu"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Pamatovat, kde byly fotky pořízeny?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Přidejte do fotek a videí označení míst, kde jste je pořídili."\n\n"Ostatní aplikace budou mít k těmto informacím přístup společně s přístupem k uloženým obrázkům."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Ne, děkuji"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ano"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Fotoaparát"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Hledat"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotky"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Alba"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotka"</item>
+    <item quantity="other" msgid="3813306834113858135">"Fotky: %1$d"</item>
+  </plurals>
+</resources>
diff --git a/res/values-da/filtershow_strings.xml b/res/values-da/filtershow_strings.xml
new file mode 100644
index 0000000..c120489
--- /dev/null
+++ b/res/values-da/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fotoredigeringsværktøj"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Billedet kan ikke indlæses."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Angiver baggrund"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Rammer"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Fortryd"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Annuller fortryd"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Vis historik"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Skjul historik"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Vis billedtilstand"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Skjul billedtilstand"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Indstillinger"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Der er ændringer på dette billede, som ikke er gemt."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Vil du gemme, før du afslutter?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Gem og afslut"</string>
+    <string name="exit" msgid="242642957038770113">"Afslut"</string>
+    <string name="history" msgid="455767361472692409">"Historik"</string>
+    <string name="reset" msgid="9013181350779592937">"Nulstil"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Anvendte effekter"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Sammenlign"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Anvend"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Nulstil"</string>
+    <string name="aspect" msgid="4025244950820813059">"Billedformat"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ingen"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fastlåst"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Eksponering"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Skarphed"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Livlighed"</string>
+    <string name="saturation" msgid="7026791551032438585">"Mætning"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"BW Filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Automatisk farve"</string>
+    <string name="hue" msgid="6231252147971086030">"Nuance"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Skygger"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Fremhævninger"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kurver"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignet"</string>
+    <string name="redeye" msgid="4508883127049472069">"Røde øjne"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Tegn"</string>
+    <string name="straighten" msgid="26025591664983528">"Ret op"</string>
+    <string name="crop" msgid="5781263790107850771">"Beskær"</string>
+    <string name="rotate" msgid="2796802553793795371">"Roter"</string>
+    <string name="mirror" msgid="5482518108154883096">"Spejl"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Ingen"</string>
+    <string name="edge" msgid="7036064886242147551">"Kanter"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Reducer"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rød"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Grøn"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blå"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Størrelse"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Farve"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linjer"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Tusch"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Stænk"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Ryd"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Vælg tilpasset farve"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Vælg farve"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Vælg størrelse"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
new file mode 100644
index 0000000..c325b81
--- /dev/null
+++ b/res/values-da/strings.xml
@@ -0,0 +1,398 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleri"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Billedramme"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videoafspiller"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Indlæser video ..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Indlæser billede..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Indlæser konto..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Genoptag video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Genoptag afspilning fra %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Genoptag afspilning"</string>
+    <string name="loading" msgid="7038208555304563571">"Indlæser..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Kunne ikke indlæses"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Billedet kunne ikke indlæses"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniature"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Start igen"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Tryk på et ansigt for at begynde."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Gemmer billede..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Billedet gemmes i <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Det beskårne billede kunne ikke gemmes."</string>
+    <string name="crop_label" msgid="521114301871349328">"Beskær billede"</string>
+    <string name="trim_label" msgid="274203231381209979">"Beskær video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Vælg foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Vælg video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Vælg element"</string>
+    <string name="select_album" msgid="1557063764849434077">"Vælg album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Vælg gruppe"</string>
+    <string name="set_image" msgid="2331476809308010401">"Angiv billedet som"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Angiv baggrund"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Angiver baggrund..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapet"</string>
+    <string name="delete" msgid="2839695998251824487">"Slet"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vil du slette det valgte element?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vil du slette valgte elementer?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bekræft"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annuller"</string>
+    <string name="share" msgid="3619042788254195341">"Del"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Del panoramabillede"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Del som foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Slettet"</string>
+    <string name="undo" msgid="2930873956446586313">"FORTRYD"</string>
+    <string name="select_all" msgid="3403283025220282175">"Markér alle"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Fjern markering af alle"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diasshow"</string>
+    <string name="details" msgid="8415120088556445230">"Detaljer"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d ud af %2$d emner:"</string>
+    <string name="close" msgid="5585646033158453043">"Luk"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Skift til kamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d valgt"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d valgt"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d valgt"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d valgt"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d valgt"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d valgt"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d valgt"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d valgt"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d valgt"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Vis på kort"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Roter til venstre"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Roter til højre"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Elementet blev ikke fundet."</string>
+    <string name="edit" msgid="1502273844748580847">"Rediger"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Håndterer anmodninger om cachelagring"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Cachelagrer..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Beskær"</string>
+    <string name="trim_action" msgid="703098114452883524">"Beskær"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Slå lyd fra"</string>
+    <string name="set_as" msgid="3636764710790507868">"Indstil som"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Videolyden kan ikke slås fra."</string>
+    <string name="video_err" msgid="7003051631792271009">"Videoen kan ikke afspilles."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Efter placering"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Efter tid"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Efter tags"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Efter mennesker"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Efter album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Efter størrelse"</string>
+    <string name="untagged" msgid="7281481064509590402">"Utagget"</string>
+    <string name="no_location" msgid="4043624857489331676">"Ingen placering"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Nogle placeringer kunne ikke identificeres på grund af netværksproblemer."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Billederne i dette album kunne ikke hentes. Prøv igen senere."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Kun billeder"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Kun videoer"</string>
+    <string name="show_all" msgid="6963292714584735149">"Billeder og videoer"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Billedgalleri"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Ingen billeder."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Det beskårne billede er gemt i <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Ingen tilgængelige albummer."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O tilgængelige billeder/videoer."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Indlæg"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Gør tilgængelig offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Opdater"</string>
+    <string name="done" msgid="217672440064436595">"Udført"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d ud af %2$d enheder:"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="description" msgid="3016729318096557520">"Beskrivelse"</string>
+    <string name="time" msgid="1367953006052876956">"Tid"</string>
+    <string name="location" msgid="3432705876921618314">"Placering"</string>
+    <string name="path" msgid="4725740395885105824">"Sti:"</string>
+    <string name="width" msgid="9215847239714321097">"Bredde"</string>
+    <string name="height" msgid="3648885449443787772">"Højde"</string>
+    <string name="orientation" msgid="4958327983165245513">"Retning"</string>
+    <string name="duration" msgid="8160058911218541616">"Varighed"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-type"</string>
+    <string name="file_size" msgid="8486169301588318915">"Filstørrelse"</string>
+    <string name="maker" msgid="7921835498034236197">"Fremstiller"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Blitz"</string>
+    <string name="aperture" msgid="5920657630303915195">"Blænderåbning"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Fokallængde"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Hvidbalance"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Eksponeringstid"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manuel"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Blitz affyret"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Ingen blitz"</string>
+    <string name="unknown" msgid="3506693015896912952">"Ukendt"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blå"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Sort/hvid"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Gør albummet tilgængeligt offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Gør albummer tilgængelige offline."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dette element er gemt lokalt og er tilgængeligt offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Alle albummer"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Lokale albummer"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-enheder"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa-albummer"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ledig"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> eller mindre"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> eller over"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> til <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importer"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importen er fuldført"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Importen mislykkedes"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kameraet er tilkoblet."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kameraet er frakoblet."</string>
+    <string name="click_import" msgid="6407959065464291972">"Tryk her for at importere"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Vælg et album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Bland alle billeder"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Vælg et billede"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Vælg billeder"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Diasshow"</string>
+    <string name="albums" msgid="7320787705180057947">"Albummer"</string>
+    <string name="times" msgid="2023033894889499219">"Tidspunkter"</string>
+    <string name="locations" msgid="6649297994083130305">"Placering"</string>
+    <string name="people" msgid="4114003823747292747">"Personer"</string>
+    <string name="tags" msgid="5539648765482935955">"Tags"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupper efter"</string>
+    <string name="settings" msgid="1534847740615665736">"Indstillinger"</string>
+    <string name="add_account" msgid="4271217504968243974">"Tilføj konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Download"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Redigerede onlinefotos"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importeret"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skærmbillede"</string>
+    <string name="help" msgid="7368960711153618354">"Hjælp"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Intet lager"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Intet tilgængeligt eksternt lager"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmstrip-visning"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Gittervisning"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Se i fuld skærm"</string>
+    <string name="trimming" msgid="9122385768369143997">"Beskæring"</string>
+    <string name="muting" msgid="5094925919589915324">"Slår lyden fra"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Vent"</string>
+    <string name="save_into" msgid="9155488424829609229">"Videoen gemmes i <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Der kan ikke beskæres – målvideoen er for kort"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panoramabilledet gengives"</string>
+    <string name="save" msgid="613976532235060516">"Gem"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Indhold scannes..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elementer er scannet"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d element er scannet"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d elementer er scannet"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sorterer..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Scanning er fuldført"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importerer..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Denne enhed indeholder intet indhold, der kan importeres."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Der er ingen MTP-enhed tilsluttet"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kamerafejl"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Der kan ikke oprettes forbindelse til kameraet."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kameraet er deaktiveret på grund af sikkerhedspolitikker."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Vent..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Isæt USB-lager, før du tager kameraet i brug."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Indsæt et SD-kort, før kameraet tages i brug."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Forbereder USB-lager…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Forbereder SD-kort ..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Der kunne ikke fås adgang til USB-lagring."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Der kunne ikke fås adgang til SD-kortet."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ANNULLER"</string>
+    <string name="review_ok" msgid="1156261588693116433">"FÆRDIG"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Optagelse af tidsforløb"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Vælg kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Bagest"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Forrest"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Gem placering"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Nedtællingsur"</string>
+    <!-- String.format failed for translation -->
+    <!-- no translation found for pref_camera_timer_entry:other (6455381617076792481) -->
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Beep under nedtællingen"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Fra"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Til"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videokvalitet"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Høj"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Lav"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Tidsforløb"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Indstillinger for kamera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Indstillinger for videokamera"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Billedstørrelse"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fokustilstand"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatisk"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Uendelighed"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Blitztilstand"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatisk"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Til"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Fra"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Hvidbalance"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatisk"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Blødt lys"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Dagslys"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Hårdt lys"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Overskyet"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scenetilstand"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatisk"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Bevægelse"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nat"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Solnedgang"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Fest"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Kan ikke vælges i motivtilstand."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Eksponering"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Der er snart ikke mere plads i USB-lager. Rediger indstillingerne for kvalitet, eller slet nogle billeder eller andre filer."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Der er snart ikke mere plads på dit SD-kort. Rediger indstillingerne for kvalitet, eller slet nogle billeder eller andre filer."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Størrelsesgrænse er nået."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"For hurtig"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Forbereder panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panorama kunne ikke gemmes."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Optagelse af panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Venter på et tidligere panoramabillede"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Gemmer..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panoramabilledet gengives"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Tryk for at fokusere."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effekter"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ingen"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Klem"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Store øjne"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Stor mund"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Lille mund"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Stor næse"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Små øjne"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"I rummet"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Solnedgang"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Din video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Læg din enhed ned."\n"Træd ud af syne et øjeblik."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Tryk for at tage et foto, mens du optager."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Videooptagelsen er startet."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Videooptagelsen er stoppet."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Videoøjebliksbilleder deaktiveres, når specialeffekter er tændt."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Ryd effekter"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"FJOLLEDE ANSIGTER"</string>
+    <string name="effect_background" msgid="6579360207378171022">"BAGGRUND"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Lukkerknap"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menu-knap"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Det seneste foto"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Skift mellem frontkameraet og bagsidekameraet"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Valg af kamera, video eller panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Flere indstillingskontroller"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Luk indstillingskontroller"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoomkontrol"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Formindsk %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Forøg %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Afkrydsningsfeltet for %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Skift til foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Skift til video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Skift til panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Skift til nyt panorama"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Annullering af gennemgang"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Gennemgang fuldført"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Lav foto/video til anmeldelsen om"</string>
+    <string name="capital_on" msgid="5491353494964003567">"TIL"</string>
+    <string name="capital_off" msgid="7231052688467970897">"FRA"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Fra"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 time"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 timer"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekunder"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutter"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"timer"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Udført"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Angiv tidsinterval"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Funktionen Tidsforløb er deaktiveret. Aktivér den for at indstille tidsintervallet."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Nedtællingsur er slukket. Slå den til at tælle ned, før du tager et billede."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Indstil varigheden i sekunder"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Tælle ned for at tage et foto"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Skal fotosteder huskes?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Tag dine fotos og videoer med de placeringer, hvor de blev taget."\n\n"Andre apps kan få adgang til disse oplysninger sammen med dine gemte billeder."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nej tak"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ja"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Søg"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albummer"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d billede"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d billeder"</item>
+  </plurals>
+</resources>
diff --git a/res/values-de/filtershow_strings.xml b/res/values-de/filtershow_strings.xml
new file mode 100644
index 0000000..037abc2
--- /dev/null
+++ b/res/values-de/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fotoeditor"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Bild kann nicht geladen werden."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Hintergrund wird festgelegt..."</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Rahmen"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Rückgängig machen"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Wiederholen"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Verlauf anzeigen"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Verlauf ausblenden"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Bildstatus anzeigen"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Bildstatus ausblenden"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Einstellungen"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Dieses Bild weist nicht gespeicherte Änderungen auf."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Möchten Sie vor dem Schließen speichern?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Speichern und schließen"</string>
+    <string name="exit" msgid="242642957038770113">"Schließen"</string>
+    <string name="history" msgid="455767361472692409">"Verlauf"</string>
+    <string name="reset" msgid="9013181350779592937">"Zurücksetzen"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Angewendete Effekte"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Vergleichen"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Übernehmen"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Zurücksetzen"</string>
+    <string name="aspect" msgid="4025244950820813059">"Seitenverhältnis"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Kein Effekt"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fest"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Belichtung"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Schärfe"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Farbsättigung"</string>
+    <string name="saturation" msgid="7026791551032438585">"Sättigung"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"SW-Filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autom. Farbe"</string>
+    <string name="hue" msgid="6231252147971086030">"Farbton"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Schatten"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Highlights"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kurven"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignettierung"</string>
+    <string name="redeye" msgid="4508883127049472069">"Rote Augen"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Zeichnen"</string>
+    <string name="straighten" msgid="26025591664983528">"Ausrichten"</string>
+    <string name="crop" msgid="5781263790107850771">"Zuschneiden"</string>
+    <string name="rotate" msgid="2796802553793795371">"Drehen"</string>
+    <string name="mirror" msgid="5482518108154883096">"Spiegeln"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Keines"</string>
+    <string name="edge" msgid="7036064886242147551">"Kanten"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Verkleinern"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rot"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Grün"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blau"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Größe"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Farbe"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linien"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Filzstift"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Spritzer"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Löschen"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Benutzerdefinierte Farbe wählen"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Farbe auswählen"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Größe auswählen"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
new file mode 100644
index 0000000..e052719
--- /dev/null
+++ b/res/values-de/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerie"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Bildrahmen"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Google Video Player"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Video wird geladen..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Bild wird geladen…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Konto wird geladen..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Mit Video fortfahren"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Mit Wiedergabe fortfahren ab %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Mit Wiedergabe fortfahren"</string>
+    <string name="loading" msgid="7038208555304563571">"Wird geladen..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Konnte nicht geladen werden."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Bild konnte nicht geladen werden."</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Keine Miniaturansicht"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Starten"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Zum Beginnen auf ein Gesicht tippen"</string>
+    <string name="saving_image" msgid="7270334453636349407">"Bild wird gespeichert..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Bild wird in <xliff:g id="ALBUM_NAME">%1$s</xliff:g> gespeichert…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Zugeschnittenes Bild nicht gespeichert"</string>
+    <string name="crop_label" msgid="521114301871349328">"Bild zuschneiden"</string>
+    <string name="trim_label" msgid="274203231381209979">"Video schneiden"</string>
+    <string name="select_image" msgid="7841406150484742140">"Foto auswählen"</string>
+    <string name="select_video" msgid="4859510992798615076">"Video auswählen"</string>
+    <string name="select_item" msgid="2816923896202086390">"Bild auswählen"</string>
+    <string name="select_album" msgid="1557063764849434077">"Album auswählen"</string>
+    <string name="select_group" msgid="6744208543323307114">"Gruppe auswählen"</string>
+    <string name="set_image" msgid="2331476809308010401">"Bild festlegen als"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Hintergrund wählen"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Hintergrund wird festgelegt..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Hintergrund"</string>
+    <string name="delete" msgid="2839695998251824487">"Löschen"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ausgewähltes Element löschen?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Ausgewählte Elemente löschen?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bestätigen"</string>
+    <string name="cancel" msgid="3637516880917356226">"Abbrechen"</string>
+    <string name="share" msgid="3619042788254195341">"Teilen"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Panorama teilen"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Als Foto teilen"</string>
+    <string name="deleted" msgid="6795433049119073871">"Gelöscht"</string>
+    <string name="undo" msgid="2930873956446586313">"Rückgängig"</string>
+    <string name="select_all" msgid="3403283025220282175">"Alle auswählen"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Auswahl für alle aufheben"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diashow"</string>
+    <string name="details" msgid="8415120088556445230">"Details"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d von %2$d Elementen:"</string>
+    <string name="close" msgid="5585646033158453043">"Schließen"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Zu Kamera wechseln"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d ausgewählt"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d ausgewählt"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d ausgewählt"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d ausgewählt"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d ausgewählt"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d ausgewählt"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d ausgewählt"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d ausgewählt"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d ausgewählt"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Auf Karte anzeigen"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Nach links drehen"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Nach rechts drehen"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Bild wurde nicht gefunden."</string>
+    <string name="edit" msgid="1502273844748580847">"Bearbeiten"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Caching-Anfragen werden verarbeitet."</string>
+    <string name="caching_label" msgid="4521059045896269095">"Caching läuft..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Zuschneiden"</string>
+    <string name="trim_action" msgid="703098114452883524">"Zuschneiden"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Ton aus"</string>
+    <string name="set_as" msgid="3636764710790507868">"Festlegen als"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Ton aus nicht möglich"</string>
+    <string name="video_err" msgid="7003051631792271009">"Video kann nicht wiedergegeben werden."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Nach Standort"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Nach Zeit"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Nach Tags"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Nach Personen"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Nach Album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Nach Größe"</string>
+    <string name="untagged" msgid="7281481064509590402">"Nicht getaggt"</string>
+    <string name="no_location" msgid="4043624857489331676">"Kein Aufnahmeort"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Einige Standorte konnten aufgrund von Netzwerkproblemen nicht identifiziert werden."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Die Fotos aus diesem Album konnten nicht heruntergeladen werden. Bitte versuchen Sie es später erneut."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Nur Bilder"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Nur Videos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Bilder und Videos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerie"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Keine Fotos vorhanden"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Zugeschnittenes Bild gespeichert in <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Keine Alben verfügbar"</string>
+    <string name="empty_album" msgid="4542880442593595494">"Keine Bilder oder Videos vorhanden"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Posts"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Offline bereitstellen"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Aktualisieren"</string>
+    <string name="done" msgid="217672440064436595">"Fertig"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d von %2$d Elementen:"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="description" msgid="3016729318096557520">"Beschreibung"</string>
+    <string name="time" msgid="1367953006052876956">"Uhrzeit"</string>
+    <string name="location" msgid="3432705876921618314">"Ort"</string>
+    <string name="path" msgid="4725740395885105824">"Pfad"</string>
+    <string name="width" msgid="9215847239714321097">"Breite"</string>
+    <string name="height" msgid="3648885449443787772">"Höhe"</string>
+    <string name="orientation" msgid="4958327983165245513">"Ausrichtung"</string>
+    <string name="duration" msgid="8160058911218541616">"Dauer"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-Typ"</string>
+    <string name="file_size" msgid="8486169301588318915">"Dateigröße"</string>
+    <string name="maker" msgid="7921835498034236197">"Hersteller"</string>
+    <string name="model" msgid="8240207064064337366">"Modell"</string>
+    <string name="flash" msgid="2816779031261147723">"Blitz"</string>
+    <string name="aperture" msgid="5920657630303915195">"Blende"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Brennweite"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Weißabgleich"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Einblendungsdauer"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manuell"</string>
+    <string name="auto" msgid="4296941368722892821">"Automatisch"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Blitz ausgelöst"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Ohne Blitz"</string>
+    <string name="unknown" msgid="3506693015896912952">"Unbekannt"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blau"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Schwarz-Weiß"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Album wird offline bereitgestellt."</item>
+    <item quantity="other" msgid="4948604338155959389">"Alben werden offline bereitgestellt."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dieses Element ist lokal gespeichert und offline verfügbar."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Alle Alben"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Lokale Alben"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-Geräte"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa-Alben"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> verfügbar"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> oder kleiner"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> oder größer"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> bis <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importieren"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Import abgeschlossen"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Import fehlgeschlagen"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera verbunden"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera nicht verbunden"</string>
+    <string name="click_import" msgid="6407959065464291972">"Zum Importieren hier berühren"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Album auswählen"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Zufallsauswahl"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Bild auswählen"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Bilder auswählen"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Diashow"</string>
+    <string name="albums" msgid="7320787705180057947">"Alben"</string>
+    <string name="times" msgid="2023033894889499219">"Zeiten"</string>
+    <string name="locations" msgid="6649297994083130305">"Orte"</string>
+    <string name="people" msgid="4114003823747292747">"Personen"</string>
+    <string name="tags" msgid="5539648765482935955">"Tags"</string>
+    <string name="group_by" msgid="4308299657902209357">"Gruppieren nach"</string>
+    <string name="settings" msgid="1534847740615665736">"Einstellungen"</string>
+    <string name="add_account" msgid="4271217504968243974">"Konto hinzufügen"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Downloads"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Bearbeitete Online-Fotos"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importiert"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshots"</string>
+    <string name="help" msgid="7368960711153618354">"Hilfe"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Kein Speicher"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Kein externer Speicher verfügbar"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmstreifen-Ansicht"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Rasteransicht"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Vollbildansicht"</string>
+    <string name="trimming" msgid="9122385768369143997">"Wird zugeschnitten"</string>
+    <string name="muting" msgid="5094925919589915324">"Ton wird ausgeschaltet..."</string>
+    <string name="please_wait" msgid="7296066089146487366">"Bitte warten"</string>
+    <string name="save_into" msgid="9155488424829609229">"Video wird in <xliff:g id="ALBUM_NAME">%1$s</xliff:g> gespeichert…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Zuschneiden nicht möglich: Ziel-Video zu kurz"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panorama wird gerendert..."</string>
+    <string name="save" msgid="613976532235060516">"Speichern"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Inhalt wird gescannt..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d Elemente gescannt"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d Element gescannt"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d Elemente gescannt"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Wird sortiert..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Scan abgeschlossen"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Wird importiert..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Es sind keine Inhalte zum Importieren auf dieses Gerät vorhanden."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Es ist kein MTP-Gerät angeschlossen."</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kamerafehler"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Keine Verbindung zur Kamera möglich"</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Die Kamera wurde aufgrund von Sicherheitsrichtlinien deaktiviert."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Camcorder"</string>
+    <string name="wait" msgid="8600187532323801552">"Bitte warten..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Stellen Sie vor Verwendung der Kamera einen USB-Speicher bereit."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Legen Sie vor Verwendung der Kamera eine SD-Karte ein."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB-Speicher wird vorbereitet"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD-Karte wird vorbereitet..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Kein Zugriff auf USB-Speicher möglich"</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Kein Zugriff auf SD-Karte möglich"</string>
+    <string name="review_cancel" msgid="8188009385853399254">"Abbrechen"</string>
+    <string name="review_ok" msgid="1156261588693116433">"Fertig"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Zeitrafferaufnahme"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Kamera wählen"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Rückseite"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Vorderseite"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Ort speichern"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Countdown-Timer"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 Sekunde"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d Sekunden"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Piepton während Countdown"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Aus"</string>
+    <string name="setting_on" msgid="8602246224465348901">"An"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videoqualität"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Hoch"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Niedrig"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Zeitraffer"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kameraeinstellungen"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Camcordereinstellungen"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Bildgröße"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 Megapixel"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 Megapixel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 Megapixel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 Megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 Megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 Megapixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fokussierungsmodus"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatisch"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Unendlich"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Blitzmodus"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatisch"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"An"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Aus"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Weißabgleich"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatisch"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Glühlampenlicht"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Tageslicht"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Neonlicht"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Bewölkt"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Szenenmodus"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatisch"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Action"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nachtaufnahme"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Sonnenuntergang"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Party"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Diese Einstellung kann im Szenenmodus nicht ausgewählt werden."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Belichtung"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Ihr USB-Speicher bietet nicht mehr genug Speicherplatz. Ändern Sie die Qualitätseinstellung oder löschen Sie Bilder oder andere Dateien."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Auf Ihrer SD-Karte ist nicht mehr genügend Speicherplatz vorhanden. Ändern Sie die Qualitätseinstellung oder löschen Sie Bilder oder andere Dateien."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Maximale Größe erreicht"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Zu schnell"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Panorama wird vorbereitet..."</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panorama konnte nicht gespeichert werden."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Panorama wird aufgenommen..."</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Warten auf vorheriges Panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Speichern..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panorama wird gerendert..."</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Zum Fokussieren tippen"</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effekte"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Effekt löschen"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Quetschen"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Große Augen"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Großer Mund"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Kleiner Mund"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Große Nase"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Kleine Augen"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Im All"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Sonnenuntergang"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Mein Video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Legen Sie Ihr Gerät ab."\n"Treten Sie für einen kurzen Moment aus dem Aufnahmebereich."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Zum Erstellen eines Fotos während der Aufnahme tippen"</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Videoaufzeichnung wurde gestartet."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Videoaufzeichnung wurde beendet."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Video-Schnappschuss ist bei Spezialeffekten deaktiviert."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Effekte löschen"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"Lustige Gesichter"</string>
+    <string name="effect_background" msgid="6579360207378171022">"Hintergrund"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Auslöseknopf"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menüschaltfläche"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Neuestes Foto"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Zwischen Kamera auf der Vorder- und Rückseite wechseln"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Auswahl: Kamera, Video oder Panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Weitere Einstellungen"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Einstellungen schließen"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoom-Steuerung"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Verkleinern %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Vergrößern %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Kontrollkästchen \"%1$s\""</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"In Kamera-Modus wechseln"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"In Video-Modus wechseln"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"In Panorama-Modus wechseln"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Zum neuen Panorama wechseln"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Überprüfung abbrechen"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Überprüfung abgeschlossen"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Überprüfen – erneut aufnehmen"</string>
+    <string name="capital_on" msgid="5491353494964003567">"An"</string>
+    <string name="capital_off" msgid="7231052688467970897">"Aus"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Aus"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 Sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 Sekunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 Minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 Minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 Stunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 Stunden"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 Stunden"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"Sekunden"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"Minuten"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"Stunden"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Fertig"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Zeitintervall festlegen"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Die Zeitraffer-Funktion ist ausgeschaltet. Schalten Sie sie ein, um das Zeitintervall festzulegen."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Der Countdown-Timer ist deaktiviert. Aktivieren Sie ihn, damit vor der Aufnahme eines Bildes ein Countdown erfolgt."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Dauer in Sekunden festlegen"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Countdown für Fotoaufnahme"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Aufnahmeorte für Fotos speichern?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Taggen Sie Ihre Fotos und Videos mit den Standorten, an denen sie aufgenommen wurden."\n\n"Andere Apps können auf diese Informationen und Ihre gespeicherten Bilder zugreifen."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nein"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ja"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Suchen"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Alben"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d Foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d Fotos"</item>
+  </plurals>
+</resources>
diff --git a/res/values-el/filtershow_strings.xml b/res/values-el/filtershow_strings.xml
new file mode 100644
index 0000000..76e3d2a
--- /dev/null
+++ b/res/values-el/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Επεξεργασία φωτογραφιών"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Δεν είναι δυνατή η φόρτωση της εικόνας!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Ορισμός ταπετσαρίας…"</string>
+    <string name="original" msgid="3524493791230430897">"Αρχική"</string>
+    <string name="borders" msgid="2067345080568684614">"Σύνορα"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Αναίρεση"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Επανάληψη"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Εμφάνιση ιστορικού"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Απόκρυψη ιστορικού"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Εμφ.κατάστ.εικόνας"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Απόκρ.κατάστ.εικόνας"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Ρυθμίσεις"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Υπάρχουν μη αποθηκευμένες αλλαγές σε αυτήν την εικόνα."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Θέλετε να γίνει αποθήκευση πριν από την έξοδο;"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Αποθήκευση και έξοδος"</string>
+    <string name="exit" msgid="242642957038770113">"Έξοδος"</string>
+    <string name="history" msgid="455767361472692409">"Ιστορικό"</string>
+    <string name="reset" msgid="9013181350779592937">"Επαναφορά"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Εφαρμοσμένα εφέ"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Σύγκριση"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Εφαρμογή"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Επαναφορά"</string>
+    <string name="aspect" msgid="4025244950820813059">"Διαστάσεις"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Κανένα"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Σταθερός"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Έκθεση"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Οξύτητα"</string>
+    <string name="contrast" msgid="2310908487756769019">"Αντίθεση"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Ζωντάνια χρώμ."</string>
+    <string name="saturation" msgid="7026791551032438585">"Κορεσμός"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Φίλτρο ΑΜ"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Αυτόματο χρώμα"</string>
+    <string name="hue" msgid="6231252147971086030">"Απόχρωση"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Σκιές"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Φωτεινά σημεία"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Καμπύλες"</string>
+    <string name="vignette" msgid="934721068851885390">"Βινιετάρισμα"</string>
+    <string name="redeye" msgid="4508883127049472069">"Κόκκινα μάτια"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Σχεδίαση"</string>
+    <string name="straighten" msgid="26025591664983528">"Ευθυγράμμιση"</string>
+    <string name="crop" msgid="5781263790107850771">"Περικοπή"</string>
+    <string name="rotate" msgid="2796802553793795371">"Εναλλαγή"</string>
+    <string name="mirror" msgid="5482518108154883096">"Κατοπτρισμός"</string>
+    <string name="negative" msgid="6998313764388022201">"Αρνητικό"</string>
+    <string name="none" msgid="6633966646410296520">"Κανένα"</string>
+    <string name="edge" msgid="7036064886242147551">"Άκρες"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Σμίκρυνση"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Κόκκινο"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Πράσινο"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Μπλε"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Στυλ"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Μέγεθος"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Χρώμα"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Γραμμές"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Μαρκαδόρος"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Πιτσιλιές"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Διαγραφή"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Επιλογή προσαρμ.χρώματος"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Επιλογή χρώματος"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Επιλογή μεγέθους"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"ΟΚ"</string>
+</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
new file mode 100644
index 0000000..98d425b
--- /dev/null
+++ b/res/values-el/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Συλλογή"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Πλαίσιο εικόνας"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Πρόγραμμα αναπαραγωγής βίντεο"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Φόρτωση βίντεο..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Φόρτωση εικόνας…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Φόρτωση λογαριασμού…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Συνέχιση βίντεο"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Συνέχιση αναπαραγωγής από το %s;"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Συνέχιση αναπαραγωγής"</string>
+    <string name="loading" msgid="7038208555304563571">"Φόρτωση..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Δεν ήταν δυνατή η φόρτωση"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Δεν ήταν δυνατή η φόρτωση της εικόνας"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Δεν υπάρχει μικρογραφία"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Έναρξη από την αρχή"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"ΟΚ"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Αγγίξτε κάποιο πρόσωπο για να ξεκινήσετε."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Αποθήκευση εικόνας..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Αποθήκευση εικόνας στο λεύκωμα <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Αδυναμία αποθήκευσης αποκομμένης εικόνας"</string>
+    <string name="crop_label" msgid="521114301871349328">"Περικοπή εικόνας"</string>
+    <string name="trim_label" msgid="274203231381209979">"Περικοπή βίντεο"</string>
+    <string name="select_image" msgid="7841406150484742140">"Επιλογή φωτογραφίας"</string>
+    <string name="select_video" msgid="4859510992798615076">"Επιλογή βίντεο"</string>
+    <string name="select_item" msgid="2816923896202086390">"Επιλεγμένο αντικείμ."</string>
+    <string name="select_album" msgid="1557063764849434077">"Επιλογή λευκώματος"</string>
+    <string name="select_group" msgid="6744208543323307114">"Επιλογή ομάδας"</string>
+    <string name="set_image" msgid="2331476809308010401">"Ορισμός εικόνας ως"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Ορισμός ταπετσαρίας"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Ορισμός ταπετσαρίας..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Ταπετσαρία"</string>
+    <string name="delete" msgid="2839695998251824487">"Διαγραφή"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Διαγραφή επιλεγμέν. στοιχείου;"</item>
+    <item quantity="other" msgid="5874316486520635333">"Διαγραφή επιλεγμέν. στοιχείων;"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Επιβεβαίωση"</string>
+    <string name="cancel" msgid="3637516880917356226">"Ακύρωση"</string>
+    <string name="share" msgid="3619042788254195341">"Κοινή χρήση"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Κοινή χρήση πανοράματος"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Κοινή χρήση ως φωτογραφίας"</string>
+    <string name="deleted" msgid="6795433049119073871">"Διαγράφηκε"</string>
+    <string name="undo" msgid="2930873956446586313">"ΑΝΑΙΡΕΣΗ"</string>
+    <string name="select_all" msgid="3403283025220282175">"Επιλογή όλων"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Αποεπιλογή όλων"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Προβολή διαφανειών"</string>
+    <string name="details" msgid="8415120088556445230">"Λεπτομέρειες"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d από %2$d στοιχεία:"</string>
+    <string name="close" msgid="5585646033158453043">"Κλείσιμο"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Φωτογραφική μηχανή"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Επιλέχθηκαν %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Επιλέχθηκαν %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Επιλέχθηκαν %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Επιλέχθηκαν %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Επιλέχθηκαν %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Επιλέχθηκαν %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Επιλέχθηκαν %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Επιλέχθηκαν %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Επιλέχθηκαν %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Εμφάνιση στον χάρτη"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Αριστερή περιστροφή"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Δεξιά περιστροφή"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Δεν ήταν δυνατή η εύρεση."</string>
+    <string name="edit" msgid="1502273844748580847">"Επεξεργασία"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Επεξεργασία αιτημάτων προσωρινής αποθήκευσης"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Προσωρ. αποθ..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Περικοπή"</string>
+    <string name="trim_action" msgid="703098114452883524">"Περικοπή"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Σίγαση"</string>
+    <string name="set_as" msgid="3636764710790507868">"Ορισμός ως"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Αδύνατη η σίγαση του βίντεο."</string>
+    <string name="video_err" msgid="7003051631792271009">"Δεν είναι δυνατή η αναπαραγωγή βίντεο."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Κατά τοποθεσία"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Κατά ώρα"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Κατά ετικέτα"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Κατά άτομα"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Κατά λεύκωμα"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Κατά μέγεθος"</string>
+    <string name="untagged" msgid="7281481064509590402">"Χωρίς ετικέτα"</string>
+    <string name="no_location" msgid="4043624857489331676">"Καμία τοποθεσία"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Δεν ήταν δυνατός ο προσδιορισμός ορισμένων τοποθεσιών λόγω προβλημάτων δικτύου."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Δεν ήταν δυνατή η λήψη των φωτογραφιών σε αυτό το λεύκωμα. Δοκιμάστε ξανά αργότερα."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Μόνο εικόνες"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Μόνο βίντεο"</string>
+    <string name="show_all" msgid="6963292714584735149">"Εικόνες και βίντεο"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Συλλογή φωτογραφιών"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Δεν υπάρχουν φωτογραφίες."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Η αποκομμένη εικόνα αποθηκεύτηκε στον φάκελο <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Δεν υπάρχουν διαθέσιμα λευκώματα."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Δεν υπάρχουν διαθέσιμες εικόνες/βίντεο."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Αναρτήσεις"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Διαθέσιμα εκτός σύνδεσης"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Ανανέωση"</string>
+    <string name="done" msgid="217672440064436595">"Τέλος"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d από %2$d στοιχεία:"</string>
+    <string name="title" msgid="7622928349908052569">"Τίτλος"</string>
+    <string name="description" msgid="3016729318096557520">"Περιγραφή"</string>
+    <string name="time" msgid="1367953006052876956">"Ώρα"</string>
+    <string name="location" msgid="3432705876921618314">"Τοποθεσία"</string>
+    <string name="path" msgid="4725740395885105824">"Διαδρομή"</string>
+    <string name="width" msgid="9215847239714321097">"Πλάτος"</string>
+    <string name="height" msgid="3648885449443787772">"Ύψος"</string>
+    <string name="orientation" msgid="4958327983165245513">"Προσανατολισμός"</string>
+    <string name="duration" msgid="8160058911218541616">"Διάρκεια"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Τύπος MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Μέγεθος αρχείου"</string>
+    <string name="maker" msgid="7921835498034236197">"Δημιουργός"</string>
+    <string name="model" msgid="8240207064064337366">"Μοντέλο"</string>
+    <string name="flash" msgid="2816779031261147723">"Φλας"</string>
+    <string name="aperture" msgid="5920657630303915195">"Διάφραγμα"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Μήκος εστίασης"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Ισορρ. λευκού"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Χρόνος έκθεσης"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"χιλιοστά"</string>
+    <string name="manual" msgid="6608905477477607865">"Μη αυτόματο"</string>
+    <string name="auto" msgid="4296941368722892821">"Αυτόματο"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Το φλας άναψε"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Όχι φλας"</string>
+    <string name="unknown" msgid="3506693015896912952">"Άγνωστο"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Αρχική"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Εποχής"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Άμεσα"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Λεύκανση"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Μπλε"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Α/Μ"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Γροθιά"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Επεξεργασία X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Διάθεση λευκώματος εκτός σύνδεσης."</item>
+    <item quantity="other" msgid="4948604338155959389">"Διάθεση λευκωμάτων εκτός σύνδεσης."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Αυτό το αντικείμενο είναι αποθηκευμένο τοπικά και διαθέσιμο εκτός σύνδεσης."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Όλα τα λευκώματα"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Τοπικά λευκώματα"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Συσκευές MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Λευκώματα Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ελεύθερα"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ή μικρότερο"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ή μεγαλύτερο"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> έως <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Εισαγωγή"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Η εισαγωγή ολοκληρ."</string>
+    <string name="import_fail" msgid="8497942380703298808">"Μη επιτυχημένη εισαγωγή"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Φωτογραφική μηχανή συνδεδεμένη."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Φωτογρ. μηχανή αποσυνδεδεμένη"</string>
+    <string name="click_import" msgid="6407959065464291972">"Αγγίξτε εδώ για να κάνετε εισαγωγή"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Επιλογή λευκώματος"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Τυχαία αναπ. όλων των εικόνων"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Επιλέξτε μια εικόνα"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Επιλογή εικόνων"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Προβολή διαφανειών"</string>
+    <string name="albums" msgid="7320787705180057947">"Λευκώματα"</string>
+    <string name="times" msgid="2023033894889499219">"Φορές"</string>
+    <string name="locations" msgid="6649297994083130305">"Τοποθεσίες"</string>
+    <string name="people" msgid="4114003823747292747">"Άτομα"</string>
+    <string name="tags" msgid="5539648765482935955">"Ετικέτες"</string>
+    <string name="group_by" msgid="4308299657902209357">"Ομαδοποίηση κατά"</string>
+    <string name="settings" msgid="1534847740615665736">"Ρυθμίσεις"</string>
+    <string name="add_account" msgid="4271217504968243974">"Προσθήκη λογαριασμού"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Φωτογραφική μηχανή"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Λήψη"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Φωτογραφίες επεξεργασμένες στο διαδίκτυο"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Έγινε εισαγωγή"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Στιγμιότυπο οθόνης"</string>
+    <string name="help" msgid="7368960711153618354">"Βοήθεια"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Δεν υπάρχ.αποθ.χώρ."</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Δεν υπάρχει διαθέσιμος εξωτερικός χώρος αποθήκευσης"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Προβολή σε φιλμ"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Προβολή πλέγματος"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Πλήρης οθόνη"</string>
+    <string name="trimming" msgid="9122385768369143997">"Περικοπή"</string>
+    <string name="muting" msgid="5094925919589915324">"Σίγαση"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Περιμένετε"</string>
+    <string name="save_into" msgid="9155488424829609229">"Αποθήκευση βίντεο στο άλμπουμ <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Δεν είναι δυνατή η περικοπή : το βίντεο-στόχος είναι πολύ σύντομο"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Απόδοση πανοράματος"</string>
+    <string name="save" msgid="613976532235060516">"Αποθήκευση"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Σάρωση περιεχομένου…"</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d στοιχεία έχουν σαρωθεί"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d στοιχείο έχει σαρωθεί"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d στοιχεία έχουν σαρωθεί"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Ταξινόμηση…"</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Η σάρωση έχει ολοκληρωθεί"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Εισαγωγή…"</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Δεν υπάρχει διαθέσιμο περιεχόμενο για εισαγωγή σε αυτή τη συσκευή."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Δεν υπάρχει συνδεδεμένη συσκευή MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Σφάλμα φωτογραφικής μηχανής"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Δεν είναι δυνατή η σύνδεση με τη φωτογραφική μηχανή."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Η φωτογραφική μηχανή έχει απενεργοποιηθεί λόγω των πολιτικών ασφαλείας."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Φωτογ.μηχανή"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Βιντεοκάμ."</string>
+    <string name="wait" msgid="8600187532323801552">"Περιμένετε..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Προσαρτήστε τον χώρο αποθήκευσης USB προτού κάνετε χρήση της φωτογραφικής μηχανής."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Εισαγάγετε κάρτα SD πριν χρησιμοποιήσετε τη φωτογραφική μηχανή."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Προετοιμασία χώρου αποθ. USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Προετοιμασία κάρτας SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Δεν ήταν δυνατή η πρόσβαση στο χώρο αποθήκευσης USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Δεν ήταν δυνατή η πρόσβαση στην κάρτα SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ΑΚΥΡΩΣΗ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ΤΕΛΟΣ"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Εγγραφή παρέλευσης χρόνου"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Επιλογή φωτ. μηχανής"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Πίσω"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Μπροστά"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Αποθήκευση τοποθεσίας"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Χρονόμετρο αντίστροφης μέτρησης"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 δευτερόλεπτο"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d δευτερόλεπτα"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Ήχος κατά τη διάρκεια της αντίστροφης μέτρησης"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Απενεργοποιημ."</string>
+    <string name="setting_on" msgid="8602246224465348901">"Ενεργοποιημ."</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Ποιότητα βίντεο"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Υψηλή"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Χαμηλή"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Παρέλευση χρόνου"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Ρυθμίσεις φωτογραφικής μηχανής"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Ρυθμίσεις βιντεοκάμερας"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Μέγεθος εικόνας"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M εικονοστ."</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 M εικονοστ."</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M εικονοστ."</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M  εικονοστ."</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3M εικονοστ."</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M εικονοστ."</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Λειτουργία εστίασης"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Αυτόματο"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Άπειρο"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Απόσταση"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Λειτουργία Flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Αυτόματο"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Ενεργοποιημένο"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"ΑΠΕΝΕΡΓΟΠΟΙΗΜΕΝΟ"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Ισορροπία λευκού"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Αυτόματο"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Λαμπτήρας πυρακτώσεως"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Φως ημέρας"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Λαμπτήρας φθορισμού"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Συννεφιά"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Λειτουργία σκηνής"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Αυτόματο"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Ενέργεια"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Νύχτα"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Ηλιοβασίλεμα"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Πάρτι"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Δεν υπάρχει δυνατότητα επιλογής στη λειτουργία σκηνής."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Έκθεση"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Ο διαθέσιμος χώρος USB είναι ελάχιστος. Αλλάξτε τη ρύθμιση της ποιότητας ή διαγράψτε κάποιες εικόνες ή άλλα αρχεία."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Ο διαθέσιμος χώρος στην κάρτα SD είναι ελάχιστος. Αλλάξτε τη ρύθμιση ποιότητας ή διαγράψτε κάποιες εικόνες ή άλλα αρχεία."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Συμπληρώθηκε το όριο μεγέθους."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Πολύ γρήγορα"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Προετοιμασία πανοραμικής εικ."</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Δεν ήταν δυνατή η αποθήκευση του πανοράματος."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Πανόραμα"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Λήψη πανοράματος"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Αναμονή για προηγ. πανοραμική εικόνα"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Aποθήκευση..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Απόδοση πανοράματος"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Πατήστε για εστίαση."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Εφέ"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Κανένα"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Πιέστε"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Μεγάλα μάτια"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Μεγάλο στόμα"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Μικρό στόμα"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Μεγάλη μύτη"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Μικρά μάτια"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Στο διάστημα"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Δύση ηλίου"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Το βίντεό σας"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Αφήστε τη συσκευή σας κάτω"\n"Απομακρυνθείτε για λίγο από την εμβέλειά της."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Αγγίξτε για λήψη φωτογραφίας κατά την εγγραφή."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Έχει ξεκινήσει η εγγραφή βίντεο."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Έχει διακοπεί η εγγραφή βίντεο."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Το στιγμιότυπο οθόνης βίντεο απενεργοποιείται με ενεργά τα εφέ."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Διαγραφή εφέ"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"ΑΣΤΕΙΑ ΠΡΟΣΩΠΑ"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ΦΟΝΤΟ"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Κουμπί κλείστρου"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Κουμπί μενού"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Πιο πρόσφατη φωτογραφία"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Διακόπτης εμπρός και πίσω φωτογραφικής μηχανής"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Επιλογέας φωτογραφικής μηχανής, βίντεο ή πανοράματος"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Περισσότερα στοιχεία ελέγχου ρυθμίσεων"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Κλείσιμο στοιχείων ελέγχου ρυθμίσεων"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Έλεγχος εστίασης"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Μείωση %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Αύξηση %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s πλαίσιο επιλογής"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Μετάβαση σε φωτογραφία"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Μετάβαση σε λειτουργία βίντεο"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Μετάβαση σε πανόραμα"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Μετάβαση σε νέο πανόραμα"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Ακύρωση αναθεώρησης"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Ολοκλήρωση αναθεώρησης"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Έλεγχος νέας λήψης"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ΕΝΕΡΓΟΠΟΙΗΣΗ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"ΑΠΕΝΕΡΓΟΠΟΙΗΣΗ"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Απενεργοποιημένη"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 δευτερόλεπτο"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 δευτερόλεπτο"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 δευτερόλεπτο"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 δευτερόλεπτα"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 λεπτό"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 λεπτό"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 λεπτό"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 λεπτά"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 ώρα"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 ώρα"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 ώρα"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ώρες"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ώρες"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"δευτερόλεπτα"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"λεπτά"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ώρες"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Τέλος"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Ορισμός χρονικού διαστήματος"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Η λειτουργία παρέλευσης χρόνου έχει απενεργοποιηθεί. Ενεργοποιήστε την για να ορίσετε το χρονικό διάστημα."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Το χρονόμετρο αντίστροφης μέτρησης είναι απενεργοποιημένο. Ενεργοποιήστε το για να ξεκινήσει η αντίστροφη μέτρηση πριν τη λήψη φωτογραφίας."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Ορισμός διάρκειας σε δευτερόλεπτα"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Αντίστροφη μέτρηση για λήψη φωτογραφίας"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Απομνημόνευση τοποθεσίας φωτογραφιών;"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Προσθέστε ετικέτες στις φωτογραφίες και τα βίντεό σας με την τοποθεσία στην οποία έχουν τραβηχτεί."\n\n"Οι άλλες εφαρμογές μπορούν να αποκτήσουν πρόσβαση σε αυτές τις πληροφορίες μαζί με τις αποθηκευμένες σας εικόνες."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Όχι ευχαριστώ"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ναι"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Κάμερα"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Αναζήτηση"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Φωτογραφίες"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Λεύκωμα"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d φωτογραφία"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d φωτογραφ."</item>
+  </plurals>
+</resources>
diff --git a/res/values-en-rGB/filtershow_strings.xml b/res/values-en-rGB/filtershow_strings.xml
new file mode 100644
index 0000000..be9a85f
--- /dev/null
+++ b/res/values-en-rGB/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Photo Editor"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Cannot load the image!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Setting wallpaper"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Borders"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Undo"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Redo"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Show history"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Hide history"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Show Image State"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Hide Image State"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Settings"</string>
+    <string name="unsaved" msgid="8704442449002374375">"There are unsaved changes to this image."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Do you want to save before exiting?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Save and Exit"</string>
+    <string name="exit" msgid="242642957038770113">"Exit"</string>
+    <string name="history" msgid="455767361472692409">"History"</string>
+    <string name="reset" msgid="9013181350779592937">"Reset"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Applied Effects"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Compare"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Apply"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Reset"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspect"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1, 1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"None"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fixed"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposure"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Sharpness"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vibrancy"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturation"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"BW Filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autocolour"</string>
+    <string name="hue" msgid="6231252147971086030">"Hue"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Shadows"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Highlights"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curves"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignette"</string>
+    <string name="redeye" msgid="4508883127049472069">"Red Eye"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Draw"</string>
+    <string name="straighten" msgid="26025591664983528">"Straighten"</string>
+    <string name="crop" msgid="5781263790107850771">"Crop"</string>
+    <string name="rotate" msgid="2796802553793795371">"Rotate"</string>
+    <string name="mirror" msgid="5482518108154883096">"Mirror"</string>
+    <string name="negative" msgid="6998313764388022201">"Negative"</string>
+    <string name="none" msgid="6633966646410296520">"None"</string>
+    <string name="edge" msgid="7036064886242147551">"Edges"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Downsample"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Red"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Green"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blue"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Style"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Size"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Colour"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Lines"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marker"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Spatter"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Clear"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Choose custom colour"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Select Colour"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Select Size"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..788d170
--- /dev/null
+++ b/res/values-en-rGB/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Gallery"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Picture frame"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Video player"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Loading video…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Loading image…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Loading account…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Resume video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Resume playing from %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Resume playing"</string>
+    <string name="loading" msgid="7038208555304563571">"Loading…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Couldn\'t load"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Couldn\'t load the image"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"No thumbnail"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Start again"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Touch a face to begin."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Saving picture…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Saving picture to <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Couldn\'t save cropped image."</string>
+    <string name="crop_label" msgid="521114301871349328">"Crop picture"</string>
+    <string name="trim_label" msgid="274203231381209979">"Trim video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Select photo"</string>
+    <string name="select_video" msgid="4859510992798615076">"Select video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Select item"</string>
+    <string name="select_album" msgid="1557063764849434077">"Select album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Select group"</string>
+    <string name="set_image" msgid="2331476809308010401">"Set picture as"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Set wallpaper"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Setting wallpaper..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string>
+    <string name="delete" msgid="2839695998251824487">"Delete"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Delete selected item?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Delete selected items?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirm"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancel"</string>
+    <string name="share" msgid="3619042788254195341">"Share"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Share panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Share as photo"</string>
+    <string name="deleted" msgid="6795433049119073871">"Deleted"</string>
+    <string name="undo" msgid="2930873956446586313">"UNDO"</string>
+    <string name="select_all" msgid="3403283025220282175">"Select all"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Deselect all"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Slideshow"</string>
+    <string name="details" msgid="8415120088556445230">"Details"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d of %2$d items:"</string>
+    <string name="close" msgid="5585646033158453043">"Close"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Switch to camera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d selected"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d selected"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d selected"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d selected"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d selected"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d selected"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d selected"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d selected"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d selected"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Show on map"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Rotate left"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rotate right"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Couldn\'t find item."</string>
+    <string name="edit" msgid="1502273844748580847">"Edit"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Process Caching Requests"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Caching..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Crop"</string>
+    <string name="trim_action" msgid="703098114452883524">"Trim"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Mute"</string>
+    <string name="set_as" msgid="3636764710790507868">"Set as"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Can\'t mute video."</string>
+    <string name="video_err" msgid="7003051631792271009">"Cannot play video"</string>
+    <string name="group_by_location" msgid="316641628989023253">"By location"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"By time"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"By tags"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"By people"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"By album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"By size"</string>
+    <string name="untagged" msgid="7281481064509590402">"Untagged"</string>
+    <string name="no_location" msgid="4043624857489331676">"No location"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Some locations couldn\'t be identified due to network problems."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Couldn\'t download the photos in this album. Retry later."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Images only"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Videos only"</string>
+    <string name="show_all" msgid="6963292714584735149">"Images &amp; videos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Photo Gallery"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"No photos."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Cropped image saved to <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"No albums available."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O images/videos available."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Posts"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Make available offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Refresh"</string>
+    <string name="done" msgid="217672440064436595">"Done"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d of %2$d items:"</string>
+    <string name="title" msgid="7622928349908052569">"Title"</string>
+    <string name="description" msgid="3016729318096557520">"Description"</string>
+    <string name="time" msgid="1367953006052876956">"Time"</string>
+    <string name="location" msgid="3432705876921618314">"Location"</string>
+    <string name="path" msgid="4725740395885105824">"Path"</string>
+    <string name="width" msgid="9215847239714321097">"Width"</string>
+    <string name="height" msgid="3648885449443787772">"Height"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientation"</string>
+    <string name="duration" msgid="8160058911218541616">"Duration"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME type"</string>
+    <string name="file_size" msgid="8486169301588318915">"File size"</string>
+    <string name="maker" msgid="7921835498034236197">"Maker"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Aperture"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Focal Length"</string>
+    <string name="white_balance" msgid="1582509289994216078">"White balance"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Exposure time"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash fired"</string>
+    <string name="flash_off" msgid="1445443413822680010">"No flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Unknown"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blue"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"B/W"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Making album available offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Making albums available offline"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"This item is stored locally and available offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"All albums"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Local albums"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP devices"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa albums"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> free"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> or below"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> or above"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> to <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Import"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Import complete"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Import unsuccessful"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Camera connected"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Camera disconnected"</string>
+    <string name="click_import" msgid="6407959065464291972">"Touch here to import"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Choose an album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Shuffle all images"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Choose an image:"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Choose images"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Slide show"</string>
+    <string name="albums" msgid="7320787705180057947">"Albums"</string>
+    <string name="times" msgid="2023033894889499219">"Times"</string>
+    <string name="locations" msgid="6649297994083130305">"Locations"</string>
+    <string name="people" msgid="4114003823747292747">"People"</string>
+    <string name="tags" msgid="5539648765482935955">"Tags"</string>
+    <string name="group_by" msgid="4308299657902209357">"Group by"</string>
+    <string name="settings" msgid="1534847740615665736">"Settings"</string>
+    <string name="add_account" msgid="4271217504968243974">"Add account"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Camera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Download"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Edited Online Photos"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Imported"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshot"</string>
+    <string name="help" msgid="7368960711153618354">"Help"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"No Storage"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"No external storage available"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmstrip view"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Grid view"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Fullscreen view"</string>
+    <string name="trimming" msgid="9122385768369143997">"Trimming"</string>
+    <string name="muting" msgid="5094925919589915324">"Muting"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Please wait"</string>
+    <string name="save_into" msgid="9155488424829609229">"Saving video to <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Cannot trim: target video is too short"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Rendering panorama"</string>
+    <string name="save" msgid="613976532235060516">"Save"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Scanning content..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d items scanned"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d item scanned"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d items scanned"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sorting..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Scanning done"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importing..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"There is no content available for importing on this device."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"There is no MTP device connected"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Camera error"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Can\'t connect to the camera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Camera has been disabled because of security policies."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Camera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Camcorder"</string>
+    <string name="wait" msgid="8600187532323801552">"Please wait…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Please mount USB storage before using the camera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Insert an SD card before using the camera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Preparing USB storage…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Preparing SD card…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Couldn\'t access USB storage."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Couldn\'t access SD card."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"CANCEL"</string>
+    <string name="review_ok" msgid="1156261588693116433">"DONE"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Time lapse recording"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Choose camera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Back"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Front"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Store location"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Countdown timer"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 second"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d seconds"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Beep during countdown"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Off"</string>
+    <string name="setting_on" msgid="8602246224465348901">"On"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Video quality"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"High"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Low"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Time lapse"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Camera settings"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Camcorder settings"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Picture size"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M pixels"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Focus mode"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinity"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Flash mode"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Auto"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"On"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Off"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"White balance"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Auto"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescent"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Daylight"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescent"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Cloudy"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scene mode"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Auto"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Action"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Night"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Sunset"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Party"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Not selectable in scene mode."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exposure"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Your USB storage is running out of space. Change the quality setting or delete some images or other files."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Your SD card is running out of space. Change the quality setting or delete some images or other files."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Size limit reached."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Too fast"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Preparing panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Couldn\'t save panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Capturing panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Waiting for previous panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Saving…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Rendering panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Touch to focus."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effects"</string>
+    <string name="effect_none" msgid="3601545724573307541">"None"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Squeeze"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Big eyes"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Big mouth"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Small mouth"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Big nose"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Small eyes"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"In space"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Sunset"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Your video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Set your device down."\n"Move out of view for a moment."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Touch to take photo while recording."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Video recording has started."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Video recording has stopped."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Video snapshot is disabled when special effects are on."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Clear effects"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"SILLY FACES"</string>
+    <string name="effect_background" msgid="6579360207378171022">"BACKGROUND"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Shutter button"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menu button"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Most recent photo"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Front and back camera switch"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Camera, video or panorama selector"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"More settings controls"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Close settings controls"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoom control"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Decrease %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Increase %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s tick box"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Switch to photo"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Switch to video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Switch to panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Switch to new panorama"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Review cancel"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Review done"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Review retake"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ON"</string>
+    <string name="capital_off" msgid="7231052688467970897">"OFF"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Off"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 second"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 seconds"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0–5 Minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hour"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 hour"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 hours"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 hours"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"seconds"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutes"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"hours"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Done"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Set Time Interval"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Time lapse feature is off. Turn it on to set time interval."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Countdown timer is off. Turn it on to count down before taking a picture."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Set duration in seconds"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Counting down to take a photo"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Remember photo locations?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Tag your photos and videos with the locations where they are taken."\n\n"Other apps can access this information along with your saved images."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"No thanks"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Yes"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Camera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Search"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Photos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albums"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d photo"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d photos"</item>
+  </plurals>
+</resources>
diff --git a/res/values-es-rUS/filtershow_strings.xml b/res/values-es-rUS/filtershow_strings.xml
new file mode 100644
index 0000000..d2f5080
--- /dev/null
+++ b/res/values-es-rUS/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor fotográfico"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"No se puede cargar la imagen."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Estableciendo fondo de pantalla..."</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Bordes"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Deshacer"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Rehacer"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Mostrar historial"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ocultar historial"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Mostrar estado imag."</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ocultar estado imag."</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Configuración"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Hay cambios sin guardar en esta imagen."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"¿Quieres guardar los cambios antes de salir?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Guardar y salir"</string>
+    <string name="exit" msgid="242642957038770113">"Salir"</string>
+    <string name="history" msgid="455767361472692409">"Historial"</string>
+    <string name="reset" msgid="9013181350779592937">"Restablecer"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Efectos aplicados"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Comparar"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Aplicar"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Restablecer"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspecto"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ninguno"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fijo"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Pequeño planeta"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposición"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Nitidez"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contraste"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Intensidad"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturación"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtro B/N"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Color autom."</string>
+    <string name="hue" msgid="6231252147971086030">"Tono"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Sombras"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Zonas brillant."</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curvas"</string>
+    <string name="vignette" msgid="934721068851885390">"Viñeta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Ojos rojos"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Dibujar"</string>
+    <string name="straighten" msgid="26025591664983528">"Enderezar"</string>
+    <string name="crop" msgid="5781263790107850771">"Recortar"</string>
+    <string name="rotate" msgid="2796802553793795371">"Rotar"</string>
+    <string name="mirror" msgid="5482518108154883096">"Espejo"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativo"</string>
+    <string name="none" msgid="6633966646410296520">"Ninguno"</string>
+    <string name="edge" msgid="7036064886242147551">"Bordes"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Reducir"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RVA"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rojo"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Verde"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Azul"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Estilo"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Tamaño"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Color"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Líneas"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marcador"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Salpicadura"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Borrar"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Elegir un color personalizado"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Seleccionar color"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Seleccionar tamaño"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"Aceptar"</string>
+</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..322196b
--- /dev/null
+++ b/res/values-es-rUS/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galería"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Marco de imagen"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Reproductor de video."</string>
+    <string name="loading_video" msgid="4013492720121891585">"Cargando el video..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Cargando imagen..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Cargando cuenta..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Retomar video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"¿Deseas retomar la reproducción desde %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar la reproducción"</string>
+    <string name="loading" msgid="7038208555304563571">"Cargando…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"No se pudo cargar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"No se pudo cargar la imagen."</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Sin miniatura"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Empezar de nuevo"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Aceptar"</string>
+    <string name="ok" msgid="5296833083983263293">"Aceptar"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Toca una cara para comenzar."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Guardando imagen..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Guardando imagen en <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"No se pudo guardar la imagen recortada."</string>
+    <string name="crop_label" msgid="521114301871349328">"Recortar imagen"</string>
+    <string name="trim_label" msgid="274203231381209979">"Recortar video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Seleccionar foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Seleccionar video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Seleccionar elemento"</string>
+    <string name="select_album" msgid="1557063764849434077">"Seleccionar álbum"</string>
+    <string name="select_group" msgid="6744208543323307114">"Seleccionar un grupo"</string>
+    <string name="set_image" msgid="2331476809308010401">"Establecer imagen como"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Establecer como fondo de pantalla"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Estableciendo fondo de pantalla..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Papel tapiz"</string>
+    <string name="delete" msgid="2839695998251824487">"Eliminar"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"¿Eliminar elemento?"</item>
+    <item quantity="other" msgid="5874316486520635333">"¿Eliminar elementos?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Compartir"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Compartir panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Compartir como foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Eliminada"</string>
+    <string name="undo" msgid="2930873956446586313">"DESHACER"</string>
+    <string name="select_all" msgid="3403283025220282175">"Seleccionar todo"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Desmarcar todo"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Presentación de diapositivas"</string>
+    <string name="details" msgid="8415120088556445230">"Detalles"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d de %2$d elementos:"</string>
+    <string name="close" msgid="5585646033158453043">"Cerrar"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Cambiar a cámara"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d seleccionado(s)"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d seleccionado(s)"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d seleccionado(s)"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d seleccionado(s)"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d seleccionado(s)"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d seleccionado(s)"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d seleccionado(s)"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d seleccionado(s)"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d seleccionado(s)"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar en el mapa"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Rotar hacia la izquierda"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rotar hacia la derecha"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"No se pudo encontrar el elemento."</string>
+    <string name="edit" msgid="1502273844748580847">"Editar"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Procesando solicitudes de almacenamiento en caché"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Alm en caché..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Recortar"</string>
+    <string name="trim_action" msgid="703098114452883524">"Recortar"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Silenciar"</string>
+    <string name="set_as" msgid="3636764710790507868">"Establecer como"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"No se puede silenciar video"</string>
+    <string name="video_err" msgid="7003051631792271009">"No se puede reproducir el video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Por ubicación"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Por fecha"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Por etiquetas"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Por persona"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Por tamaño"</string>
+    <string name="untagged" msgid="7281481064509590402">"No etiquetado"</string>
+    <string name="no_location" msgid="4043624857489331676">"No hay ubicación"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"No se pudieron identificar algunas ubicaciones debido a problemas de la red."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"No se pudieron descargar las fotos de este álbum. Vuelve a intentarlo más adelante."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Sólo imágenes"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Sólo videos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imágenes y videos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galería de fotos"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"No hay fotos."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"La imagen recortada se guardó en <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"No hay álbumes disponibles."</string>
+    <string name="empty_album" msgid="4542880442593595494">"No hay imágenes/videos disponibles."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Publicaciones"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Hacer disponible sin conexión"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Actualizar"</string>
+    <string name="done" msgid="217672440064436595">"Listo"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d elementos:"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="description" msgid="3016729318096557520">"Descripción"</string>
+    <string name="time" msgid="1367953006052876956">"Hora"</string>
+    <string name="location" msgid="3432705876921618314">"Ubicación"</string>
+    <string name="path" msgid="4725740395885105824">"Ruta"</string>
+    <string name="width" msgid="9215847239714321097">"Ancho"</string>
+    <string name="height" msgid="3648885449443787772">"Altura"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientación"</string>
+    <string name="duration" msgid="8160058911218541616">"Duración"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Tipo de MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Tamaño del archivo"</string>
+    <string name="maker" msgid="7921835498034236197">"Creador"</string>
+    <string name="model" msgid="8240207064064337366">"Modelo"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Apertura"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Longitud focal"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balance de blancos"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Tiempo de exposición"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Automático"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash activado"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Sin flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Desconocido"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instantánea"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Decolorar"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Azul"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Blanco y negro"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Proceso X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Café con leche"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografía"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Permitiendo que el álbum esté disponible sin conexión"</item>
+    <item quantity="other" msgid="4948604338155959389">"Permitiendo que los álbumes estén disponibles sin conexión"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Este elemento se guardó de manera local y se encuentra disponible sin conexión."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Todos los álbumes"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Álbumes locales"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Dispositivos MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Álbumes de Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> libre"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o menos"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o más"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importar"</string>
+    <string name="import_complete" msgid="3875040287486199999">"La importación ha finalizado."</string>
+    <string name="import_fail" msgid="8497942380703298808">"Error al importar"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Se conectó la cámara."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Se desconectó la cámara."</string>
+    <string name="click_import" msgid="6407959065464291972">"Toca aquí para importar."</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Elegir un álbum"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Reproducir todas las imágenes aleatoriamente"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Elegir una imagen"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Elegir imágenes"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Presentación de diapositivas"</string>
+    <string name="albums" msgid="7320787705180057947">"Álbumes"</string>
+    <string name="times" msgid="2023033894889499219">"Fecha"</string>
+    <string name="locations" msgid="6649297994083130305">"Ubicación"</string>
+    <string name="people" msgid="4114003823747292747">"Personas"</string>
+    <string name="tags" msgid="5539648765482935955">"Etiquetas"</string>
+    <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
+    <string name="settings" msgid="1534847740615665736">"Configuración"</string>
+    <string name="add_account" msgid="4271217504968243974">"Agregar cuenta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Cámara"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Descargas"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Fotos editadas en línea"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importadas"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Capturas de pantalla"</string>
+    <string name="help" msgid="7368960711153618354">"Ayuda"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Sin almacenamiento"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"No hay almacenamiento externo disponible."</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Vista de tira película"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Vista de cuadrícula"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Pantalla completa"</string>
+    <string name="trimming" msgid="9122385768369143997">"Recortando"</string>
+    <string name="muting" msgid="5094925919589915324">"Silenciando"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Espera."</string>
+    <string name="save_into" msgid="9155488424829609229">"Guardando video en <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"No se puede recortar: el video de destino es demasiado corto."</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Creando panorama..."</string>
+    <string name="save" msgid="613976532235060516">"Guardar"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Examinando contenido..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elementos examinados"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d elemento examinado"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d elementos examinados"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Ordenando..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"El contenido terminó de examinarse."</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importando..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"No hay contenido disponible para importar a este dispositivo."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"No hay dispositivos MTP conectados."</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Error de cámara"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"No se puede establecer conexión con la cámara."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"La cámara ha sido desactivada por políticas de seguridad."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Cámara"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Cámara de video"</string>
+    <string name="wait" msgid="8600187532323801552">"Espera, por favor..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Activa el almacenamiento USB antes de usar la cámara."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Inserta una tarjeta SD antes de usar la cámara."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Preparando almacenamiento USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Preparando la tarjeta SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"No se pudo acceder al almacenamiento USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"No se pudo acceder a la tarjeta SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"CANCELAR"</string>
+    <string name="review_ok" msgid="1156261588693116433">"LISTO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Grabación a intervalos de tiempo"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Elegir cámara"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Parte trasera"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Parte delantera"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Almacenar ubicación"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Temp. de cuenta regresiva"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 segundo"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d segundos"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Pitido en cuenta"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Desactivada"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Activada"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Calidad del video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Alta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Baja"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Intervalo"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Configuración de cámara"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Configuración de videocámara"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Tamaño de imagen"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 MP"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 MP"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 MP"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 MP"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 MP"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 MP"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Modo de enfoque"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automático"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinito"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automático"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Activado"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Desactivado"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balance blancos"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automático"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescente"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Luz del día"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescente"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nublado"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Modo de preselección de escenas"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automático"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Acción"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nocturno"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Atardecer"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Fiesta"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"No se puede seleccionar en el modo Escena."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Valor de exposición"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"Aceptar"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"El almacenamiento USB se está quedando sin espacio. Cambia la configuración de calidad o elimina algunas imágenes u otros archivos."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Tu tarjeta SD se está quedando sin espacio. Cambia la configuración de calidad o elimina algunas imágenes u otros archivos."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Se alcanzó el límite del tamaño."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Muy rápido"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Preparando el panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"No se pudo guardar la im. panorámica."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Capturando el panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Esperando el panorama anterior"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Guardando..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Creando panorama..."</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Toca para enfocar."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efectos"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ninguno"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Comprimir"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Ojos grandes"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Boca grande"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Boca pequeña"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Nariz grande"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Ojos pequeños"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"En el espacio"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Atardecer"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Tu video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Desactiva el dispositivo."\n"Deja de usarlo durante unos minutos."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Toca para tomar una foto mientras grabas un video."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Comenzó la grabación de video."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Se detuvo la grabación de video."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Las instantáneas de video se inhabilitan al activar los efectos."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Eliminar efectos"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"CARAS GRACIOSAS"</string>
+    <string name="effect_background" msgid="6579360207378171022">"FONDO"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Botón del obturador"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Botón de menú"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Foto más reciente"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Selector de cámara delantera y trasera"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Selector de cámara, video o panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Más controles de configuración"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Cerrar controles de configuración"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Control de zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Disminución de %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Aumentar %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Casilla de verificación %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Cambiar al modo Cámara"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Cambiar al modo Video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Cambiar al modo Panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Cambiar a nuevo panorama"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Cancelar"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Listo"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Revisar nueva toma"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ACTIVADO"</string>
+    <string name="capital_off" msgid="7231052688467970897">"DESACTIVADO"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Apagado"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hora"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 horas"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"segundos"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutos"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"horas"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Listo"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Establecer intervalo de tiempo"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"La función de intervalo de tiempo está desactivada. Actívala para definir el intervalo de tiempo."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"El temporizador de cuenta regresiva está desactivado. Actívalo en la cuenta regresiva antes de tomar una foto."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Configurar la duración en segundos"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Cuenta regresiva para tomar una foto"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"¿Recordar ubicaciones de las fotos?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Etiqueta tus fotos y videos con la ubicación donde fueron tomados."\n\n"Otras aplicaciones pueden acceder a esta información junto con tus imágenes guardadas."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"No, gracias"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Sí"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Cámara"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Búsqueda"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Álbumes"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotos"</item>
+  </plurals>
+</resources>
diff --git a/res/values-es/filtershow_strings.xml b/res/values-es/filtershow_strings.xml
new file mode 100644
index 0000000..bf81a16
--- /dev/null
+++ b/res/values-es/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor de fotos"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Error al cargar la imagen"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Estableciendo fondo de pantalla..."</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Margen"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Deshacer"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Rehacer"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Mostrar historial"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ocultar historial"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Mostrar estado imagen"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ocultar estado de la imagen"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Ajustes"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Hay cambios sin guardar en esta imagen."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"¿Quieres guardar antes de salir?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Guardar y salir"</string>
+    <string name="exit" msgid="242642957038770113">"Salir"</string>
+    <string name="history" msgid="455767361472692409">"Historial"</string>
+    <string name="reset" msgid="9013181350779592937">"Restablecer"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Efectos aplicados"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Comparar"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Aplicar"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Restablecer"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspecto"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ninguno"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fijo"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Pequeño planeta"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposición"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Nitidez"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contraste"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Intensidad"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturación"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtro b/n"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Color automático"</string>
+    <string name="hue" msgid="6231252147971086030">"Tonalidad"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Sombras"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Lo más destacado"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curvar"</string>
+    <string name="vignette" msgid="934721068851885390">"Viñeta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Ojos rojos"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Dibujo"</string>
+    <string name="straighten" msgid="26025591664983528">"Enderezar"</string>
+    <string name="crop" msgid="5781263790107850771">"Recortar"</string>
+    <string name="rotate" msgid="2796802553793795371">"Girar"</string>
+    <string name="mirror" msgid="5482518108154883096">"Espejo"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativo"</string>
+    <string name="none" msgid="6633966646410296520">"Ninguno"</string>
+    <string name="edge" msgid="7036064886242147551">"Bordes"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Reducir calidad"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rojo"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Verde"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Azul"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Estilo"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Tamaño"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Color"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Líneas"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Rotulador"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Manchas"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Borrar"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Elegir un color personalizado"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Seleccionar color"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Seleccionar tamaño"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"Aceptar"</string>
+</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
new file mode 100644
index 0000000..6ce3e5c
--- /dev/null
+++ b/res/values-es/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galería"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Picture frame"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Reproductor de vídeo"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Cargando vídeo…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Cargando imagen…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Cargando cuenta..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Reanudar vídeo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Reanudar reproducción a partir de %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Reanudar reproducción"</string>
+    <string name="loading" msgid="7038208555304563571">"Cargando..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Error al cargar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"No se puede cargar la imagen."</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"No hay miniaturas."</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Volver a reproducir"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Aceptar"</string>
+    <string name="ok" msgid="5296833083983263293">"Aceptar"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Toca una cara para comenzar."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Guardando imagen..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Guardando imagen en <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Error al guardar la imagen recortada"</string>
+    <string name="crop_label" msgid="521114301871349328">"Recortar"</string>
+    <string name="trim_label" msgid="274203231381209979">"Recortar vídeo"</string>
+    <string name="select_image" msgid="7841406150484742140">"Seleccionar foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Seleccionar vídeo"</string>
+    <string name="select_item" msgid="2816923896202086390">"Seleccionar elemento"</string>
+    <string name="select_album" msgid="1557063764849434077">"Seleccionar álbum"</string>
+    <string name="select_group" msgid="6744208543323307114">"Seleccionar grupo"</string>
+    <string name="set_image" msgid="2331476809308010401">"Establecer como..."</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Establecer fondo de pantalla"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Estableciendo fondo de pantalla..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fondo de pantalla"</string>
+    <string name="delete" msgid="2839695998251824487">"Borrar"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"¿Eliminar elemento seleccionado?"</item>
+    <item quantity="other" msgid="5874316486520635333">"¿Eliminar elementos seleccionados?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Compartir"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Compartir panorámica"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Compartir como foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Eliminada"</string>
+    <string name="undo" msgid="2930873956446586313">"DESHACER"</string>
+    <string name="select_all" msgid="3403283025220282175">"Seleccionar todo"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Desmarcar todo"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Presentación"</string>
+    <string name="details" msgid="8415120088556445230">"Detalles"</string>
+    <string name="details_title" msgid="2611396603977441273">"Elemento: %1$d de %2$d"</string>
+    <string name="close" msgid="5585646033158453043">"Cerrar"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Cambiar a la cámara"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d seleccionados"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d seleccionados"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d seleccionados"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d seleccionados"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d seleccionados"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d seleccionados"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d seleccionados"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d seleccionados"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d seleccionados"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar en el mapa"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Girar a la izquierda"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Girar a la derecha"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"No se ha podido encontrar el elemento."</string>
+    <string name="edit" msgid="1502273844748580847">"Editar"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Procesando solicitudes de almacenamiento en caché"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Almacenando en caché..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Recortar"</string>
+    <string name="trim_action" msgid="703098114452883524">"Recortar"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Silenciar"</string>
+    <string name="set_as" msgid="3636764710790507868">"Establecer como"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"No se puede silenciar el vídeo."</string>
+    <string name="video_err" msgid="7003051631792271009">"No se puede reproducir el vídeo."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Por ubicación"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Por fecha"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Por etiquetas"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Por personas"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Por tamaño"</string>
+    <string name="untagged" msgid="7281481064509590402">"Sin etiquetas"</string>
+    <string name="no_location" msgid="4043624857489331676">"Sin ubicación"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"No se ha podido identificar algunas ubicaciones debido a errores en la red."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"No se ha podido descargar las fotos del álbum. Inténtalo de nuevo más tarde."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Solo imágenes"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Solo vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imágenes y vídeos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galería de fotos"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"No hay fotos."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"La imagen recortada se ha guardado en <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"No hay álbumes disponibles."</string>
+    <string name="empty_album" msgid="4542880442593595494">"No hay imágenes ni vídeos disponibles."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Publicaciones"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Disponible sin conex."</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Actualizar"</string>
+    <string name="done" msgid="217672440064436595">"Listo"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"Elemento: %1$d de %2$d"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="description" msgid="3016729318096557520">"Descripción"</string>
+    <string name="time" msgid="1367953006052876956">"Hora"</string>
+    <string name="location" msgid="3432705876921618314">"Ubicación"</string>
+    <string name="path" msgid="4725740395885105824">"Ruta"</string>
+    <string name="width" msgid="9215847239714321097">"Ancho"</string>
+    <string name="height" msgid="3648885449443787772">"Altura"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientación"</string>
+    <string name="duration" msgid="8160058911218541616">"Duración"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Tipo de MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Tamaño del archivo"</string>
+    <string name="maker" msgid="7921835498034236197">"Creador"</string>
+    <string name="model" msgid="8240207064064337366">"Modelo"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Apertura"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Longitud focal"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balance de blancos"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Tiempo exposición"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Automático"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash activado"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Sin flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Desconocido"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instantánea"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Decolorar"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Azul"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Blanco y negro"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Procesamiento X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Haciendo que el álbum pueda verse sin conexión"</item>
+    <item quantity="other" msgid="4948604338155959389">"Haciendo que los álbumes puedan verse sin conexión..."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"El elemento se ha almacenado de forma local y está disponible sin conexión."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Todos los álbumes"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Álbumes locales"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Dispositivos MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Álbumes de Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> libres"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o inferior"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o superior"</string>
+    <string name="size_between" msgid="8779660840898917208">"De <xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importar"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importación completada"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Error al importar"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Cámara conectada"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Cámara desconectada"</string>
+    <string name="click_import" msgid="6407959065464291972">"Toca aquí para realizar la importación."</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Seleccionar un álbum"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Mostrar imágenes aleatoriamente"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Seleccionar una imagen"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Seleccionar imágenes"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Presentación"</string>
+    <string name="albums" msgid="7320787705180057947">"Álbumes"</string>
+    <string name="times" msgid="2023033894889499219">"Fecha"</string>
+    <string name="locations" msgid="6649297994083130305">"Ubicaciones"</string>
+    <string name="people" msgid="4114003823747292747">"Personas"</string>
+    <string name="tags" msgid="5539648765482935955">"Etiquetas"</string>
+    <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
+    <string name="settings" msgid="1534847740615665736">"Ajustes"</string>
+    <string name="add_account" msgid="4271217504968243974">"Añadir cuenta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Cámara"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Descargadas"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Fotos online editadas"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importadas"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Capturas de pantalla"</string>
+    <string name="help" msgid="7368960711153618354">"Ayuda"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Sin almacenamiento"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"No hay almacenamiento externo disponible."</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Vista de tira de película"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Vista de cuadrícula"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Pantalla completa"</string>
+    <string name="trimming" msgid="9122385768369143997">"Recortando..."</string>
+    <string name="muting" msgid="5094925919589915324">"Silenciando..."</string>
+    <string name="please_wait" msgid="7296066089146487366">"Espera..."</string>
+    <string name="save_into" msgid="9155488424829609229">"Guardando vídeo en <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"No se puede recortar: el vídeo de destino es demasiado corto."</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Creando panorámica..."</string>
+    <string name="save" msgid="613976532235060516">"Guardar"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Analizando contenido..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elementos analizados"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d elemento analizado"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d elementos analizados"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Ordenando..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Análisis completo"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importando..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"No hay contenido disponible para importar en este dispositivo."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"No hay dispositivos MTP conectados"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Error de cámara"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"No se puede acceder a la cámara."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Se ha inhabilitado la cámara debido a las políticas de seguridad."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Cámara"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videocámara"</string>
+    <string name="wait" msgid="8600187532323801552">"Por favor, espera..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Para poder usar la cámara, activa el almacenamiento USB."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Para poder usar la cámara, inserta una tarjeta SD."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Preparando almacenamiento USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Preparando tarjeta SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"No se ha podido acceder al almacenamiento USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"No se ha podido acceder a la tarjeta SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"CANCELAR"</string>
+    <string name="review_ok" msgid="1156261588693116433">"LISTO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Grabación a intervalos de tiempo"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Seleccionar cámara"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Trasera"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Delantera"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Añadir ubicación"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Temporizador de cuenta atrás"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 segundo"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d segundos"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Utilizar pitido"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Desactivado"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Activado"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Calidad de vídeo"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Alta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Baja"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Intervalo de tiempo"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Ajustes de la cámara"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Configuración de videocámara"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Tamaño imagen"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 MP"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 MP"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 MP"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 MP"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 MP"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 MP"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Modo de enfoque"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinito"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automático"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Activado"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Desactivado"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balance de blancos"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automático"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescente"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Luz natural"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescente"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nublado"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Modo de escena"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automático"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Acción"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nocturno"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Atardecer"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Fiesta"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"No se puede seleccionar en el modo de escena."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exposición"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"Aceptar"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"No queda espacio en el almacenamiento USB. Cambia la configuración de calidad o elimina algunas imágenes u otros archivos."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"No queda espacio en la tarjeta SD. Cambia la configuración de calidad o elimina algunas imágenes u otros archivos."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Se ha alcanzado el límite de tamaño."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Muy rápido"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Preparando modo panorámico"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Error al guardar imagen panorámica"</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorámico"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Capturando panorámica"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Esperando foto panorámica anterior..."</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Guardando..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Creando panorámica..."</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Toca para enfocar"</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efectos"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ninguno"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Comprimir"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Ojos grandes"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Boca grande"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Boca pequeña"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Nariz grande"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Ojos pequeños"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"En el espacio"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Atardecer"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Tu vídeo"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Desactiva el dispositivo."\n"Deja de usarlo durante unos minutos."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Toca para hacer una foto mientras grabas un vídeo."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Se ha iniciado la grabación de vídeo."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"La grabación de vídeo se ha detenido."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"La instantánea de vídeo se inhabilita al activar efectos especiales."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Borrar efectos"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"CARAS GRACIOSAS"</string>
+    <string name="effect_background" msgid="6579360207378171022">"FONDO"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Botón del obturador"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Botón de menú"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Foto más reciente"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Opción de cámara trasera y delantera"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Cámara, vídeo o modo panorámico"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Más controles de configuración"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Cerrar controles de configuración"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Control de zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Reducir %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Aumentar %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Casilla de verificación %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Cambiar a la cámara"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Cambiar a vídeo"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Cambiar a modo panorámico"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Cambiar a nueva panorámica"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Cancelar"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Listo"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Revisar repetición"</string>
+    <string name="capital_on" msgid="5491353494964003567">"SÍ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"NO"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Desactivado"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hora"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 horas"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"segundos"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutos"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"horas"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Listo"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Establecer intervalo de tiempo"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"El intervalo de tiempo está desactivado. Activa esta función para establecer un intervalo."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"El temporizador de cuenta atrás está desactivado. Activa esta opción para ver la cuenta atrás antes de hacer una foto."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Definir duración en segundos"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Cuenta atrás para hacer una foto"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"¿Recordar ubicaciones de las fotos?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Etiqueta tus fotos y vídeos con las ubicaciones donde se han realizado."\n\n"Otras aplicaciones pueden acceder a esta información, así como a las imágenes guardadas."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"No, gracias"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Sí"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Cámara"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Buscar"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Álbumes"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotos"</item>
+  </plurals>
+</resources>
diff --git a/res/values-et/filtershow_strings.xml b/res/values-et/filtershow_strings.xml
new file mode 100644
index 0000000..2d9ea29
--- /dev/null
+++ b/res/values-et/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fototöötlus"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Pilti ei saa laadida!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Taustapildi määramine"</string>
+    <string name="original" msgid="3524493791230430897">"Originaal"</string>
+    <string name="borders" msgid="2067345080568684614">"Äärised"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Võta tagasi"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Tee uuesti"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Kuva ajalugu"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Peida ajalugu"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Kuva pildi olek"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Peida pildi olek"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Seaded"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Kujutisel on salvestamata muudatusi."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Kas soovite enne väljumist salvestada?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Salvesta ja välju"</string>
+    <string name="exit" msgid="242642957038770113">"Välju"</string>
+    <string name="history" msgid="455767361472692409">"Ajalugu"</string>
+    <string name="reset" msgid="9013181350779592937">"Lähtesta"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Rakendatud efektid"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Võrdle"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Rakenda"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Lähtesta"</string>
+    <string name="aspect" msgid="4025244950820813059">"Kuvasuhe"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Mitte ühtegi"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fikseeritud"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Pisike planeet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Säriaeg"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Teravus"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Kirkus"</string>
+    <string name="saturation" msgid="7026791551032438585">"Küllastus"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"MV filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autom. värvid"</string>
+    <string name="hue" msgid="6231252147971086030">"Värvitoon"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Varjud"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Esiletõstmine"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kõverad"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinjett"</string>
+    <string name="redeye" msgid="4508883127049472069">"Punasilmsus"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Joonistus"</string>
+    <string name="straighten" msgid="26025591664983528">"Rihi otseks"</string>
+    <string name="crop" msgid="5781263790107850771">"Kärpimine"</string>
+    <string name="rotate" msgid="2796802553793795371">"Pööra"</string>
+    <string name="mirror" msgid="5482518108154883096">"Peegelpilt"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatiiv"</string>
+    <string name="none" msgid="6633966646410296520">"Mitte ühtegi"</string>
+    <string name="edge" msgid="7036064886242147551">"Servad"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Vähend."</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Punane"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Roheline"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Sinine"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stiil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Suurus"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Värv"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Jooned"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marker"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Pritsi"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Tühjenda"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Valige kohandatud värv"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Värvi valimine"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Suuruse valimine"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
new file mode 100644
index 0000000..7873ccd
--- /dev/null
+++ b/res/values-et/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerii"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Pildiraam"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videopleier"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Video laadimine ..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Kujutise laadimine ..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Kontode laadimine ..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Jätka videot"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Kas jätkata esitust alates %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Jätka esitust"</string>
+    <string name="loading" msgid="7038208555304563571">"Laadimine ..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Laadimine ebaõnnestus"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kujutist ei õnnestunud laadida"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Pisipilti pole"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Alusta uuesti"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Alustamiseks näo puudutamine."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Pildi salvestamine ..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Pildi salvestamine albumisse <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Kärbitud kujutist ei saanud salvestada."</string>
+    <string name="crop_label" msgid="521114301871349328">"Kärbi pilti"</string>
+    <string name="trim_label" msgid="274203231381209979">"Kärbi videot"</string>
+    <string name="select_image" msgid="7841406150484742140">"Valige foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Vali video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Valige üksus"</string>
+    <string name="select_album" msgid="1557063764849434077">"Albumi valimine"</string>
+    <string name="select_group" msgid="6744208543323307114">"Rühma valimine"</string>
+    <string name="set_image" msgid="2331476809308010401">"Seadke pilt kui"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Taustapildiks määr."</string>
+    <string name="wallpaper" msgid="140165383777262070">"Taustapildi määramine ..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Taustapilt"</string>
+    <string name="delete" msgid="2839695998251824487">"Kustuta"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Kas kustutada valitud üksus?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Kas kustutada valitud üksused?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Kinnitamine"</string>
+    <string name="cancel" msgid="3637516880917356226">"Tühista"</string>
+    <string name="share" msgid="3619042788254195341">"Jaga"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Panoraami jagamine"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Fotona jagamine"</string>
+    <string name="deleted" msgid="6795433049119073871">"Kustutatud"</string>
+    <string name="undo" msgid="2930873956446586313">"VÕTA TAGASI"</string>
+    <string name="select_all" msgid="3403283025220282175">"Vali kõik"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Tühista kõik valikud"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Slaidiseanss"</string>
+    <string name="details" msgid="8415120088556445230">"Üksikasjad"</string>
+    <string name="details_title" msgid="2611396603977441273">"Üksusi: %1$d/%2$d"</string>
+    <string name="close" msgid="5585646033158453043">"Sule"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Üleminek rakendusse Kaamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Valitud %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Valitud %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Valitud %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Valitud %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Valitud %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Valitud %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Valitud %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Valitud %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Valitud %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Näita kaardil"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Pööra vasakule"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Pööra paremale"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Üksust ei leitud."</string>
+    <string name="edit" msgid="1502273844748580847">"Muuda"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Vahemällu lisamise taotluste töötlemine"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Vahemällu ..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Kärbi"</string>
+    <string name="trim_action" msgid="703098114452883524">"Kärbi"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Vaigista"</string>
+    <string name="set_as" msgid="3636764710790507868">"Seadista kui"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Video vaigistamine ei õnnestu."</string>
+    <string name="video_err" msgid="7003051631792271009">"Videot ei saa esitada."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Asukoha järgi"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Aja järgi"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Märgendite järgi"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Inimeste järgi"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Albumi järgi"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Suuruse järgi"</string>
+    <string name="untagged" msgid="7281481064509590402">"Märgendita"</string>
+    <string name="no_location" msgid="4043624857489331676">"Asukoht puudub"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Mõnda asukohta ei suudetud võrguprobleemide tõttu tuvastada."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Fotosid ei saa sellesse albumisse alla laadida. Proovige hiljem uuesti."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Ainult kujutised"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Ainult videod"</string>
+    <string name="show_all" msgid="6963292714584735149">"Kujutised ja videod"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerii"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Fotod puuduvad."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Kärbitud kujutis salvestati kausta <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Ühtegi albumit pole saadaval."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O kujutist/videot on saadaval."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Postitused"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Muuda offlainis kasutatavaks"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Värskenda"</string>
+    <string name="done" msgid="217672440064436595">"Valmis"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d üksust"</string>
+    <string name="title" msgid="7622928349908052569">"Pealkiri"</string>
+    <string name="description" msgid="3016729318096557520">"Kirjeldus"</string>
+    <string name="time" msgid="1367953006052876956">"Kellaaeg"</string>
+    <string name="location" msgid="3432705876921618314">"Asukoht"</string>
+    <string name="path" msgid="4725740395885105824">"Tee"</string>
+    <string name="width" msgid="9215847239714321097">"Laius"</string>
+    <string name="height" msgid="3648885449443787772">"Kõrgus"</string>
+    <string name="orientation" msgid="4958327983165245513">"Paigutus"</string>
+    <string name="duration" msgid="8160058911218541616">"Kestus"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-tüüp"</string>
+    <string name="file_size" msgid="8486169301588318915">"Faili suurus"</string>
+    <string name="maker" msgid="7921835498034236197">"Looja"</string>
+    <string name="model" msgid="8240207064064337366">"Mudel"</string>
+    <string name="flash" msgid="2816779031261147723">"Välk"</string>
+    <string name="aperture" msgid="5920657630303915195">"Ava"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Fookuskaugus"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Valge tasakaal"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Säriaeg"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Käsitsi"</string>
+    <string name="auto" msgid="4296941368722892821">"Autom."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Välk sees"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Välguta"</string>
+    <string name="unknown" msgid="3506693015896912952">"Tundmatu"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Originaal"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Pleekimine"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Sinine"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Mustvalge"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X-töötlus"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litorgaafia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Tegime albumi võrguühenduseta kättesaadavaks."</item>
+    <item quantity="other" msgid="4948604338155959389">"Tegime albumid võrguühenduseta kättesaadavaks."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"See üksus on kohalikult salvestatud ja offlainis kättesaadav."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Kõik albumid"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Kohalikud albumid"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-seadmed"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa albumid"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> tasuta"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> või väiksem"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> või suurem"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> kuni <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Impordi"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importim. lõpetatud"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Importimine ebaõnnestus"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kaamera on ühendatud."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kaamera pole ühendatud."</string>
+    <string name="click_import" msgid="6407959065464291972">"Puudutage impordiks siia"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Vali album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Kuva kõik kujutised juhuesitusena"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Vali kujutis"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Kujutiste valimine"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Slaidiseanss"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumid"</string>
+    <string name="times" msgid="2023033894889499219">"Ajad"</string>
+    <string name="locations" msgid="6649297994083130305">"Asukohad"</string>
+    <string name="people" msgid="4114003823747292747">"Inimesed"</string>
+    <string name="tags" msgid="5539648765482935955">"Märgendid"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupeerimisalus:"</string>
+    <string name="settings" msgid="1534847740615665736">"Seaded"</string>
+    <string name="add_account" msgid="4271217504968243974">"Lisa konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kaamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Alla laaditud"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Muudetud fotod võrgus"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Imporditud"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekraanipilt"</string>
+    <string name="help" msgid="7368960711153618354">"Abi"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Mäluseade puudub"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Ükski välismäluseade ei ole saadaval"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmiribakuva"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Ruudustiku kuva"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Täisekraani vaade"</string>
+    <string name="trimming" msgid="9122385768369143997">"Kärpimine"</string>
+    <string name="muting" msgid="5094925919589915324">"Vaigistamine"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Palun oodake"</string>
+    <string name="save_into" msgid="9155488424829609229">"Video salvestamine albumisse <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Ei saa kärpida: lõppvideo on liiga lühike"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panoraami renderdamine"</string>
+    <string name="save" msgid="613976532235060516">"Salvesta"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Sisu skannimine ..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d üksust on skannitud"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d üksus on skannitud"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d üksust on skannitud"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sortimine ..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skannimine on lõppenud"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importimine ..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Seadmes pole importimiseks saadaval mingit sisu."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Ühtegi MTP-seadet pole ühendatud"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kaamera viga"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Ei saa kaameraga ühendada."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kaamera on keelatud turvaeeskirjade tõttu."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kaamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokaamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Oodake ..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Paigaldage USB-mäluseade enne kaamera kasutamist."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Enne kaamera kasutamist sisestage SD-kaart."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB-seadme ettevalmistamine…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD-kaardi ettevalmistamine ..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Juurdepääs USB-mäluseadmele ebaõnnestus."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Juurdepääs SD-kaardile ebaõnnestus."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"TÜHISTA"</string>
+    <string name="review_ok" msgid="1156261588693116433">"VALMIS"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Aeglase filmimise salvestamine"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Vali kaamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Tagasi"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Eestvaade"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Talletuse asukoht"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Iseavaja taimer"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sekund"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d sekundit"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Iseavaja heli"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Väljas"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Sees"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Video kvaliteet"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Kõrge"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Madal"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Aeglustus"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kaamera seaded"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Videokaamera seaded"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Pildi suurus"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapikslit"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M pikslit"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M pikslit"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M pikslit"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3M pikslit"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M pikslit"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Teravustamisrežiim"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automaatne"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Lõpmatus"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Välgurežiim"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automaatne"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Sees"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Väljas"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Valge tasakaal"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automaatne"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Hõõglamp"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Päevavalgus"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Päevavalguslamp"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Pilves"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Stseenirežiim"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automaatne"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Toiming"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Öö"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Päikeseloojang"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Pidu"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Seda ei saa stseenirežiimis valida."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Säriaeg"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Teie USB-mäluseadme ruum on otsa saamas. Muutke kvaliteediseadeid või kustutage kujutisi või teisi faile."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Teie SD-kaardi ruum on otsa saamas. Muutke kvaliteedi seadeid või kustutage kujutisi või teisi faile."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Suuruspiirang on saavutatud."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Liiga kiire"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Panoraami ettevalmistus"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panoraami ei saanud salvestada."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panoraam"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Panoraami jäädvustamine"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Eelmise panoraami ootel"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Salvestus ..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panoraami renderdamine"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Puudutage fokuseerimiseks."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efektid"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Mitte ühtegi"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Pitsita"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Suured silmad"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Suur suu"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Väike suu"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Suur nina"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Väiksed silmad"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Kosmoses"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Päikeseloojang"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Teie video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Pange seade käest ära."\n"Astuge korraks vaateväljast eemale."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Puudutage salvestamise ajal foto jäädvustamiseks."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Video salvestamine algas."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Video salvestamine lõppes."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Kui eriefektid on sisse lülitatud, on video hetktõmmis keelatud."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Nulli efektid"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"NALJAKAD NÄOD"</string>
+    <string name="effect_background" msgid="6579360207378171022">"TAUST"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Päästiku nupp"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menüü nupp"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Viimane foto"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Eesmise ja tagumise kaamera lüliti"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kaamera, video või panoraami valija"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Rohkem seadete juhtnuppe"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Sule seadete juhtnupud"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Suumi juhtimine"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Vähenda %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Suurenda %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s märkeruut"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Aktiveeri fotorežiim"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Aktiveeri videorežiim"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Aktiveeri panoraamrežiim"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Lülitu uuele panoraamile"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Arvustuse tühistamine"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Arvustus valmis"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Uue võtte ülevaade"</string>
+    <string name="capital_on" msgid="5491353494964003567">"SEES"</string>
+    <string name="capital_off" msgid="7231052688467970897">"VÄLJAS"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Väljas"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekundit"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutit"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 tund"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 tundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 tundi"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekundit"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutit"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"tundi"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Valmis"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Ajavahemiku määramine"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Aeglustamisfunktsioon on välja lülitatud. Ajavahemiku määramiseks lülitage funktsioon sisse."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Iseavaja taimer on väljas. Lülitage see sisse, et loendada pildi tegemiseni jäänud aega."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Määrake kestus sekundites"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Aega foto tegemiseni"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Kas jätta meelde fotode jäädvustamise asukohad?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Märkige oma fotodele ja videotele jäädvustamise asukoht."\n\n"Muud rakendused pääsevad lisaks salvestatud piltidele juurde ka sellele teabele."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Ei, tänan"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Jah"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kaamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Otsing"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotod"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumid"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotot"</item>
+  </plurals>
+</resources>
diff --git a/res/values-fa/filtershow_strings.xml b/res/values-fa/filtershow_strings.xml
new file mode 100644
index 0000000..7c5872d
--- /dev/null
+++ b/res/values-fa/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"ویرایشگر عکس"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"تصویر بارگیری نمی‌شود!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"تنظیم تصویر زمینه"</string>
+    <string name="original" msgid="3524493791230430897">"اصلی"</string>
+    <string name="borders" msgid="2067345080568684614">"حاشیه‌ها"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"لغو عمل"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"انجام مجدد"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"نمایش سابقه"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"پنهان کردن سابقه"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"نمایش وضعیت تصویر"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"پنهان کردن وضعیت تصویر"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"تنظیمات"</string>
+    <string name="unsaved" msgid="8704442449002374375">"تغییرات ذخیره نشده‌ای در این تصویر وجود دارد."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"آیا می‌خواهید پیش از خروج تغییرات ذخیره شود؟"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"ذخیره و خروج"</string>
+    <string name="exit" msgid="242642957038770113">"خروج"</string>
+    <string name="history" msgid="455767361472692409">"سابقه"</string>
+    <string name="reset" msgid="9013181350779592937">"بازنشانی"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"جلوه‌های اعمال شده"</string>
+    <string name="compare_original" msgid="8140838959007796977">"مقایسه"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"اعمال‌ کردن"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"بازنشانی"</string>
+    <string name="aspect" msgid="4025244950820813059">"نسبت ابعادی"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"۱:۱"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"۴:۳"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"۳:۴"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"۴:۶"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"۵:۷"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"۷:۵"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"۱۶:۹"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"هیچکدام"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"ثابت"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"سیاره کوچک"</string>
+    <string name="exposure" msgid="6526397045949374905">"نوردهی"</string>
+    <string name="sharpness" msgid="6463103068318055412">"وضوح"</string>
+    <string name="contrast" msgid="2310908487756769019">"کنتراست"</string>
+    <string name="vibrance" msgid="3326744578577835915">"درخشندگی"</string>
+    <string name="saturation" msgid="7026791551032438585">"اشباع رنگ"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"فیلتر سیاه و سفید"</string>
+    <string name="wbalance" msgid="6346581563387083613">"رنگ خودکار"</string>
+    <string name="hue" msgid="6231252147971086030">"رنگ‌مایه"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"سایه‌ها"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"هایلایت"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"نمودارها"</string>
+    <string name="vignette" msgid="934721068851885390">"محو لبه‌ها"</string>
+    <string name="redeye" msgid="4508883127049472069">"قرمزی چشم"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"طراحی"</string>
+    <string name="straighten" msgid="26025591664983528">"صاف‌ کردن"</string>
+    <string name="crop" msgid="5781263790107850771">"برش"</string>
+    <string name="rotate" msgid="2796802553793795371">"چرخش"</string>
+    <string name="mirror" msgid="5482518108154883096">"معکوس کردن"</string>
+    <string name="negative" msgid="6998313764388022201">"نگاتیو"</string>
+    <string name="none" msgid="6633966646410296520">"هیچکدام"</string>
+    <string name="edge" msgid="7036064886242147551">"لبه‌ها"</string>
+    <string name="kmeans" msgid="1630263230946107457">"وارهول"</string>
+    <string name="downsample" msgid="3552938534146980104">"کاهش وضوح"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"قرمز"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"سبز"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"آبی"</string>
+    <string name="draw_style" msgid="2036125061987325389">"سبک"</string>
+    <string name="draw_size" msgid="4360005386104151209">"اندازه"</string>
+    <string name="draw_color" msgid="2119030386987211193">"رنگ"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"خطوط"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"نشانگر"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"سبک پاشش"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"پاک کردن"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"رنگ سفارشی را انتخاب کنید"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"رنگ را انتخاب کنید"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"انتخاب اندازه"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"تأیید"</string>
+</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
new file mode 100644
index 0000000..200e41e
--- /dev/null
+++ b/res/values-fa/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"گالری"</string>
+    <string name="gadget_title" msgid="259405922673466798">"قاب عکس"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"پخش کننده ویدئو"</string>
+    <string name="loading_video" msgid="4013492720121891585">"در حال بارگیری ویدئو..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"در حال بارگیری تصویر …"</string>
+    <string name="loading_account" msgid="928195413034552034">"بارگیری حساب؟؟؟"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"از سرگیری ویدئو"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"ادامه پخش از %s ؟"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"از سرگیری پخش"</string>
+    <string name="loading" msgid="7038208555304563571">"در حال بارگیری…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"بارگیری نشد"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"تصویر بارگیری نمی‌شود"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"تصویر کوچکی وجود ندارد"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"شروع مجدد"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"تأیید"</string>
+    <string name="ok" msgid="5296833083983263293">"تأیید"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"برای شروع یک چهره را لمس کنید."</string>
+    <string name="saving_image" msgid="7270334453636349407">"در حال ذخیره عکس..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"ذخیره تصویر در <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"ذخیره تصویر برش‌خورده امکان‌پذیر نیست."</string>
+    <string name="crop_label" msgid="521114301871349328">"برش تصویر"</string>
+    <string name="trim_label" msgid="274203231381209979">"برش ویدیو"</string>
+    <string name="select_image" msgid="7841406150484742140">"انتخاب عکس"</string>
+    <string name="select_video" msgid="4859510992798615076">"انتخاب ویدئو"</string>
+    <string name="select_item" msgid="2816923896202086390">"انتخاب مورد"</string>
+    <string name="select_album" msgid="1557063764849434077">"آلبوم را انتخاب کنید"</string>
+    <string name="select_group" msgid="6744208543323307114">"انتخاب گروه"</string>
+    <string name="set_image" msgid="2331476809308010401">"تنظیم تصویر به‌عنوان"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"تنظیم تصویر زمینه"</string>
+    <string name="wallpaper" msgid="140165383777262070">"تنظیم تصویر زمینه..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"تصویر زمینه"</string>
+    <string name="delete" msgid="2839695998251824487">"حذف"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"مورد انتخابی حذف شود؟"</item>
+    <item quantity="other" msgid="5874316486520635333">"موارد انتخابی حذف شوند؟"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"تأیید"</string>
+    <string name="cancel" msgid="3637516880917356226">"لغو"</string>
+    <string name="share" msgid="3619042788254195341">"اشتراک‌گذاری"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"اشتراک‌گذاری پانوراما"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"اشتراک‌گذاری به عنوان عکس"</string>
+    <string name="deleted" msgid="6795433049119073871">"پاک شد"</string>
+    <string name="undo" msgid="2930873956446586313">"واگرد"</string>
+    <string name="select_all" msgid="3403283025220282175">"انتخاب همه"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"لغو انتخاب همه"</string>
+    <string name="slideshow" msgid="4355906903247112975">"نمایش اسلاید"</string>
+    <string name="details" msgid="8415120088556445230">"جزئیات"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d از %2$d مورد:"</string>
+    <string name="close" msgid="5585646033158453043">"بستن"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"جابجایی به دوربین"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d انتخاب شد"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d انتخاب شد"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d انتخاب شد"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d انتخاب شد"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d انتخاب شد"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d انتخاب شد"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d انتخاب شد"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d انتخاب شد"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d انتخاب شد"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"نمایش در نقشه"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"چرخش به چپ"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"چرخش به راست"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"مورد یافت نشد."</string>
+    <string name="edit" msgid="1502273844748580847">"ویرایش"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"پردازش درخواست‌های ذخیره در حافظهٔ پنهان"</string>
+    <string name="caching_label" msgid="4521059045896269095">"در حال ذخیره در حافظهٔ پنهان..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"برش"</string>
+    <string name="trim_action" msgid="703098114452883524">"برش"</string>
+    <string name="mute_action" msgid="5296241754753306251">"بی‌صدا"</string>
+    <string name="set_as" msgid="3636764710790507868">"تنظیم به‌عنوان"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"بیصدا کردن ویدیو انجام نشد."</string>
+    <string name="video_err" msgid="7003051631792271009">"ویدئو پخش نمی‌شود."</string>
+    <string name="group_by_location" msgid="316641628989023253">"بر اساس محل"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"بر اساس زمان"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"بر اساس برچسب‌ها"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"براساس افراد"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"بر اساس آلبوم"</string>
+    <string name="group_by_size" msgid="153766174950394155">"بر اساس اندازه"</string>
+    <string name="untagged" msgid="7281481064509590402">"بدون برچسب‌گذاری"</string>
+    <string name="no_location" msgid="4043624857489331676">"مکانی موجود نیست"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"شناسایی برخی از مکان‌ها به دلیل مشکلات شبکه امکان‌پذیر نیست."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"عکس‌های این آلبوم را نمی‌توان دانلود کرد. بعداً دوباره امتحان کنید."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"فقط تصاویر"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"فقط ویدئوها"</string>
+    <string name="show_all" msgid="6963292714584735149">"تصاویر و ویدئوها"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"گالری عکس"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"عکسی موجود نیست."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"تصویر بریده شده در <xliff:g id="FOLDER_NAME">%s</xliff:g> ذخیره شد."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"آلبومی موجود نیست."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O تصویر/ویدئو موجود است."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"پست‌ها"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"در دسترس بودن در هنگام آفلاین"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"بازخوانی"</string>
+    <string name="done" msgid="217672440064436595">"انجام شد"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d از %2$d مورد:"</string>
+    <string name="title" msgid="7622928349908052569">"عنوان"</string>
+    <string name="description" msgid="3016729318096557520">"توصیف"</string>
+    <string name="time" msgid="1367953006052876956">"زمان"</string>
+    <string name="location" msgid="3432705876921618314">"موقعیت مکانی"</string>
+    <string name="path" msgid="4725740395885105824">"مسیر"</string>
+    <string name="width" msgid="9215847239714321097">"عرض"</string>
+    <string name="height" msgid="3648885449443787772">"ارتفاع"</string>
+    <string name="orientation" msgid="4958327983165245513">"جهت"</string>
+    <string name="duration" msgid="8160058911218541616">"مدت"</string>
+    <string name="mimetype" msgid="8024168704337990470">"نوع MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"اندازه فایل"</string>
+    <string name="maker" msgid="7921835498034236197">"سازنده"</string>
+    <string name="model" msgid="8240207064064337366">"مدل"</string>
+    <string name="flash" msgid="2816779031261147723">"فلاش"</string>
+    <string name="aperture" msgid="5920657630303915195">"دریچه دیافراگم"</string>
+    <string name="focal_length" msgid="1291383769749877010">"فاصله کانونی"</string>
+    <string name="white_balance" msgid="1582509289994216078">"توازن سفیدی"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"زمان نوردهی"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"میلیمتر"</string>
+    <string name="manual" msgid="6608905477477607865">"دستی"</string>
+    <string name="auto" msgid="4296941368722892821">"خودکار"</string>
+    <string name="flash_on" msgid="7891556231891837284">"فلاش زده شد"</string>
+    <string name="flash_off" msgid="1445443413822680010">"بدون فلاش"</string>
+    <string name="unknown" msgid="3506693015896912952">"ناشناس"</string>
+    <string name="ffx_original" msgid="372686331501281474">"اصلی"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"آنتیک"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"فوری"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"سفیدکننده"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"آبی"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"سیاه‌/سفید"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"پانچ"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"فرآیند X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"آلبوم را به صورت آفلاین در دسترس قرار دهید."</item>
+    <item quantity="other" msgid="4948604338155959389">"آلبوم‌ها را به صورت آفلاین در دسترس قرار دهید."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"این مورد به صورت محلی ذخیره می‌شود و به طور آفلاین در دسترس است."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"همه آلبوم‌ها"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"آلبوم‌های محلی"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"دستگاه‌های MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"آلبوم‌های Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> آزاد"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> یا کمتر"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> یا بیشتر"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> تا <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"وارد کردن"</string>
+    <string name="import_complete" msgid="3875040287486199999">"وارد کردن انجام شد"</string>
+    <string name="import_fail" msgid="8497942380703298808">"وارد کردن ناموفق بود"</string>
+    <string name="camera_connected" msgid="916021826223448591">"دوربین متصل شد."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"اتصال دوربین قطع شد."</string>
+    <string name="click_import" msgid="6407959065464291972">"برای وارد کردن اینجا را لمس کنید"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"انتخاب یک آلبوم"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"نمایش تصادفی همه تصاویر"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"انتخاب یک تصویر"</string>
+    <string name="widget_type" msgid="1364653978966343448">"انتخاب تصاویر"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"نمایش اسلاید"</string>
+    <string name="albums" msgid="7320787705180057947">"آلبوم‌ها"</string>
+    <string name="times" msgid="2023033894889499219">"دوره"</string>
+    <string name="locations" msgid="6649297994083130305">"مکان‌ها"</string>
+    <string name="people" msgid="4114003823747292747">"افراد"</string>
+    <string name="tags" msgid="5539648765482935955">"نشان‌ها"</string>
+    <string name="group_by" msgid="4308299657902209357">"گروه بندی براساس"</string>
+    <string name="settings" msgid="1534847740615665736">"تنظیمات"</string>
+    <string name="add_account" msgid="4271217504968243974">"افزودن حساب"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"دوربین"</string>
+    <string name="folder_download" msgid="7186215137642323932">"دانلود"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"ویرایش آنلاین عکس"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"وارد شده"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"عکس صفحه"</string>
+    <string name="help" msgid="7368960711153618354">"راهنما"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"فاقد دستگاه ذخیره‌‌‌سازی"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"هیچ دستگاه ذخیره خارجی دردسترس نیست"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"نمایش نوار فیلم"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"نمای شبکه‌ای"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"نمایش تمام صفحه"</string>
+    <string name="trimming" msgid="9122385768369143997">"کوتاه کردن"</string>
+    <string name="muting" msgid="5094925919589915324">"بیصدا کردن"</string>
+    <string name="please_wait" msgid="7296066089146487366">"لطفاً منتظر بمانید"</string>
+    <string name="save_into" msgid="9155488424829609229">"درحال ذخیره ویدیو در <xliff:g id="ALBUM_NAME">%1$s</xliff:g> ..."</string>
+    <string name="trim_too_short" msgid="751593965620665326">"نمی‌تواند کوتاه شود: ویدیوی موردنظر بسیار کوتاه است"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"در حال تولید تصویر پانوراما"</string>
+    <string name="save" msgid="613976532235060516">"ذخیره"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"در حال اسکن کردن محتوا..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d مورد اسکن شد"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d مورد اسکن شد"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d مورد اسکن شد"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"در حال مرتب‌سازی..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"اسکن انجام شد"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"درحال وارد کردن..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"هیچ محتوایی برای وارد کردن در این دستگاه، موجود نیست."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"هیچ دستگاه MTP متصل نیست"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"خطای دوربین"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"اتصال به دوربین امکان‌پذیر نیست."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"به دلیل خط مشی‌های امنیتی، دوربین غیرفعال شده است."</string>
+    <string name="camera_label" msgid="6346560772074764302">"دوربین"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"دوربین فیلمبرداری"</string>
+    <string name="wait" msgid="8600187532323801552">"لطفاً منتظر بمانید…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"قبل از استفاده از دوربین حافظهٔ USB را وصل کنید."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"قبل از استفاده از دوربین یک کارت SD وارد کنید."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"آماده سازی حافظهٔ USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"در حال آماده سازی کارت SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"دسترسی به حافظهٔ USB ممکن نیست."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"دسترسی به کارت SD ممکن نیست."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"لغو"</string>
+    <string name="review_ok" msgid="1156261588693116433">"انجام شد"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"زمان سپری شده ضبط"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"انتخاب دوربین"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"برگشت"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"جلو"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"ذخیره موقعیت مکانی"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"تایمر شمارش معکوس"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"۱ ثانیه"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d ثانیه"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"بیپ هنگام شمارش معکوس"</string>
+    <string name="setting_off" msgid="4480039384202951946">"غیرفعال"</string>
+    <string name="setting_on" msgid="8602246224465348901">"فعال"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"کیفیت ویدئو"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"بالا"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"کم"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"زمان سپری شده"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"تنظیمات دوربین"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"تنظیمات دوربین فیلمبرداری"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"اندازه تصویر"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"۸ مگاپیکسل"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"۵ مگاپیکسل"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"۳ مگاپیکسل"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"۲ مگاپیکسل"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"۱.۳ مگاپیکسل"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"۱ مگاپیکسل"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"حالت فوکوس"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"خودکار"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"بی نهایت"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"ماکرو"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"حالت فلاش"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"خودکار"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"روشن"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"خاموش"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"توازن سفیدی"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"خودکار"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"تابان"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"روشنایی روز"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"فلورسنت"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"ابری"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"حالت منظره"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"خودکار"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"عملکرد"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"شب"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"غروب آفتاب"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"طرف مقابل"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"در حالت صحنه قابل انتخاب نیست."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"نوردهی"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"تأیید"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"حافظهٔ USB پر است. تنظیمات کیفیت را تغییر دهید یا برخی تصاویر یا سایر فایل‌ها را حذف کنید."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"کارت SD شما پر شده است. تنظیمات کیفیت را تغییر دهید یا برخی تصاویر یا فایل‌های دیگر را حذف کنید."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"بیش از حداکثر مجاز."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"بسیار سریع"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"تهیه پانوراما"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"ذخیره پانوراما امکان‌پذیر نیست."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"پانوراما"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"گرفتن عکس پانوراما"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"در حال انتظار برای پانورامای قبلی"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"در حال ذخیره..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"در حال تولید تصویر پانوراما"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"برای فوکوس لمس کنید."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"جلوه‌ها"</string>
+    <string name="effect_none" msgid="3601545724573307541">"هیچکدام"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"کوچک کردن"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"چشمان بزرگ"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"دهان بزرگ"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"دهان کوچک"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"بینی بزرگ"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"چشمان کوچک"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"در فضا"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"غروب آفتاب"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"ویدئوی شما"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"دستگاه خود را کنار بگذارید."\n"برای یک لحظه از دید خارج شوید."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"برای گرفتن عکس در هنگام ضبط لمس کنید."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"ضبط ویدیو شروع شد."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"ضبط ویدیو متوقف شد."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"هنگام فعال بودن جلوه‌های ویژه، عکس فوری از ویدئو غیرفعال است."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"پاک کردن جلوه‌ها"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"چهره‌های احمقانه"</string>
+    <string name="effect_background" msgid="6579360207378171022">"پس‌زمینه"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"دکمه شاتر"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"دکمه منو"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"جدیدترین عکس"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"کلید دوربین جلو و عقب"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"انتخابگر دوربین، ویدئو یا پانوراما"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"کنترل‌های تنظیم بیشتر"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"بستن کنترل‌های تنظیم"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"کنترل بزرگنمایی"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"کاهش %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"افزایش %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"کادر انتخاب %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"رفتن به عکس"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"رفتن به ویدئو"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"رفتن به پانوراما"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"تغییر به پانورامای جدید"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"لغو بازبینی"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"بازبینی انجام شد"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"بازبینی عکس مجدد"</string>
+    <string name="capital_on" msgid="5491353494964003567">"روشن"</string>
+    <string name="capital_off" msgid="7231052688467970897">"خاموش"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"خاموش"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"۰.۵ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"۱ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"۱.۵ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"۲ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"۲.۵ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"۳ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"۴ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"۵ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"۶ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"۱۰ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"۱۲ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"۱۵ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"۲۴ ثانیه"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"۰.۵ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"۱ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"۱.۵ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"۲ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"۲.۵ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"۳ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"۴ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"۵ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"۶ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"۱۰ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"۱۲ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"۱۵ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"۲۴ دقیقه"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"۰.۵ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"۱ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"۱.۵ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"۲ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"۲.۵ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"۳ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"۴ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"۵ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"۶ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"۱۰ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"۱۲ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"۱۵ ساعت"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"۲۴ ساعت"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"ثانیه"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"دقیقه"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ساعت"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"انجام شد"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"تنظیم فاصله زمانی"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"ویژگی زمان سپری شده خاموش است. برای تنظیم فاصله زمانی آن را روشن کنید."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"تایمر شمارش معکوس خاموش است. قبل از گرفتن تصویر آن را برای شمارش معکوس روشن کنید."</string>
+    <string name="set_duration" msgid="5578035312407161304">"تنظیم مدت زمان به ثانیه"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"شمارش معکوس برای گرفتن یک عکس"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"موقعیت مکانی عکس به‌خاطر سپرده شود؟"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"محل گرفتن عکس‌ها و ویدیوها را به آنها برچسب کنید."\n\n" سایر برنامه‌ها می‌توانند به این اطلاعات در کنار تصاویر ذخیره شده شما دسترسی پیدا کنند."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"نه متشکرم"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"بله"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"دوربین"</string>
+    <string name="menu_search" msgid="7580008232297437190">"جستجو"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"عکس‌ها"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"آلبوم‌ها"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d عکس"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d عکس "</item>
+  </plurals>
+</resources>
diff --git a/res/values-fi/filtershow_strings.xml b/res/values-fi/filtershow_strings.xml
new file mode 100644
index 0000000..0ad3542
--- /dev/null
+++ b/res/values-fi/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Kuvanmuokkaussovellus"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Kuvaa ei voi ladata."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Asetetaan taustakuvaa"</string>
+    <string name="original" msgid="3524493791230430897">"Alkuperäinen"</string>
+    <string name="borders" msgid="2067345080568684614">"Reunukset"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Kumoa"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Toista"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Näytä historia"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Piilota historia"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Näytä kuvan tila"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Piilota kuvan tila"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Asetukset"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Tähän kuvaan on tehty muutoksia, joita ei ole tallennettu."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Haluatko tallentaa ennen sulkemista?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Tallenna ja sulje"</string>
+    <string name="exit" msgid="242642957038770113">"Sulje"</string>
+    <string name="history" msgid="455767361472692409">"Historia"</string>
+    <string name="reset" msgid="9013181350779592937">"Palauta"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Käytetyt tehosteet"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Vertaa"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Käytä"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Palauta"</string>
+    <string name="aspect" msgid="4025244950820813059">"Kuvasuhde"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ei mitään"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Kiinteä"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Pikkuplaneetta"</string>
+    <string name="exposure" msgid="6526397045949374905">"Valotus"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Terävyys"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrasti"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Värien eloisuus"</string>
+    <string name="saturation" msgid="7026791551032438585">"Värikylläisyys"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"MV-suodin"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autom. värit"</string>
+    <string name="hue" msgid="6231252147971086030">"Sävy"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Tummat alueet"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Valokohdat"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Valotuskäyrät"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinjetti"</string>
+    <string name="redeye" msgid="4508883127049472069">"Punasilmäisyys"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Piirrä"</string>
+    <string name="straighten" msgid="26025591664983528">"Suorista"</string>
+    <string name="crop" msgid="5781263790107850771">"Rajaa"</string>
+    <string name="rotate" msgid="2796802553793795371">"Kierrä"</string>
+    <string name="mirror" msgid="5482518108154883096">"Peilikuva"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatiivi"</string>
+    <string name="none" msgid="6633966646410296520">"Ei mitään"</string>
+    <string name="edge" msgid="7036064886242147551">"Reunat"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Pienennys"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Punainen"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Vihreä"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Sininen"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Tyyli"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Koko"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Väri"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Viivat"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Tussi"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Roiskeet"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Tyhjennä"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Valitse oma väri"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Valitse väri"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Valitse koko"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
new file mode 100644
index 0000000..6617999
--- /dev/null
+++ b/res/values-fi/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Valokuvakehys"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d.%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d.%2$02d.%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videosoitin"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Ladataan videota…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Ladataan kuvaa..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Tiliä ladataan..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Jatka videon toistoa"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Jatketaanko toistoa kohdasta %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Jatka toistoa"</string>
+    <string name="loading" msgid="7038208555304563571">"Ladataan…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Ei voi ladata"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kuvaa ei voi ladata"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Ei pikkukuvaa"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Aloita alusta"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Aloita koskettamalla kasvoja."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Tallennetaan kuvaa…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Tallennetaan kuvaa albumiin <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="save_error" msgid="6857408774183654970">"Rajattua kuvaa ei voitu tallentaa."</string>
+    <string name="crop_label" msgid="521114301871349328">"Leikkaa kuvaa"</string>
+    <string name="trim_label" msgid="274203231381209979">"Lyhennä videota"</string>
+    <string name="select_image" msgid="7841406150484742140">"Valitse valokuva"</string>
+    <string name="select_video" msgid="4859510992798615076">"Valitse video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Valitse kohde"</string>
+    <string name="select_album" msgid="1557063764849434077">"Valitse albumi"</string>
+    <string name="select_group" msgid="6744208543323307114">"Valitse ryhmä"</string>
+    <string name="set_image" msgid="2331476809308010401">"Aseta kuva"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Aseta taustakuva"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Asetetaan taustakuvaa..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"taustakuvaksi"</string>
+    <string name="delete" msgid="2839695998251824487">"Poista"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Poistetaanko valittu kohde?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Poistetaanko valitut kohteet?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Vahvista"</string>
+    <string name="cancel" msgid="3637516880917356226">"Peruuta"</string>
+    <string name="share" msgid="3619042788254195341">"Jaa"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Jaa panoraama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Jaa kuvana"</string>
+    <string name="deleted" msgid="6795433049119073871">"Poistettu"</string>
+    <string name="undo" msgid="2930873956446586313">"KUMOA"</string>
+    <string name="select_all" msgid="3403283025220282175">"Valitse kaikki"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Poista kaikki valinnat"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diaesitys"</string>
+    <string name="details" msgid="8415120088556445230">"Tiedot"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d/%2$d kohdetta:"</string>
+    <string name="close" msgid="5585646033158453043">"Sulje"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Vaihda kameraan"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d valittu"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d valittu"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d valittu"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d valittu"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d valittu"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d valittu"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d valittu"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d valittu"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d valittu"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Näytä kartalla"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Kierrä vastapäivään"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Kierrä myötäpäivään"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Kohdetta ei löytynyt."</string>
+    <string name="edit" msgid="1502273844748580847">"Muokkaa"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Käsitellään välimuistipyyntöjä"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Vie välimuist."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Rajaa"</string>
+    <string name="trim_action" msgid="703098114452883524">"Leikkaa"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Mykistä"</string>
+    <string name="set_as" msgid="3636764710790507868">"Aseta"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Videon mykistys epäonnistui."</string>
+    <string name="video_err" msgid="7003051631792271009">"Videon toistaminen epäonnistui."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Sijainnin mukaan"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Ajan mukaan"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Tunnisteiden mukaan"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Ihmiset"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Albumin mukaan"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Koon mukaan"</string>
+    <string name="untagged" msgid="7281481064509590402">"Merkitsemättömät"</string>
+    <string name="no_location" msgid="4043624857489331676">"Ei sijaintia"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Verkkoyhteysongelma – joitakin paikkoja ei voi tunnistaa."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Albumin valokuvia ei voi ladata. Yritä myöhemmin uudelleen."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Vain kuvat"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Vain videot"</string>
+    <string name="show_all" msgid="6963292714584735149">"Kuvat ja videot"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Kuvagalleria"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Ei valokuvia."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Rajattu kuva tallennettu kansioon <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Ei albumeja käytettävissä."</string>
+    <string name="empty_album" msgid="4542880442593595494">"0 kuvaa/videota saatavilla."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Julkaisut"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Aseta offline-käytettäväksi"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Päivitä"</string>
+    <string name="done" msgid="217672440064436595">"Valmis"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d kohdetta:"</string>
+    <string name="title" msgid="7622928349908052569">"Nimi"</string>
+    <string name="description" msgid="3016729318096557520">"Kuvaus"</string>
+    <string name="time" msgid="1367953006052876956">"Aika"</string>
+    <string name="location" msgid="3432705876921618314">"Sijainti"</string>
+    <string name="path" msgid="4725740395885105824">"Polku"</string>
+    <string name="width" msgid="9215847239714321097">"Leveys"</string>
+    <string name="height" msgid="3648885449443787772">"Korkeus"</string>
+    <string name="orientation" msgid="4958327983165245513">"Suunta"</string>
+    <string name="duration" msgid="8160058911218541616">"Kesto"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-tyyppi"</string>
+    <string name="file_size" msgid="8486169301588318915">"Tiedoston koko"</string>
+    <string name="maker" msgid="7921835498034236197">"Tekijä"</string>
+    <string name="model" msgid="8240207064064337366">"Malli"</string>
+    <string name="flash" msgid="2816779031261147723">"Salama"</string>
+    <string name="aperture" msgid="5920657630303915195">"Aukko"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Polttoväli"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Valkotasapaino"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Valotusaika"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manuaalinen"</string>
+    <string name="auto" msgid="4296941368722892821">"Autom."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Salama käyt."</string>
+    <string name="flash_off" msgid="1445443413822680010">"Ei salamaa"</string>
+    <string name="unknown" msgid="3506693015896912952">"Tuntematon"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Alkuperäinen"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vanha"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Pika"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Valkaisu"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Sininen"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"MV"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Isku"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X-prosessi"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Asetetaan albumeja offline-käyttöön."</item>
+    <item quantity="other" msgid="4948604338155959389">"Asetetaan albumeja offline-käyttöön."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Tämä kohde on tallennettu laitteelle ja käytettävissä offline-tilassa."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Kaikki albumit"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Paikalliset albumit"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-laitteet"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa-albumit"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> vapaana"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> tai alle"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> tai yli"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> – <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Tuo"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Tuonti valmis"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Tuonti ei onnistu"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera yhdistetty."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera irrotettu."</string>
+    <string name="click_import" msgid="6407959065464291972">"Tuo koskettamalla tätä"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Valitse albumi"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Sekoita kaikki kuvat"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Valitse kuva"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Valitse kuvat"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Diaesitys"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumit"</string>
+    <string name="times" msgid="2023033894889499219">"Kerrat"</string>
+    <string name="locations" msgid="6649297994083130305">"Sijainnit"</string>
+    <string name="people" msgid="4114003823747292747">"Henkilöt"</string>
+    <string name="tags" msgid="5539648765482935955">"Tunnisteet"</string>
+    <string name="group_by" msgid="4308299657902209357">"Ryhmittely:"</string>
+    <string name="settings" msgid="1534847740615665736">"Asetukset"</string>
+    <string name="add_account" msgid="4271217504968243974">"Lisää tili"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Lataus"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Muokatut verkkokuvat"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Tuonti"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Kuvakaappaus"</string>
+    <string name="help" msgid="7368960711153618354">"Ohje"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Ei tallennustilaa"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Ei ulkoista tallennustilaa"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filminäkymä"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Ruudukkonäkymä"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Koko ruudun näkymä"</string>
+    <string name="trimming" msgid="9122385768369143997">"Leikataan"</string>
+    <string name="muting" msgid="5094925919589915324">"Mykistetään"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Odota"</string>
+    <string name="save_into" msgid="9155488424829609229">"Tallennetaan videota albumiin <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Ei voi leikata: kohdevideo on liian lyhyt"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panoraamaa hahmonnetaan"</string>
+    <string name="save" msgid="613976532235060516">"Tallenna"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Skannataan sisältöä..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d kohdetta skannattu"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d kohde skannattu"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d kohdetta skannattu"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Järjestellään..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skannaus valmis"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Tuodaan..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Tällä laitteella ei ole tuotavaa sisältöä."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"MTP-laitetta ei ole yhdistetty"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kameravirhe"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Ei saada yhteyttä kameraan."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kamera on poistettu käytöstä suojauskäytäntöjen vuoksi."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Odota…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Ota USB-tallennustila käyttöön ennen kameran käyttöä."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Aseta SD-kortti ennen kameran käyttämistä."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Valmistellaan USB-tilaa..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Valmistellaan SD-korttia…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"USB-tallennuslaitetta ei voi käyttää."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"SD-korttia ei voi käyttää."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"PERUUTA"</string>
+    <string name="review_ok" msgid="1156261588693116433">"VALMIS"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Intervallikuvauksen tallennus"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Valitse kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Takaisin"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Etupuoli"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Tallennussijainti"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Ajastin"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sekunti"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d sekuntia"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Ajastimen ääni"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Pois käytöstä"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Käytössä"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videon laatu"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Korkea"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Alhainen"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Intervallikuvaus"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kameran asetukset"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Videokameran asetukset"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Kuvan koko"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapikseliä"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapikseliä"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapikseliä"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapikseliä"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapiks."</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapikseli"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Tarkennustila"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automaattinen"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Loputon"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Salaman tila"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automaattinen"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Käytössä"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Pois käytöstä"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Valkotasapaino"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automaattinen"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Hehkulamppuvalo"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Päivänvalo"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Loisteputkivalo"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Pilvinen"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Kuvaustila"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automaattinen"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Toiminta"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Yö"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Auringonlasku"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Juhlat"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Ei valittavissa kuvaustilassa."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Valotus"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USB-tallennustilasi on vähissä. Vaihda laatuasetusta tai poista kuvia tai muita tiedostoja."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"SD-korttisi tila on vähissä. Vaihda laatuasetusta tai poista kuvia tai muita tiedostoja."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Videon koko on suurin mahdollinen."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Liian nopea"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Valmistellaan panoraamaa"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panoraaman tallennus epäonnistui."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panoraama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Panoraamakuvaus"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Odotetaan edellistä panoraamaa"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Tallennetaan…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panoraamaa hahmonnetaan"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Tarkenna koskettamalla."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Tehosteet"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ei mitään"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Litistetty"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Isot silmät"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Iso suu"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Pieni suu"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Iso nenä"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Pienet silmät"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Avaruus"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Auringonlasku"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Oma videosi"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Aseta laite alustalle."\n"Astu hetkeksi pois kuvasta."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Ota kuva tallennuksen aikana koskettamalla ruutua."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Videon tallennus on alkanut."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Videon tallennus on pysäytetty."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Videon kuvakaappausta ei voi käyttää erikoistehosteiden kanssa."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Tyhjennä tehosteet"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"HAUSKAT KASVOT"</string>
+    <string name="effect_background" msgid="6579360207378171022">"TAUSTA"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Laukaisupainike"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Valikkopainike"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Viimeisin valokuva"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Etu- ja takakamerakytkin"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kamera-, video- tai panoraamavalitsin"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Lisää asetuksia"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Sulje asetukset"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoomauksen hallinta"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Vähennä %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Kasvata %1$s-arvoa"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s -valintaruutu"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Vaihda kuvatilaan"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Vaihda videotilaan"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Vaihda panoraamatilaan"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Vaihda uuteen panoraamaan"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Peruuta"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Tarkistus valmis"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Kuvaa uudelleen"</string>
+    <string name="capital_on" msgid="5491353494964003567">"KÄYTÖSSÄ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"POIS KÄYTÖSTÄ"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Pois käytöstä"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekunti"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuutti"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minuuttia"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 tunti"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 tuntia"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 tuntia"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekuntia"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minuuttia"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"tuntia"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Valmis"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Aseta aikaväli"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Intervallikuvaus ei ole käytössä. Ota ominaisuus käyttöön, jos haluat määrittää aikavälin."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Ajastin ei ole käytössä. Ota se käyttöön, jos haluat ottaa kuvan ajastimella."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Aseta kesto sekunteina"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Kuva otetaan, kun ajastin saavuttaa nollan"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Muistetaanko valokuvien sijainnit?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Merkitse valokuviin ja videoihin niiden kuvauspaikat."\n\n"Muut sovellukset voivat käyttää näitä tietoja tallennettujen kuviesi yhteydessä."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Ei kiitos"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Kyllä"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Haku"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Kuvat"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumit"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d valokuva"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d valokuvaa"</item>
+  </plurals>
+</resources>
diff --git a/res/values-fr/filtershow_strings.xml b/res/values-fr/filtershow_strings.xml
new file mode 100644
index 0000000..c77817c
--- /dev/null
+++ b/res/values-fr/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Outil de retouche photo"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Impossible de charger l\'image."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Définition du fond d\'écran en cours…"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Contours"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Annuler"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Rétablir"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Afficher historique"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Masquer l\'historique"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Afficher état image"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Masquer état image"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Paramètres"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Certaines modifications apportées à l\'image n\'ont pas été enregistrées."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Souhaitez-vous enregistrer les modifications avant de fermer le programme ?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Enregistrer et quitter"</string>
+    <string name="exit" msgid="242642957038770113">"Quitter"</string>
+    <string name="history" msgid="455767361472692409">"Historique"</string>
+    <string name="reset" msgid="9013181350779592937">"Réinitialiser"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Effets appliqués"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Comparer"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Appliquer"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Réinitialiser"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspect"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Aucun"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fixe"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Petite planète"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposition"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Netteté"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contraste"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Couleurs vives"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturation"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtre N&amp;B"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Coloration auto"</string>
+    <string name="hue" msgid="6231252147971086030">"Teinte"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Ombres"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Reflets"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Courbes"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignetage"</string>
+    <string name="redeye" msgid="4508883127049472069">"Yeux rouges"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Dessiner"</string>
+    <string name="straighten" msgid="26025591664983528">"Redresser"</string>
+    <string name="crop" msgid="5781263790107850771">"Rogner"</string>
+    <string name="rotate" msgid="2796802553793795371">"Rotation"</string>
+    <string name="mirror" msgid="5482518108154883096">"Miroir"</string>
+    <string name="negative" msgid="6998313764388022201">"Négatif"</string>
+    <string name="none" msgid="6633966646410296520">"Aucun"</string>
+    <string name="edge" msgid="7036064886242147551">"Bords"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Réduction"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RVB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rouge"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Vert"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Bleu"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Style"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Taille"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Couleur"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Lignes"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marqueur"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Éclaboussures"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Effacer"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Sélection couleur personnalisée"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Sélectionner couleur"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Sélectionner la taille"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
new file mode 100644
index 0000000..d4e9132
--- /dev/null
+++ b/res/values-fr/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerie"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Cadre d\'image"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Lecteur Google Vidéos"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Chargement de la vidéo..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Chargement de l\'image..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Chargement infos compte..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Reprendre la vidéo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Reprendre la lecture à partir de %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Reprendre la lecture"</string>
+    <string name="loading" msgid="7038208555304563571">"Chargement en cours…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Impossible de charger"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Impossible de charger l\'image."</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Aucune vignette"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Démarrer"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Appuyer sur un visage pour commencer"</string>
+    <string name="saving_image" msgid="7270334453636349407">"Enregistrement de l\'image"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Enregistrement de l\'image dans l\'album <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Impossible d\'enregistrer l\'image rognée."</string>
+    <string name="crop_label" msgid="521114301871349328">"Rogner l\'image"</string>
+    <string name="trim_label" msgid="274203231381209979">"Découper la vidéo"</string>
+    <string name="select_image" msgid="7841406150484742140">"Sélectionner photo"</string>
+    <string name="select_video" msgid="4859510992798615076">"Sélectionner vidéo"</string>
+    <string name="select_item" msgid="2816923896202086390">"Sélectionner élément"</string>
+    <string name="select_album" msgid="1557063764849434077">"Sélectionner album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Sélectionnez groupe"</string>
+    <string name="set_image" msgid="2331476809308010401">"Utiliser l\'image comme"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Définir fond d\'écran"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Définition du fond d\'écran…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fond d\'écran"</string>
+    <string name="delete" msgid="2839695998251824487">"Supprimer"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Supprimer élément sélectionné ?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Suppr. éléments sélectionnés ?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmer"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annuler"</string>
+    <string name="share" msgid="3619042788254195341">"Partager"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Partager la vue panoramique"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Partager en tant que photo"</string>
+    <string name="deleted" msgid="6795433049119073871">"Supprimée"</string>
+    <string name="undo" msgid="2930873956446586313">"ANNULER"</string>
+    <string name="select_all" msgid="3403283025220282175">"Tout sélectionner"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Tout désélectionner"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diaporama"</string>
+    <string name="details" msgid="8415120088556445230">"Détails"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d élément(s) sur %2$d :"</string>
+    <string name="close" msgid="5585646033158453043">"Fermer"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Passer en mode Appareil photo"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d sélectionné"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d sélectionné"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d sélectionnés"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d sélectionné"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d sélectionné"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d sélectionnés"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d sélectionné"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d sélectionné"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d sélectionnés"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Afficher sur la carte"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Faire pivoter à gauche"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Faire pivoter à droite"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Impossible de trouver l\'élément."</string>
+    <string name="edit" msgid="1502273844748580847">"Retoucher"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Demandes de mise en cache en cours de traitement"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Mise en cache…"</string>
+    <string name="crop_action" msgid="3427470284074377001">"Rogner"</string>
+    <string name="trim_action" msgid="703098114452883524">"Rogner"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Couper le son"</string>
+    <string name="set_as" msgid="3636764710790507868">"Définir comme"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Impossible de couper le son."</string>
+    <string name="video_err" msgid="7003051631792271009">"Impossible de lire la vidéo."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Par emplacement"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Par date"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Par tag"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Par personnes"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Par album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Par taille"</string>
+    <string name="untagged" msgid="7281481064509590402">"Aucun tag"</string>
+    <string name="no_location" msgid="4043624857489331676">"Aucun lieu"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Certains lieux n\'ont pas pu être identifiés en raison de problèmes réseau."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Impossible de télécharger les photos de cet album. Veuillez réessayer ultérieurement."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Images uniquement"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Vidéos uniquement"</string>
+    <string name="show_all" msgid="6963292714584735149">"Images et vidéos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galerie photos"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Aucune photo"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Image rognée enregistrée dans <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Aucun album disponible."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Aucune image/vidéo disponible."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Posts"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Consulter hors connexion"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Actualiser"</string>
+    <string name="done" msgid="217672440064436595">"OK"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d élément(s) sur %2$d :"</string>
+    <string name="title" msgid="7622928349908052569">"Titre"</string>
+    <string name="description" msgid="3016729318096557520">"Description"</string>
+    <string name="time" msgid="1367953006052876956">"Heure"</string>
+    <string name="location" msgid="3432705876921618314">"Lieu"</string>
+    <string name="path" msgid="4725740395885105824">"Chemin d\'accès"</string>
+    <string name="width" msgid="9215847239714321097">"Largeur"</string>
+    <string name="height" msgid="3648885449443787772">"Hauteur"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientation"</string>
+    <string name="duration" msgid="8160058911218541616">"Durée"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Type MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Taille fichier"</string>
+    <string name="maker" msgid="7921835498034236197">"Auteur"</string>
+    <string name="model" msgid="8240207064064337366">"Modèle"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Ouverture"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Longueur focale"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balance blancs"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Durée d\'expo"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manuel"</string>
+    <string name="auto" msgid="4296941368722892821">"Automatique"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash déclenché"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Flash désactivé"</string>
+    <string name="unknown" msgid="3506693015896912952">"Inconnue"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instantané"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Décoloration"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Bleu"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Noir et blanc"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Intense"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Rayons X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Lithographie"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Activation de consultation d\'album hors connexion."</item>
+    <item quantity="other" msgid="4948604338155959389">"Activation consultation d\'albums hors connexion."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Cet élément est stocké localement et disponible hors connexion."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Tous les albums"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Albums en local"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Appareils MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Albums Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> disponible(s)"</string>
+    <string name="size_below" msgid="2074956730721942260">"jusqu\'à <xliff:g id="SIZE">%1$s</xliff:g>"</string>
+    <string name="size_above" msgid="5324398253474104087">"au-delà de <xliff:g id="SIZE">%1$s</xliff:g>"</string>
+    <string name="size_between" msgid="8779660840898917208">"de <xliff:g id="MIN_SIZE">%1$s</xliff:g> à <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importer"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importation terminée"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Échec de l\'importation."</string>
+    <string name="camera_connected" msgid="916021826223448591">"Appareil photo connecté."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Appareil photo déconnecté."</string>
+    <string name="click_import" msgid="6407959065464291972">"Appuyez ici pour importer"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Sélectionner un album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Affichage aléatoire des images"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Sélectionner une image"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Sélectionner images"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Diaporama"</string>
+    <string name="albums" msgid="7320787705180057947">"Albums"</string>
+    <string name="times" msgid="2023033894889499219">"Heures"</string>
+    <string name="locations" msgid="6649297994083130305">"Lieux"</string>
+    <string name="people" msgid="4114003823747292747">"Contacts"</string>
+    <string name="tags" msgid="5539648765482935955">"Tags"</string>
+    <string name="group_by" msgid="4308299657902209357">"Regrouper par"</string>
+    <string name="settings" msgid="1534847740615665736">"Paramètres"</string>
+    <string name="add_account" msgid="4271217504968243974">"Ajouter un compte"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Appareil photo"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Téléchargements"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Photos en ligne retouchées"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importations"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captures d\'écran"</string>
+    <string name="help" msgid="7368960711153618354">"Aide"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Aucune mémoire"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Aucune mémoire de stockage externe disponible."</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Pellicule"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Grille"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Affichage plein écran"</string>
+    <string name="trimming" msgid="9122385768369143997">"Découpe en cours"</string>
+    <string name="muting" msgid="5094925919589915324">"Coupure du son"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Veuillez patienter."</string>
+    <string name="save_into" msgid="9155488424829609229">"Enregistrement de la vidéo dans l\'album \"<xliff:g id="ALBUM_NAME">%1$s</xliff:g>\" en cours…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Découpe impossible : la vidéo cible est trop courte."</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Rendu de la vue panoramique en cours…"</string>
+    <string name="save" msgid="613976532235060516">"Enregistrer"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Analyse du contenu en cours..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d éléments analysés"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d élément analysé"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d éléments analysés"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Tri en cours..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Analyse terminée."</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importation en cours..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Aucun contenu n\'est disponible pour une importation sur cet appareil."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Aucun appareil MTP n\'est connecté."</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Erreur de l\'appareil photo."</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Impossible d\'établir une connexion avec l\'appareil photo."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"L\'appareil photo a été désactivé en raison des règles de sécurité."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Appareil photo"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Caméra"</string>
+    <string name="wait" msgid="8600187532323801552">"Veuillez patienter..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Veuillez installer une mémoire de stockage USB avant d\'utiliser l\'appareil photo."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Veuillez insérer une carte SD avant d\'utiliser l\'appareil photo."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Préparation mémoire de stockage USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Préparation de la carte SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Impossible d\'accéder à la mémoire de stockage USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Impossible d\'accéder à la carte SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ANNULER"</string>
+    <string name="review_ok" msgid="1156261588693116433">"OK"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Enregistrement mode time lapse"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Sélectionner caméra"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Arrière"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Avant"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Enregist. position"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Compte à rebours"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 seconde"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d secondes"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Compte à rebours sonore"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Désactivé"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Activé"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Qualité vidéo"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Élevée"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Faible"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Intervalle de temps"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Paramètres appareil photo"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Mode Caméra"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Taille d\'image"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 mégapixels"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 M pixels"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Mise au point"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infini"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Mode Flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatique"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Activé"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Désactivé"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balance des blancs"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatique"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescent"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Lumière du jour"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescent"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nuageux"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Mode Scène"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatique"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Action"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nuit"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Coucher de soleil"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Fête"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Ce paramètre ne peut pas être sélectionné en mode Scène."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exposition"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"La mémoire de stockage USB est pleine. Définissez la qualité sur une valeur plus basse ou supprimez des images ou d\'autres types de fichiers."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Votre carte SD est pleine. Modifiez le paramètre de qualité ou supprimez des images ou des fichiers."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Taille maximale atteinte."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Trop rapide"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Préparation vue panoramique..."</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Impossible d\'enregistrer le panoramique."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panoramique"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Capture vue panoramique…"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Vue panoramique précédente en attente"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Enreg…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Rendu de vue panoramique…"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Appuyez pour mise au point."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effets"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Aucun"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Compresser"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Grands yeux"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Grande bouche"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Petite bouche"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Gros nez"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Petits yeux"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Dans l\'espace"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Coucher soleil"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Votre vidéo"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Posez votre appareil."\n"Sortez du cadre quelques instants."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Appuyez pour prendre une photo pendant l\'enregistrement"</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"L\'enregistrement vidéo a commencé."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"L\'enregistrement vidéo s\'est arrêté."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Instantané vidéo désactivé en cas d\'activation des effets spéciaux."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Effacer les effets"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"DRÔLES DE TÊTES"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ARRIÈRE-PLAN"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Bouton de l\'obturateur"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Bouton \"Menu\""</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Photo la plus récente"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Interrupteur des caméras avant et arrière"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Sélecteur du mode Photo, Vidéo ou Panoramique"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Plus de paramètres"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Fermer les paramètres"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Contrôle du zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Diminuer %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Augmenter %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Case à cocher %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Mode Appareil photo"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Mode vidéo"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Mode panoramique"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Passer au nouveau mode panoramique"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Examen – Annuler"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Examen – OK"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Examiner la deuxième prise de la photo"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ACTIVÉ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"DÉSACTIVÉ"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Désactivé"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"1 demi-seconde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 seconde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1 seconde et demie"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2 secondes et demie"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 secondes"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"1 demi-minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1 minute et demie"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2 minutes et demie"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"1 demi-heure"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 heure"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1 heure et demie"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2 heures et demie"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 heures"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 heures"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"secondes"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutes"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"heures"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"OK"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Définir l\'intervalle de temps"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"La fonctionnalité Time Lapse est désactivée. Veuillez l\'activer pour définir un intervalle."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"La fonctionnalité de compte à rebours est désactivée. Activez-la pour effectuer un compte à rebours avant de prendre une photo."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Définir la durée en secondes"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Compte à rebours avant la photo"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Se souvenir du lieu des photos ?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Ajoutez des tags à vos photos et à vos vidéos pour identifier l\'endroit de la prise de vue."\n\n"D\'autres applications peuvent accéder à ces informations, ainsi qu\'aux images enregistrées."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Non, merci"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Oui"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Appareil photo"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Recherche"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Photos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albums"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d photo"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d photos"</item>
+  </plurals>
+</resources>
diff --git a/res/values-hi/filtershow_strings.xml b/res/values-hi/filtershow_strings.xml
new file mode 100644
index 0000000..99d5e55
--- /dev/null
+++ b/res/values-hi/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"फ़ोटो संपादक"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"चित्र लोड नहीं हो सकता!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"वॉलपेपर सेट हो रहा है"</string>
+    <string name="original" msgid="3524493791230430897">"मूल"</string>
+    <string name="borders" msgid="2067345080568684614">"बॉर्डर"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"पूर्ववत करें"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"फिर से करें"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"इतिहास दिखाएं"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"इतिहास छिपाएं"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"चित्र स्थिति दिखाएं"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"चित्र स्थिति छिपाएं"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"सेटिंग"</string>
+    <string name="unsaved" msgid="8704442449002374375">"इस चित्र में न सहेजे गए बदलाव हैं."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"क्या आप बाहर निकलने के पहले सहेजना चाहते हैं?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"सहेजें और बाहर निकलें"</string>
+    <string name="exit" msgid="242642957038770113">"बाहर निकलें"</string>
+    <string name="history" msgid="455767361472692409">"इतिहास"</string>
+    <string name="reset" msgid="9013181350779592937">"रीसेट करें"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"लागू किए गए प्रभाव"</string>
+    <string name="compare_original" msgid="8140838959007796977">"तुलना करें"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"लागू करें"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"रीसेट करें"</string>
+    <string name="aspect" msgid="4025244950820813059">"पहलू"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"कुछ नहीं"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"निर्धारित"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"छोटा ग्रह"</string>
+    <string name="exposure" msgid="6526397045949374905">"एक्सपोज़र"</string>
+    <string name="sharpness" msgid="6463103068318055412">"शार्पनेस"</string>
+    <string name="contrast" msgid="2310908487756769019">"कंट्रास्‍ट"</string>
+    <string name="vibrance" msgid="3326744578577835915">"वाइब्रैंस"</string>
+    <string name="saturation" msgid="7026791551032438585">"संतृप्तता"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"BW फ़िल्टर"</string>
+    <string name="wbalance" msgid="6346581563387083613">"ऑटोकलर"</string>
+    <string name="hue" msgid="6231252147971086030">"ह्यू"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"छाया"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"हाइलाइट"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"वक्र"</string>
+    <string name="vignette" msgid="934721068851885390">"विनेट"</string>
+    <string name="redeye" msgid="4508883127049472069">"रेड आई"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"रेखांकन"</string>
+    <string name="straighten" msgid="26025591664983528">"सीधा करें"</string>
+    <string name="crop" msgid="5781263790107850771">"काट-छांट करें"</string>
+    <string name="rotate" msgid="2796802553793795371">"घुमाएं"</string>
+    <string name="mirror" msgid="5482518108154883096">"दर्पण"</string>
+    <string name="negative" msgid="6998313764388022201">"नेगेटिव"</string>
+    <string name="none" msgid="6633966646410296520">"कुछ नहीं"</string>
+    <string name="edge" msgid="7036064886242147551">"किनारे"</string>
+    <string name="kmeans" msgid="1630263230946107457">"वारहोल"</string>
+    <string name="downsample" msgid="3552938534146980104">"डाउनसेंपल"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"लाल"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"हरा"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"नीला"</string>
+    <string name="draw_style" msgid="2036125061987325389">"शैली"</string>
+    <string name="draw_size" msgid="4360005386104151209">"आकार"</string>
+    <string name="draw_color" msgid="2119030386987211193">"रंग"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"रेखाएं"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"मार्कर"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"स्पैटर"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"साफ़ करें"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"कस्टम रंग चुनें"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"रंग चुनें"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"आकार चुनें"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"ठीक"</string>
+</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
new file mode 100644
index 0000000..3fdaf76
--- /dev/null
+++ b/res/values-hi/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"गैलरी"</string>
+    <string name="gadget_title" msgid="259405922673466798">"चित्र फ़्रेम"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"वीडियो प्‍लेयर"</string>
+    <string name="loading_video" msgid="4013492720121891585">"वीडियो लोड हो रहा है..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"चित्र लोड हो रही है…"</string>
+    <string name="loading_account" msgid="928195413034552034">"खाता लोड हो रहा है…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"वीडियो फिर से शुरू करें"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"%s से फिर से चलाना शुरू करें ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"चलाना फिर से शुरू करें"</string>
+    <string name="loading" msgid="7038208555304563571">"लोड हो रहा है..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"लोड नहीं कर सका"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"चित्र लोड नहीं हो सकी"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"कोई थंबनेल नहीं"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"पुन: प्रारंभ करें"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ठीक"</string>
+    <string name="ok" msgid="5296833083983263293">"ठीक"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"शुरू करने के लिए कोई चेहरा स्‍पर्श करें."</string>
+    <string name="saving_image" msgid="7270334453636349407">"चित्र सहेज रहा है…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"चित्र <xliff:g id="ALBUM_NAME">%1$s</xliff:g> में सहेजा जा रहा है …"</string>
+    <string name="save_error" msgid="6857408774183654970">"काट-छांट की गई चित्र को नहीं सहेज सका."</string>
+    <string name="crop_label" msgid="521114301871349328">"चित्र काटें"</string>
+    <string name="trim_label" msgid="274203231381209979">"वीडियो ट्रिम करें"</string>
+    <string name="select_image" msgid="7841406150484742140">"फ़ोटो को चुनें"</string>
+    <string name="select_video" msgid="4859510992798615076">"वीडियो को चुनें"</string>
+    <string name="select_item" msgid="2816923896202086390">"आइटम को चुनें"</string>
+    <string name="select_album" msgid="1557063764849434077">"एल्बम को चुनें"</string>
+    <string name="select_group" msgid="6744208543323307114">"समूह को चुनें"</string>
+    <string name="set_image" msgid="2331476809308010401">"चित्र इस रूप में सेट करें"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"वॉलपेपर सेट करें"</string>
+    <string name="wallpaper" msgid="140165383777262070">"वॉलपेपर सेट कर रहा है..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"वॉलपेपर"</string>
+    <string name="delete" msgid="2839695998251824487">"हटाएं"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"चयनित आइटम हटाएं?"</item>
+    <item quantity="other" msgid="5874316486520635333">"चयनित आइटम हटाएं?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"पुष्टि करें"</string>
+    <string name="cancel" msgid="3637516880917356226">"रद्द करें"</string>
+    <string name="share" msgid="3619042788254195341">"साझा करें"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"पैनोरामा को साझा करें"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"फ़ोटो के रूप में साझा करें"</string>
+    <string name="deleted" msgid="6795433049119073871">"हटाई गई"</string>
+    <string name="undo" msgid="2930873956446586313">"पूर्ववत करें"</string>
+    <string name="select_all" msgid="3403283025220282175">"सभी को चुनें"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"सभी का चयन रद्द करें"</string>
+    <string name="slideshow" msgid="4355906903247112975">"स्लाइडशो"</string>
+    <string name="details" msgid="8415120088556445230">"विवरण"</string>
+    <string name="details_title" msgid="2611396603977441273">"%2$d में से %1$d आइटम:"</string>
+    <string name="close" msgid="5585646033158453043">"बंद करें"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"कैमरे पर जाएं"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d चयनित"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d चयनित"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d चयनित"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d चयनित"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d चयनित"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d चयनित"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d चयनित"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d चयनित"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d चयनित"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"मानचित्र पर दिखाएं"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"बाएं घुमाएं"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"दाएं घुमाएं"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"आइटम नहीं ढूंढा जा सका."</string>
+    <string name="edit" msgid="1502273844748580847">"संपादित करें"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"संचय अनुरोध संसाधित कर रहा है"</string>
+    <string name="caching_label" msgid="4521059045896269095">"संचय कर रहा है..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"काट-छांट करें"</string>
+    <string name="trim_action" msgid="703098114452883524">"ट्रिम करें"</string>
+    <string name="mute_action" msgid="5296241754753306251">"म्यूट करें"</string>
+    <string name="set_as" msgid="3636764710790507868">"इस रूप में सेट करें"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"वीडियो म्यूट नहीं हो सकता."</string>
+    <string name="video_err" msgid="7003051631792271009">"वीडियो नहीं चल सकता."</string>
+    <string name="group_by_location" msgid="316641628989023253">"स्थान द्वारा"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"समय द्वारा"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"टैग द्वारा"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"लोगों द्वारा"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"एल्बम द्वारा"</string>
+    <string name="group_by_size" msgid="153766174950394155">"आकार द्वारा"</string>
+    <string name="untagged" msgid="7281481064509590402">"टैग नहीं किया गया"</string>
+    <string name="no_location" msgid="4043624857489331676">"कोई स्थान नहीं"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"नेटवर्क समस्‍याओं के कारण कुछ स्‍थानों को पहचाना नहीं जा सका."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"इस एल्बम के फ़ोटो डाउनलोड नहीं किए जा सके. बाद में पुन: प्रयास करें."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"केवल छवियां"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"केवल वीडियो"</string>
+    <string name="show_all" msgid="6963292714584735149">"छवियां और वीडियो"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"फ़ोटो गैलरी"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"कोई फ़ोटो नहीं."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"काट-छांट की गई चित्र को <xliff:g id="FOLDER_NAME">%s</xliff:g> में सहेजा गया."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"कोई एल्‍बम उपलब्‍ध नहीं."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O छवियां/वीडियो उपलब्‍ध."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"पोस्ट"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"ऑफ़लाइन उपलब्ध कराएं"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"रीफ्रेश करें"</string>
+    <string name="done" msgid="217672440064436595">"पूर्ण"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%2$d में से %1$d आइटम:"</string>
+    <string name="title" msgid="7622928349908052569">"शीर्षक"</string>
+    <string name="description" msgid="3016729318096557520">"विवरण"</string>
+    <string name="time" msgid="1367953006052876956">"समय"</string>
+    <string name="location" msgid="3432705876921618314">"स्थान"</string>
+    <string name="path" msgid="4725740395885105824">"पथ"</string>
+    <string name="width" msgid="9215847239714321097">"चौड़ाई"</string>
+    <string name="height" msgid="3648885449443787772">"ऊंचाई"</string>
+    <string name="orientation" msgid="4958327983165245513">"अभिविन्‍यास"</string>
+    <string name="duration" msgid="8160058911218541616">"अवधि"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME प्रकार"</string>
+    <string name="file_size" msgid="8486169301588318915">"फ़ाइल का आकार"</string>
+    <string name="maker" msgid="7921835498034236197">"निर्माता"</string>
+    <string name="model" msgid="8240207064064337366">"मॉडल"</string>
+    <string name="flash" msgid="2816779031261147723">"फ़्लैश"</string>
+    <string name="aperture" msgid="5920657630303915195">"एपर्चर"</string>
+    <string name="focal_length" msgid="1291383769749877010">"फ़ोकल लंबाई"</string>
+    <string name="white_balance" msgid="1582509289994216078">"श्वेत संतुलन"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"एक्सपोज़र समय"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"मैन्युअल"</string>
+    <string name="auto" msgid="4296941368722892821">"स्वत:"</string>
+    <string name="flash_on" msgid="7891556231891837284">"फ़्लैश चलाया गया"</string>
+    <string name="flash_off" msgid="1445443413822680010">"कोई फ़्लैश नहीं"</string>
+    <string name="unknown" msgid="3506693015896912952">"अज्ञात"</string>
+    <string name="ffx_original" msgid="372686331501281474">"मूल"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"विंटेज"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"झटपट"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"ब्लीच"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"नीला"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"श्वेत/श्याम"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"पंच"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X प्रक्रिया"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"लैटे"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"लीथो"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"एल्‍बम ऑफ़लाइन उपलब्‍ध कराना."</item>
+    <item quantity="other" msgid="4948604338155959389">"एल्‍बम ऑफ़लाइन उपलब्‍ध कराना."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"यह आइटम स्‍थानीय रूप से संग्रहीत है और ऑफ़लाइन उपलब्‍ध है."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"सभी एल्बम"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"स्थानीय एल्बम"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP उपकरण"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa एल्बम"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> रिक्त"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> या कम"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> या अधिक"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> से <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"आयात करें"</string>
+    <string name="import_complete" msgid="3875040287486199999">"आयात पूर्ण"</string>
+    <string name="import_fail" msgid="8497942380703298808">"आयात विफल"</string>
+    <string name="camera_connected" msgid="916021826223448591">"कैमरा कनेक्‍ट किया गया."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"कैमरा डिस्‍कनेक्‍ट किया गया."</string>
+    <string name="click_import" msgid="6407959065464291972">"आयात करने के लिए यहां स्पर्श करें"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"कोई एल्बम चुनें"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"सभी छवियों का क्रम बदलें"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"कोई चित्र चुनें"</string>
+    <string name="widget_type" msgid="1364653978966343448">"छवियां चुनें"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"स्लाइडशो"</string>
+    <string name="albums" msgid="7320787705180057947">"एल्बम"</string>
+    <string name="times" msgid="2023033894889499219">"इतने बार"</string>
+    <string name="locations" msgid="6649297994083130305">"स्‍थान"</string>
+    <string name="people" msgid="4114003823747292747">"लोग"</string>
+    <string name="tags" msgid="5539648765482935955">"टैग"</string>
+    <string name="group_by" msgid="4308299657902209357">"इसके द्वारा समूहीकृत"</string>
+    <string name="settings" msgid="1534847740615665736">"सेटिंग"</string>
+    <string name="add_account" msgid="4271217504968243974">"खाता जोड़ें"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"कैमरा"</string>
+    <string name="folder_download" msgid="7186215137642323932">"डाउनलोड करें"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"ऑनलाइन संपादित फ़ोटो"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"आयातित"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"स्क्रीनशॉट"</string>
+    <string name="help" msgid="7368960711153618354">"सहायता"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"कोई संग्रहण नहीं"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"कोई बाहरी संग्रहण उपलब्ध नहीं है"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"फ़िल्मस्ट्रिप दृश्य"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"ग्रिड दृश्य"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"पूर्णस्क्रीन दृश्य"</string>
+    <string name="trimming" msgid="9122385768369143997">"ट्रिम कर रहा है"</string>
+    <string name="muting" msgid="5094925919589915324">"म्‍यूट हो रहा है"</string>
+    <string name="please_wait" msgid="7296066089146487366">"कृपया प्रतीक्षा करें"</string>
+    <string name="save_into" msgid="9155488424829609229">"वीडियो को <xliff:g id="ALBUM_NAME">%1$s</xliff:g> में सहेजा जा रहा है …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"ट्रिम नहीं कर सकते : लक्ष्य वीडियो बहुत छोटा है"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"पैनोरामा रेंडर किया जा रहा है"</string>
+    <string name="save" msgid="613976532235060516">"सहेजें"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"सामग्री स्कैन हो रही है..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d आइटम स्कैन किए गए"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d आइटम स्कैन किया गया"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d आइटम स्कैन किए गए"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"क्रमित हो रही है..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"स्कैन करना पूर्ण"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"आयात हो रही है..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"आयात करने के लिए इस उपकरण पर कोई सामग्री उपलब्ध नहीं है."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"कोई MTP उपकरण कनेक्ट नहीं है"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"कैमरा त्रुटि"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"कैमरे से कनेक्‍ट नहीं कर सकता."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"सुरक्षा नीतियों के कारण कैमरा अक्षम कर दिया गया है."</string>
+    <string name="camera_label" msgid="6346560772074764302">"कैमरा"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"कैमकॉर्डर"</string>
+    <string name="wait" msgid="8600187532323801552">"कृपया प्रतीक्षा करें..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"कैमरे का उपयोग करने से पहले USB संग्रहण माउंट करें."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"कैमरे का उपयोग करने से पहले SD कार्ड डालें."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB संग्रहण तैयार कर रहा है…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD कार्ड तैयार कर रहा है…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"USB संग्रहण में नहीं पहुंच सका."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"SD कार्ड में नहीं पहुंच सका."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"रद्द करें"</string>
+    <string name="review_ok" msgid="1156261588693116433">"पूर्ण"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"समय समाप्ति रिकॉर्डिंग"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"कैमरा चुनें"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"वापस जाएं"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"सामने"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"संग्रह स्थान"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"काउंटडाउन टाइमर"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 सेकंड"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d सेकंड"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"उल्टी गिनती के समय बीप"</string>
+    <string name="setting_off" msgid="4480039384202951946">"बंद"</string>
+    <string name="setting_on" msgid="8602246224465348901">"चालू"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"वीडियो गुणवत्ता"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"उच्च"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"निम्न"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"समय अंतराल"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"कैमरा सेटिंग"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"कैमकॉर्डर सेटिंग"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"चित्र आकार"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M पिक्सेल"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M पिक्सेल"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M पिक्सेल"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M पिक्सेल"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3M पिक्सेल"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M पिक्सेल"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"फ़ोकस मोड"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"स्वत:"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"अनंत"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"मैक्रो"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"फ़्लैश मोड"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"स्वत:"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"चालू"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"बंद"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"श्वेत संतुलन"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"स्वत:"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"अत्यधिक चमकीला"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"दिन का प्रकाश"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"फ़्लोरेसेंट"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"बदली"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"दृश्य मोड"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"स्वत:"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"कार्यवाही"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"रात्रि"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"सूर्यास्त"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"पार्टी"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"दृश्य मोड में चयन योग्य नहीं."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"एक्स्पोजर"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"ठीक"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"आपके USB संग्रहण में स्थान कम है. गुणवत्ता सेटिंग बदलें या कुछ छवियां या अन्य फ़ाइलें हटाएं."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"आपके SD कार्ड में स्थान कम है. गुणवत्ता सेटिंग बदलें या कुछ छवियां या अन्य फ़ाइलें हटाएं."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"आकार सीमा पहुंची."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"बहुत तेज़"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"पैनोरामा तैयार हो रहा है"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"पैनोरामा नहीं सहेज सका."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"पैनोरामा"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"पैनोरामा कैप्चर हो रहा है"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"पिछले पैनोरामा की प्रतीक्षा की जा रही है"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"सहेजा जा रहा है..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"पैनोरामा रेंडर हो रहा है"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"फ़ोकस करने हेतु स्‍पर्श करें."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"प्रभाव"</string>
+    <string name="effect_none" msgid="3601545724573307541">"कोई नहीं"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"पिचका हुआ"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"बड़ी आंखें"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"बड़ा मुंह"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"छोटा मुंह"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"बड़ी नाक"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"छोटी आंखें"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"अंतरिक्ष में"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"सूर्यास्त"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"आपका वीडियो"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"अपने उपकरण को सेट करें."\n"कुछ समय के लिए दृश्य से बाहर निकलें."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"रिकॉर्डिंग के दौरान फ़ोटो लेने के लिए स्‍पर्श करें."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"वीडियो रिकॉर्डिंग प्रारंभ हो गई है."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"वीडियो रिकॉर्डिंग रुक गई है."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"विशेष प्रभावों के चालू होने पर वीडियो स्‍नेपशॉट अक्षम हो जाता है."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"प्रभाव साफ़ करें"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"मज़ाकिया चेहरे"</string>
+    <string name="effect_background" msgid="6579360207378171022">"पृष्ठभूमि"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"शटर बटन"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"मेनू बटन"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"हाल ही का फ़ोटो"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"अगला और पिछला कैमरा स्‍विच"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"कैमरा, वीडियो या पैनोरामा चयनकर्ता"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"अधिक सेटिंग नियंत्रण"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"सेटिंग नियंत्रण बंद करें"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"ज़ूम नियंत्रण"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"%1$s घटाएं"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"%1$s बढ़ाएं"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s चेक बॉक्स"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"फ़ोटो पर स्‍विच करें"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"वीडियो पर स्विच करें"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"पैनोरामा पर स्विच करें"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"नए पैनोरामा पर स्विच करें"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"समीक्षा रद्द कर दी गई"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"समीक्षा पूर्ण"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"समीक्षा रीटेक"</string>
+    <string name="capital_on" msgid="5491353494964003567">"चालू"</string>
+    <string name="capital_off" msgid="7231052688467970897">"बंद"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"बंद"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 सेकंड"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 मिनट"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 घंटा"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 घंटा"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 घंटा"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 घंटे"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 घंटे"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"सेकंड"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"मिनट"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"घंटे"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"पूर्ण"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"समय अंतराल सेट करें"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"समय अंतराल सुविधा बंद है. समय अंतराल सेट करने के लिए इसे चालू करें."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"काउंटडाउन टाइमर बंद है. चित्र लेने से पहले उल्‍टी गिनती करने के लिए इसे चालू करें."</string>
+    <string name="set_duration" msgid="5578035312407161304">"अवधि को सेकंड में सेट करें"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"फ़ोटो लेने के लिए उल्टी गिनती कर रहा है"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"फ़ोटो के स्थान याद हैं?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"अपने फ़ोटो और वीडियो को उन स्थानों के साथ टैग करें जहां वे लिए गए हैं."\n\n"अन्य एप्लिकेशन आपके सहेजे गए चित्रों सहित इस जानकारी का उपयोग कर सकते हैं."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"नहीं, धन्‍यवाद"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"हां"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"कैमरा"</string>
+    <string name="menu_search" msgid="7580008232297437190">"खोज"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"फ़ोटो"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"एल्बम"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d फ़ोटो"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d फ़ोटो"</item>
+  </plurals>
+</resources>
diff --git a/res/values-hr/filtershow_strings.xml b/res/values-hr/filtershow_strings.xml
new file mode 100644
index 0000000..7403dd8
--- /dev/null
+++ b/res/values-hr/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Uređivač fotografija"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Nije moguće učitati sliku!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Postavljanje pozadinske slike"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Obrubi"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Poništi"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Ponovi"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Prikaži povijest"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Sakrij povijest"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Prikaži stanje slike"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Sakrij stanje slike"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Postavke"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Neke promjene slike nisu spremljene."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Želite li spremiti prije nego što iziđete?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Spremanje i izlaz"</string>
+    <string name="exit" msgid="242642957038770113">"Izlaz"</string>
+    <string name="history" msgid="455767361472692409">"Povijest"</string>
+    <string name="reset" msgid="9013181350779592937">"Poništi"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Primijenjeni efekti"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Usporedi"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Primijeni"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Poništi"</string>
+    <string name="aspect" msgid="4025244950820813059">"Omjer"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ništa"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fiksno"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Mali planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Ekspozicija"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Oštrina"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Živost boja"</string>
+    <string name="saturation" msgid="7026791551032438585">"Zasićenje"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"CB filtar"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Automatska boja"</string>
+    <string name="hue" msgid="6231252147971086030">"Nijansa"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Sjenke"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Isticanja"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Krivulje"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinjeta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Crvene oči"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Crtanje"</string>
+    <string name="straighten" msgid="26025591664983528">"Poravnanje"</string>
+    <string name="crop" msgid="5781263790107850771">"Obrezivanje"</string>
+    <string name="rotate" msgid="2796802553793795371">"Okretanje"</string>
+    <string name="mirror" msgid="5482518108154883096">"Zrcalo"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Ništa"</string>
+    <string name="edge" msgid="7036064886242147551">"Rubovi"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Smanji"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Crveno"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Zeleno"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Plavo"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Veličina"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Boja"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Crte"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Oznaka"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Prskanje"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Izbriši"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Odabir prilagođene boje"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Odabir boje"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Odabir veličine"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"U redu"</string>
+</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
new file mode 100644
index 0000000..3e3c33a
--- /dev/null
+++ b/res/values-hr/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerija"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Okvir slike"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videoplayer"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Učitavanje videozapisa…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Učitavanje slike…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Učitavanje računa…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Nastavi videozapis"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Nastaviti reprodukciju od %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Nastavak reprodukcije"</string>
+    <string name="loading" msgid="7038208555304563571">"Učitavanje…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nije bilo moguće učitati"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nije moguće učitati sliku"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Nema minijatura"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Počni ispočetka"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"U redu"</string>
+    <string name="ok" msgid="5296833083983263293">"U redu"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Dodirnite lice za početak."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Spremanje slike..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Spremanje slike u album <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Nije moguće spremiti obrezanu sliku."</string>
+    <string name="crop_label" msgid="521114301871349328">"Obrezivanje slike"</string>
+    <string name="trim_label" msgid="274203231381209979">"Skrati videozapis"</string>
+    <string name="select_image" msgid="7841406150484742140">"Odaberite fotog."</string>
+    <string name="select_video" msgid="4859510992798615076">"Odaberite videoz."</string>
+    <string name="select_item" msgid="2816923896202086390">"Odaberite stavku"</string>
+    <string name="select_album" msgid="1557063764849434077">"Odaberi album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Odabir grupe"</string>
+    <string name="set_image" msgid="2331476809308010401">"Postavi sliku kao"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Postavljanje pozadine"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Postavljanje pozadinske slike…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Pozadinska slika"</string>
+    <string name="delete" msgid="2839695998251824487">"Izbriši"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Izbrisati odabranu stavku?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Izbrisati odabrane stavke?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potvrdi"</string>
+    <string name="cancel" msgid="3637516880917356226">"Odustani"</string>
+    <string name="share" msgid="3619042788254195341">"Podijeli"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Dijeli panoramu"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Dijeli kao fotografiju"</string>
+    <string name="deleted" msgid="6795433049119073871">"Izbrisano"</string>
+    <string name="undo" msgid="2930873956446586313">"PONIŠTI"</string>
+    <string name="select_all" msgid="3403283025220282175">"Odaberi sve"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Poništi odabir za sve"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Dijaprojekcija"</string>
+    <string name="details" msgid="8415120088556445230">"Pojedinosti"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d od ukupno stavki: %2$d"</string>
+    <string name="close" msgid="5585646033158453043">"Zatvori"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Prebacivanje na fotoaparat"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Odabrano: %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Odabrano: %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Odabrano: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Odabrano: %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Odabrano: %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Odabrano: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Odabrano: %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Odabrano: %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Odabrano: %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Pokaži na karti"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Rotiraj ulijevo"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rotiraj udesno"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Nije moguće pronaći stavku."</string>
+    <string name="edit" msgid="1502273844748580847">"Uredi"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Obrada zahtjeva za predmemoriju"</string>
+    <string name="caching_label" msgid="4521059045896269095">"U predmemoriju…"</string>
+    <string name="crop_action" msgid="3427470284074377001">"Obreži"</string>
+    <string name="trim_action" msgid="703098114452883524">"Obreži"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Isključi zvuk"</string>
+    <string name="set_as" msgid="3636764710790507868">"Postavi kao"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Nije moguće isključiti zvuk."</string>
+    <string name="video_err" msgid="7003051631792271009">"Nije moguće reproducirati videozapis."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Prema lokaciji"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Po vremenu"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Po oznakama"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Po osobama"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Po albumu"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Po veličini"</string>
+    <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string>
+    <string name="no_location" msgid="4043624857489331676">"Nema lokacije"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Neke lokacije nisu se mogle identificirati zbog poteškoća s mrežnom."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Preuzimanje fotografija iz ovog albuma nije uspjelo. Pokušajte ponovo kasnije."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Samo slike"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Samo videozapisi"</string>
+    <string name="show_all" msgid="6963292714584735149">"Slike i videozapisi"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galerija fotografija"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Nema fotografija."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Obrezana slika spremljena u mapu <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nema dostupnih albuma."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Dostupno je 0 slika/videozapisa."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Postovi"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Učini dostupnim van mreže"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Osvježi"</string>
+    <string name="done" msgid="217672440064436595">"Gotovo"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d od %2$d stavki:"</string>
+    <string name="title" msgid="7622928349908052569">"Naslov"</string>
+    <string name="description" msgid="3016729318096557520">"Opis"</string>
+    <string name="time" msgid="1367953006052876956">"Vrijeme"</string>
+    <string name="location" msgid="3432705876921618314">"Lokacija"</string>
+    <string name="path" msgid="4725740395885105824">"Putanja"</string>
+    <string name="width" msgid="9215847239714321097">"Širina"</string>
+    <string name="height" msgid="3648885449443787772">"Visina"</string>
+    <string name="orientation" msgid="4958327983165245513">"Usmjerenje"</string>
+    <string name="duration" msgid="8160058911218541616">"Trajanje"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Vrsta MIME-a"</string>
+    <string name="file_size" msgid="8486169301588318915">"Vel. datoteke"</string>
+    <string name="maker" msgid="7921835498034236197">"Autor"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Otvor blende"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Žariš. duljina"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balans bijelog"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Vrijeme eksp."</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Ručno"</string>
+    <string name="auto" msgid="4296941368722892821">"Automatski"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Bljes. okinuta"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Bez bljesk."</string>
+    <string name="unknown" msgid="3506693015896912952">"Nepoznato"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Izvornik"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Plavo"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Crno-bijelo"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Bijela kava"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografija"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Spremanje albuma za izvanmrežni rad."</item>
+    <item quantity="other" msgid="4948604338155959389">"Spremanje albuma za izvanmrežni rad."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ova stavka pohranjena je lokalno i dostupna je izvan mreže."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Svi albumi"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Lokalni albumi"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP uređaji"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa albumi"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> slobodno"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ili niže"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ili više"</string>
+    <string name="size_between" msgid="8779660840898917208">"Od <xliff:g id="MIN_SIZE">%1$s</xliff:g> do <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Uvezi"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Uvoz je dovršen"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Uvoz neuspješan"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Fotoaparat je uključen."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Fotoaparat je isključen."</string>
+    <string name="click_import" msgid="6407959065464291972">"Dodirnite ovdje za uvoz"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Odaberi album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Nasumično prikaži sve slike"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Odaberite sliku"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Odabir slika"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Dijaprojekcija"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumi"</string>
+    <string name="times" msgid="2023033894889499219">"Vremena"</string>
+    <string name="locations" msgid="6649297994083130305">"Lokacije"</string>
+    <string name="people" msgid="4114003823747292747">"Osobe"</string>
+    <string name="tags" msgid="5539648765482935955">"Oznake"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupiraj po"</string>
+    <string name="settings" msgid="1534847740615665736">"Postavke"</string>
+    <string name="add_account" msgid="4271217504968243974">"Dodaj račun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparat"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Preuzimanja"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Uređene mrežne fotografije"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Uvezeno"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Snimak zaslona"</string>
+    <string name="help" msgid="7368960711153618354">"Pomoć"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nema pohrane"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nema dostupne vanjske pohrane"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Prikaz filmske vrpce"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Prikaži kao rešetku"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Na cijelom zaslonu"</string>
+    <string name="trimming" msgid="9122385768369143997">"Skraćivanje"</string>
+    <string name="muting" msgid="5094925919589915324">"Isključivanje zvuka"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Pričekajte"</string>
+    <string name="save_into" msgid="9155488424829609229">"Spremanje videozapisa u album <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Nije moguće skratiti: ciljani videozapis prekratak je"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Obrada prikaza panorame"</string>
+    <string name="save" msgid="613976532235060516">"Spremi"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Skeniranje sadržaja..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d skeniranih stavki"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d skenirana stavka"</item>
+    <item quantity="other" msgid="3138021473860555499">"Broj skeniranih stavki: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Razvrstavanje…"</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skeniranje završeno"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Uvoz..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Nema dostupnog sadržaja za uvoz na taj uređaj."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nije povezan nijedan MTP uređaj"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Pogreška fotoaparata"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Povezivanje s fotoaparatom nije moguće."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Fotoaparat je onemogućen zbog sigurnosnih pravila."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Fotoaparat"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Kamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Pričekajte…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Prije upotrebe fotoaparata instalirajte USB memoriju."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Prije upotrebe fotoaparata umetnite SD karticu."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Pripremanje USB memorije..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Priprema SD kartice…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nije moguć pristup USB pohrani."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nije moguć pristup SD kartici."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ODUSTANI"</string>
+    <string name="review_ok" msgid="1156261588693116433">"GOTOVO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Snimanje s vremenskim odmakom"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Odaberite kameru"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Natrag"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Naprijed"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Lokacija pohranjivanja"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Timer za odbrojavanje"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sekunda"</item>
+    <item quantity="other" msgid="6455381617076792481">"Za ovoliko sekundi: %d"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Zvuk pri odbrojavanju"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Isključeno"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Uključeno"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videokvaliteta"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Visoka"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Niska"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Protek vremena"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Postavke fotoaparata"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Postavke kamere"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Veličina slike"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapiksela"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapiksela"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapiksela"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapiksela"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapiksela"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapiksel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Način fokusa"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatski"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Beskonačno"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Način bljeskalice"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatski"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Uključeno"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Isključeno"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balans bijele boje"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatski"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Svjetlosni izvor sa žarnom niti"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Dnevno svjetlo"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescentno"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Oblačno"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Način scene"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatski"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Radnja"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Noć"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Zalazak sunca"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Zabava"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"U načinu scene ne može se odabirati."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Ekspozicija"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"U redu"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Na vašoj USB memoriji ponestaje prostora. Primijenite postavku kvalitete ili izbrišite neke slike ili druge datoteke."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Na vašoj kartici SD ponestaje prostora. Primijenite postavku kvalitete ili izbrišite neke slike ili druge datoteke."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Dostignuto je ograničenje veličine."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Prebrzo"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Priprema panorame"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panoramu nije moguće spremiti."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Snimanje panorame"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Čekanje na prethodnu panoramu"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Spremanje..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Obrada prikaza panorame"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Dodirnite za fokus."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efekti"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ništa"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Stisnuto"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Velike oči"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Velika usta"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Mala usta"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Veliki nos"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Male oči"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"U svemiru"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Zalazak sunca"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Vaš videozapis"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"U nastavku postavite ​​uređaj."\n"Odmaknite se na trenutak."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Dodirnite za fotografiranje tijekom snimanja."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Pokrenuto je snimanje videozapisa."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Zaustavljeno je snimanje videozapisa."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Fotografiranje је onemogućeno kada su posebni efekti uključeni."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Obriši efekte"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"ŠAŠAVA LICA"</string>
+    <string name="effect_background" msgid="6579360207378171022">"POZADINA"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Gumb Okidač"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Tipka izbornika"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Najnovija fotografija"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Prednji i stražnji prekidač fotoaparata"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Birač fotoaparata, videozapisa ili panorame"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Više kontrola postavke"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Kontrole postavke zatvaranja"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Upravljanje povećavanjem/smanjivanjem"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Smanjenje %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Povećanje %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Potvrdni okvir %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Prebaci na fotografiju"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Prebaci na videozapis"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Prebaci na panoramski način"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Prijeđi na novu panoramu"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Odustani"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Završeno"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Ponovi snimanje u pregledu"</string>
+    <string name="capital_on" msgid="5491353494964003567">"UKLJUČI"</string>
+    <string name="capital_off" msgid="7231052688467970897">"ISKLJUČI"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Isključeno"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekunda"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 sat"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 sata"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 sata"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 sata"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 sati"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 sata"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekunde"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minute"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"sati"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Gotovo"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Postavljanje vrem. intervala"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Značajka protoka vremena isključena je. Uključite ju da biste postavili vremenski interval."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Odbrojavanje je isključeno. Uključite odbrojavanje prije snimanja fotografije."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Postavite trajanje u sekundama"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Odbrojavanje do snimanja fotografije"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Zapamtiti lokacije fotografija?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Dodajte svojim fotografijama i videozapisima oznake lokacija na kojima su snimljeni."\n\n"Ostale aplikacije mogu pristupiti tim podacima s vašim spremljenim slikama."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Ne, hvala"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Da"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Fotoaparat"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Pretraživanje"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotografije"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumi"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d slika"</item>
+    <item quantity="other" msgid="3813306834113858135">"Br. slika: %1$d"</item>
+  </plurals>
+</resources>
diff --git a/res/values-hu/filtershow_strings.xml b/res/values-hu/filtershow_strings.xml
new file mode 100644
index 0000000..769c62b
--- /dev/null
+++ b/res/values-hu/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fényképszerkesztő"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Nem sikerült betölteni a képet!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Háttérkép beállítása"</string>
+    <string name="original" msgid="3524493791230430897">"Eredeti"</string>
+    <string name="borders" msgid="2067345080568684614">"Szegélyek"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Visszavonás"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Ismétlés"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Előzmények"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Előzmények elrejtése"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Képállapot"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Állapot elrejtés"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Beállítások"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Nem mentett módosítások vannak a képen."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Szeretne menteni a kilépés előtt?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Mentés és kilépés"</string>
+    <string name="exit" msgid="242642957038770113">"Kilépés"</string>
+    <string name="history" msgid="455767361472692409">"Előzmények"</string>
+    <string name="reset" msgid="9013181350779592937">"Visszaállítás"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Alkalmazott hatások"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Összehasonlítás"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Alkalmaz"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Visszaállítás"</string>
+    <string name="aspect" msgid="4025244950820813059">"Képarány"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Nincs"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Rögzített"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Kisbolygó"</string>
+    <string name="exposure" msgid="6526397045949374905">"Expozíció"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Élesség"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontraszt"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Dinamika"</string>
+    <string name="saturation" msgid="7026791551032438585">"Telítettség"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"FF szűrő"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Automat. szín"</string>
+    <string name="hue" msgid="6231252147971086030">"Színárnyalat"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Árnyékok"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Kiemelések"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Görbék"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignetta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Vörösszem"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Rajzok"</string>
+    <string name="straighten" msgid="26025591664983528">"Kiegyenesítés"</string>
+    <string name="crop" msgid="5781263790107850771">"Körülvágás"</string>
+    <string name="rotate" msgid="2796802553793795371">"Forgatás"</string>
+    <string name="mirror" msgid="5482518108154883096">"Tükrözés"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatív"</string>
+    <string name="none" msgid="6633966646410296520">"Nincs"</string>
+    <string name="edge" msgid="7036064886242147551">"Szélek"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Kicsinyítés"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Piros"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Zöld"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Kék"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stílus"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Méret"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Szín"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Vonalak"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Filctoll"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Fröcskölés"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Törlés"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Egyéni szín kiválasztása"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Szín kiválasztása"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Méret kiválasztása"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
new file mode 100644
index 0000000..b348679
--- /dev/null
+++ b/res/values-hu/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galéria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Képkeret"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videolejátszó"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Videó betöltése…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Kép betöltése..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Fiók betöltése..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Videó folytatása"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Folytatja a lejátszást innen: %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Lejátszás folytatása"</string>
+    <string name="loading" msgid="7038208555304563571">"Betöltés…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nem sikerült betölteni"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nem sikerült betölteni a képet"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Nincs indexkép"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Újrakezdés"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Érintsen meg egy arcot a kezdéshez."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Kép mentése..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Kép mentése ide: <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Nem lehet menteni a vágott képet."</string>
+    <string name="crop_label" msgid="521114301871349328">"Kép levágása"</string>
+    <string name="trim_label" msgid="274203231381209979">"Videó vágása"</string>
+    <string name="select_image" msgid="7841406150484742140">"Fénykép kiválasztása"</string>
+    <string name="select_video" msgid="4859510992798615076">"Videó kiválasztása"</string>
+    <string name="select_item" msgid="2816923896202086390">"Elem kiválasztása"</string>
+    <string name="select_album" msgid="1557063764849434077">"Album kiválasztása"</string>
+    <string name="select_group" msgid="6744208543323307114">"Csoport kiválasztása"</string>
+    <string name="set_image" msgid="2331476809308010401">"Kép beállítása, mint"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Háttérkép beállítása"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Háttérkép beállítása..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Háttérkép"</string>
+    <string name="delete" msgid="2839695998251824487">"Törlés"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Törli a kiválasztott elemet?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Törli a kiválasztott elemeket?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Megerősítés"</string>
+    <string name="cancel" msgid="3637516880917356226">"Mégse"</string>
+    <string name="share" msgid="3619042788254195341">"Megosztás"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Panorámakép megosztása"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Megosztás fényképként"</string>
+    <string name="deleted" msgid="6795433049119073871">"Törölve"</string>
+    <string name="undo" msgid="2930873956446586313">"VISSZAVONÁS"</string>
+    <string name="select_all" msgid="3403283025220282175">"Az összes kijelölése"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Az összes kijelölés törlése"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diavetítés"</string>
+    <string name="details" msgid="8415120088556445230">"Részletek"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d/%2$d elem:"</string>
+    <string name="close" msgid="5585646033158453043">"Bezárás"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Váltás kamerára"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d kiválasztva"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d kiválasztva"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d kiválasztva"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d kiválasztva"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d kiválasztva"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d kiválasztva"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d kiválasztva"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d kiválasztva"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d kiválasztva"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Megjelenítés a térképen"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Forgatás balra"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Forgatás jobbra"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Nem található az elem."</string>
+    <string name="edit" msgid="1502273844748580847">"Szerkesztés"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Tárolási kérelmek feldolgozása"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Gyorsítótárazás..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Körbevágás"</string>
+    <string name="trim_action" msgid="703098114452883524">"Vágás"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Némítás"</string>
+    <string name="set_as" msgid="3636764710790507868">"Beállítás, mint"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"A videó nem némítható."</string>
+    <string name="video_err" msgid="7003051631792271009">"Nem lehet lejátszani a videót."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Helyszín szerint"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Idő szerint"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Címkék szerint"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Arcok alapján"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Album szerint"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Méret szerint"</string>
+    <string name="untagged" msgid="7281481064509590402">"Címke nélküli"</string>
+    <string name="no_location" msgid="4043624857489331676">"Nincs helyadat"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Néhány helyet nem lehetett azonosítani hálózati problémák miatt."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Nem sikerült letölteni ennek az albumnak a képeit. Próbálja újra később."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Csak képek"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Csak videók"</string>
+    <string name="show_all" msgid="6963292714584735149">"Képek és videók"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotógaléria"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Nincsenek fényképek."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"A vágott képet a(z) <xliff:g id="FOLDER_NAME">%s</xliff:g> mappába mentettük."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nincs elérhető album."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Nincs elérhető kép/videó."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Bejegyzések"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Offline elérhető albumok"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Frissítés"</string>
+    <string name="done" msgid="217672440064436595">"Kész"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%2$d/%1$d elem:"</string>
+    <string name="title" msgid="7622928349908052569">"Beosztás"</string>
+    <string name="description" msgid="3016729318096557520">"Leírás"</string>
+    <string name="time" msgid="1367953006052876956">"Idő"</string>
+    <string name="location" msgid="3432705876921618314">"Hely"</string>
+    <string name="path" msgid="4725740395885105824">"Elérési út"</string>
+    <string name="width" msgid="9215847239714321097">"Szélesség"</string>
+    <string name="height" msgid="3648885449443787772">"Magasság"</string>
+    <string name="orientation" msgid="4958327983165245513">"Tájolás"</string>
+    <string name="duration" msgid="8160058911218541616">"Időtartam"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-típus"</string>
+    <string name="file_size" msgid="8486169301588318915">"Fájlméret"</string>
+    <string name="maker" msgid="7921835498034236197">"Készítő"</string>
+    <string name="model" msgid="8240207064064337366">"Modell"</string>
+    <string name="flash" msgid="2816779031261147723">"Vaku"</string>
+    <string name="aperture" msgid="5920657630303915195">"Rekesz"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Fókusztávolság"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Fehéregyensúly"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Exponálási idő"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Kézi"</string>
+    <string name="auto" msgid="4296941368722892821">"Automata"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Vakuvillanás"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Vaku nélkül"</string>
+    <string name="unknown" msgid="3506693015896912952">"Ismeretlen"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Eredeti"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Szépia"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Azonnali"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Fehérítő"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Kék"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Fekete-fehér"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Kivágás"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X folyamat"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Az album elérhető lesz offline módban is."</item>
+    <item quantity="other" msgid="4948604338155959389">"Albumok letöltése offline hallgatáshoz."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Az elemet a készülék helyileg tárolta, és elérhető offline módban."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Összes album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Helyi albumok"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP eszközök"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa albumok"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> szabad"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> vagy kevesebb"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> vagy több"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> - <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importálás"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importálás befejezve"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Sikertelen importálás"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera csatlakoztatva."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera leválasztva."</string>
+    <string name="click_import" msgid="6407959065464291972">"Importáláshoz érintse meg itt"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Album kiválasztása"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Az összes kép váltása"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Válasszon egy képet"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Képek kiválasztása"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Diavetítés"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumok"</string>
+    <string name="times" msgid="2023033894889499219">"Alkalom"</string>
+    <string name="locations" msgid="6649297994083130305">"Helyek"</string>
+    <string name="people" msgid="4114003823747292747">"Személyek"</string>
+    <string name="tags" msgid="5539648765482935955">"Címkék"</string>
+    <string name="group_by" msgid="4308299657902209357">"Csoportosítás"</string>
+    <string name="settings" msgid="1534847740615665736">"Beállítások"</string>
+    <string name="add_account" msgid="4271217504968243974">"Fiók hozzáadása"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Letöltés"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Szerkesztett online fotók"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importált"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Képernyőkép"</string>
+    <string name="help" msgid="7368960711153618354">"Súgó"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nincs tárhely"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nincs elérhető külső tárhely"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmszalag nézet"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Rácsnézet"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Teljes képernyő"</string>
+    <string name="trimming" msgid="9122385768369143997">"Vágás"</string>
+    <string name="muting" msgid="5094925919589915324">"Némítás..."</string>
+    <string name="please_wait" msgid="7296066089146487366">"Kérjük, várjon."</string>
+    <string name="save_into" msgid="9155488424829609229">"Videó mentése ide: <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Nem lehet megvágni: a célvideó túl rövid."</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panorámakép megjelenítése"</string>
+    <string name="save" msgid="613976532235060516">"Mentés"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Tartalom beolvasása..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elem beolvasva"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d elem beolvasva"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d elem beolvasva"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Rendezés..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"A beolvasás befejeződött."</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importálás..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Nem áll rendelkezésre az eszközre importálható tartalom."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nincs MTP-eszköz csatlakoztatva"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kamerahiba"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Nem lehet csatlakozni a kamerához."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"A biztonsági házirendek miatt a kamera le van tiltva."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Kérjük, várjon..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Kérjük, csatoljon egy USB-tárat a kamera használata előtt."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"A kamera használatához helyezzen be egy SD-kártyát."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Az USB-tár előkészítése..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD-kártya előkészítése..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nem lehet hozzáférni az USB-háttértárhoz"</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nem lehet hozzáférni az SD-kártyához"</string>
+    <string name="review_cancel" msgid="8188009385853399254">"MÉGSE"</string>
+    <string name="review_ok" msgid="1156261588693116433">"KÉSZ"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Gyorsított felvétel"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Válasszon kamerát"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Vissza"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Előre néző"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Hely tárolása"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Visszaszámlálás-időzítő"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 másodperc"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d másodperc"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Hangjelzéssel"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Ki"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Be"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videó minősége"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Magas"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Alacsony"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Gyorsított felvétel"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kamera beállításai"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Videokamera beállításai"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Képméret"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fókuszálás módja"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatikus"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Végtelen"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makró"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Vakumód"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatikus"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Be"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Ki"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Fehéregyensúly"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatikus"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Izzólámpa"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Nappali fény"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fénycső"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Felhős"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Kép jellege"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatikus"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Akció"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Éjszakai"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Napnyugta"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Buli"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Nem választható ebben a módban."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Megvilágítás"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Az USB-táron kezd elfogyni a hely. Módosítsa a minőségi beállításokat, vagy töröljön néhány képet vagy egyéb fájlt."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Az SD-kártyán kezd elfogyni a hely. Módosítsa a minőségi beállításokat, vagy töröljön néhány képet vagy egyéb fájlt."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"A videó elérte a méretkorlátot."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Túl gyors"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Panoráma előkészítése"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Nem lehet menteni a panorámát."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panoráma"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Panoráma rögzítése"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Várakozás az előző panorámára"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Mentés..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panorámakép megjelenítése"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"A fókuszáláshoz érintse meg."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Hatások"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Nincs"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Összenyomás"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Nagy szemek"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Nagy száj"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Kis száj"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Nagy orr"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Kis szemek"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Az űrben"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Napnyugta"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Saját videó"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Tegye le a készüléket."\n"Lépjen ki a képből egy pillanatra."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Érintse meg fotó készítéséhez felvétel közben."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"A videorögzítés elindult."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"A videorögzítés leállt."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"A video-pillanatfelvétel letiltva speciális effektek esetén."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Effektek törlése"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"BOLONDOS ARCOK"</string>
+    <string name="effect_background" msgid="6579360207378171022">"HÁTTÉR"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Exponáló gomb"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menü gomb"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Legutóbbi fotó"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Első és hátsó kamera kapcsolója"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kamera, videó vagy panoráma kiválasztása"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"További beállításvezérlők"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Beállításvezérlők bezárása"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Nagyítás/kicsinyítés vezérlése"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Csökkentés: %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Növelés: %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s jelölőnégyzet"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Fényképező mód"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Videó mód"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Panoráma mód"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Váltás az új Panoráma módra"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Törlés"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Kész"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Ellenőrzés – új felvétel"</string>
+    <string name="capital_on" msgid="5491353494964003567">"BE"</string>
+    <string name="capital_off" msgid="7231052688467970897">"KI"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Ki"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 másodperc"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 perc"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 óra"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 óra"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"másodperc"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"perc"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"óra"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Kész"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Időintervallum beállítása"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"A Gyorsított felvétel funkció ki van kapcsolva. Kapcsolja be az időintervallum beállításához."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"A visszaszámlálás-időzítő ki van kapcsolva. A fénykép készítése előtti visszaszámláláshoz kapcsolja be."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Időtartam megadása másodpercben"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Visszaszámlálás a fényképezésig"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Szeretné eltárolni a felvételek helyszínét?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Címkézze fel a fotókat és videókat a hellyel, ahol készítette őket."\n\n"Más alkalmazások hozzáférnek ehhez az információhoz és a mentett képekhez."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nem, köszönöm."</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Igen"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Keresés"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotók"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumok"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotó"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotó"</item>
+  </plurals>
+</resources>
diff --git a/res/values-in/filtershow_strings.xml b/res/values-in/filtershow_strings.xml
new file mode 100644
index 0000000..fe03370
--- /dev/null
+++ b/res/values-in/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor Foto"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Tidak dapat memuat gambar!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Menyetel wallpaper"</string>
+    <string name="original" msgid="3524493791230430897">"Asli"</string>
+    <string name="borders" msgid="2067345080568684614">"Batas"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Batalkan"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Ulangi"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Tampilkan Riwayat"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Sembunyikan Riwayat"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Tampilkan Status"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Sembunyikan Status"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Setelan"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Ada perubahan yang belum tersimpan pada gambar ini."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Ingin menyimpan sebelum keluar?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Simpan dan Keluar"</string>
+    <string name="exit" msgid="242642957038770113">"Keluar"</string>
+    <string name="history" msgid="455767361472692409">"Riwayat"</string>
+    <string name="reset" msgid="9013181350779592937">"Setel Ulang"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Efek yang Diterapkan"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Bandingkan"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Terapkan"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Setel Ulang"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspek"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Tidak ada"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Tetap"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Planet Mini"</string>
+    <string name="exposure" msgid="6526397045949374905">"Pencahayaan"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ketajaman"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontras"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Cemerlang"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturasi"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filter HP"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Warna Otomatis"</string>
+    <string name="hue" msgid="6231252147971086030">"Rona"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Bayangan"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Sorotan"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kurva"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinyet"</string>
+    <string name="redeye" msgid="4508883127049472069">"Mata Merah"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Gambar"</string>
+    <string name="straighten" msgid="26025591664983528">"Luruskan"</string>
+    <string name="crop" msgid="5781263790107850771">"Pangkas"</string>
+    <string name="rotate" msgid="2796802553793795371">"Putar"</string>
+    <string name="mirror" msgid="5482518108154883096">"Cermin"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatif"</string>
+    <string name="none" msgid="6633966646410296520">"Tidak ada"</string>
+    <string name="edge" msgid="7036064886242147551">"Tepi"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Downsample"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Merah"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Hijau"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Biru"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Gaya"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Ukuran"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Warna"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Garis"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Penanda"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Percikan"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Hapus"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Pilih warna khusus"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Pilih Warna"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Pilih Ukuran"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"Oke"</string>
+</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
new file mode 100644
index 0000000..e282fbe
--- /dev/null
+++ b/res/values-in/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeri"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Bingkai gambar"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Pemutar video"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Memuat video..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Memuat gambar…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Memuat akun…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Lanjutkan video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Lanjutkan pemutaran dari %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Lanjutkan pemutaran"</string>
+    <string name="loading" msgid="7038208555304563571">"Memuat…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Tidak dapat memuat"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Tidak dapat memuat gambar"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Tidak ada gambar mini"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Memulai"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Oke"</string>
+    <string name="ok" msgid="5296833083983263293">"Oke"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Sentuh wajah untuk memulai."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Menyimpan gambar…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Menyimpan gambar ke <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Tak dapat menyimpan gambar yg dipangkas."</string>
+    <string name="crop_label" msgid="521114301871349328">"Pangkas gambar"</string>
+    <string name="trim_label" msgid="274203231381209979">"Potong video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Pilih foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Pilih video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Pilih item"</string>
+    <string name="select_album" msgid="1557063764849434077">"Pilih album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Pilih grup"</string>
+    <string name="set_image" msgid="2331476809308010401">"Setel gambar sebagai"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Setel wallpaper"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Menyetel wallpaper..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string>
+    <string name="delete" msgid="2839695998251824487">"Hapus"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Hapus item yang dipilih?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Hapus item yang dipilih?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Konfirmasi"</string>
+    <string name="cancel" msgid="3637516880917356226">"Batal"</string>
+    <string name="share" msgid="3619042788254195341">"Bagikan"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Bagikan panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Bagikan sebagai foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Dihapus"</string>
+    <string name="undo" msgid="2930873956446586313">"URUNGKAN"</string>
+    <string name="select_all" msgid="3403283025220282175">"Pilih semua"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Batalkan semua pilihan"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Rangkai slide"</string>
+    <string name="details" msgid="8415120088556445230">"Detail"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d dari %2$d item:"</string>
+    <string name="close" msgid="5585646033158453043">"Tutup"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Beralih ke Kamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d dipilih"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d dipilih"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d dipilih"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d dipilih"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d dipilih"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d dipilih"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d dipilih"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d dipilih"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d dipilih"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Tampilkan pada peta"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Putar ke kiri"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Putar ke kanan"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Tidak dapat menemukan item."</string>
+    <string name="edit" msgid="1502273844748580847">"Edit"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Memproses permintaan menyimpan ke cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Smpn ke tmbolok"</string>
+    <string name="crop_action" msgid="3427470284074377001">"Pangkas"</string>
+    <string name="trim_action" msgid="703098114452883524">"Pangkas"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Bisukan"</string>
+    <string name="set_as" msgid="3636764710790507868">"Setel sebagai"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Tidak dapat membisukan video."</string>
+    <string name="video_err" msgid="7003051631792271009">"Tidak dapat memutar video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Menurut lokasi"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Menurut waktu"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Menurut tag"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Menurut orang"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Menurut album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Menurut ukuran"</string>
+    <string name="untagged" msgid="7281481064509590402">"Tidak di-tag"</string>
+    <string name="no_location" msgid="4043624857489331676">"Tidak ada lokasi"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Beberapa lokasi tidak dapat diidentifikasi karena masalah jaringan."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Tidak dapat mengunduh foto di album ini. Coba lagi nanti."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Hanya gambar"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Hanya video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Gambar &amp; video"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galeri Foto"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Tidak ada foto."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Gambar yang dipotong disimpan ke <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Tidak ada album yang tersedia."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O gambar/video tersedia."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Pos"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Jadikan agar tersedia offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Segarkan"</string>
+    <string name="done" msgid="217672440064436595">"Selesai"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d dari %2$d item:"</string>
+    <string name="title" msgid="7622928349908052569">"Judul"</string>
+    <string name="description" msgid="3016729318096557520">"Deskripsi"</string>
+    <string name="time" msgid="1367953006052876956">"Waktu"</string>
+    <string name="location" msgid="3432705876921618314">"Lokasi"</string>
+    <string name="path" msgid="4725740395885105824">"Jalur"</string>
+    <string name="width" msgid="9215847239714321097">"Lebar"</string>
+    <string name="height" msgid="3648885449443787772">"Tinggi"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientasi"</string>
+    <string name="duration" msgid="8160058911218541616">"Durasi"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Jenis MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Ukuran file"</string>
+    <string name="maker" msgid="7921835498034236197">"Pembuat"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Lampu Kilat"</string>
+    <string name="aperture" msgid="5920657630303915195">"Bukaan"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Jarak Fokus"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Kseimbangn pth"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Lama pncahyaan"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Otomatis"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Lampu kilat aktif"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Tanpa lampu kilat"</string>
+    <string name="unknown" msgid="3506693015896912952">"Tidak diketahui"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Asli"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Lawas"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instan"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Biru"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"H/P"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Proses X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Menjadikan album tersedia secara offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Menjadikan album tersedia secara offline."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Item ini tersimpan secara lokal dan tersedia secara offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Semua album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Album lokal"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Perangkat MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Album Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> bebas"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> atau kurang"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> atau lebih"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> hingga <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Impor"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Impor selesai"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Impor tidak berhasil"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera tersambung."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera terputus."</string>
+    <string name="click_import" msgid="6407959065464291972">"Sentuh di sini untuk mengimpor"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Pilih album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Kocok semua gambar"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Pilih gambar"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Pilih gambar"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Rangkai slide"</string>
+    <string name="albums" msgid="7320787705180057947">"Album"</string>
+    <string name="times" msgid="2023033894889499219">"Waktu"</string>
+    <string name="locations" msgid="6649297994083130305">"Lokasi"</string>
+    <string name="people" msgid="4114003823747292747">"Orang"</string>
+    <string name="tags" msgid="5539648765482935955">"Tag"</string>
+    <string name="group_by" msgid="4308299657902209357">"Kelompokkan menurut"</string>
+    <string name="settings" msgid="1534847740615665736">"Setelan"</string>
+    <string name="add_account" msgid="4271217504968243974">"Tambah akun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Unduhan"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Foto Online yang Diedit"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Diimpor"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Tangkapan Layar"</string>
+    <string name="help" msgid="7368960711153618354">"Bantuan"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Tak Ada Penyimpanan"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Tidak ada penyimpanan eksternal yang tersedia"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Tampilan strip film"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Tampilan kisi"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Tampilan layar penuh"</string>
+    <string name="trimming" msgid="9122385768369143997">"Pemangkasan"</string>
+    <string name="muting" msgid="5094925919589915324">"Membisukan"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Harap tunggu"</string>
+    <string name="save_into" msgid="9155488424829609229">"Menyimpan video ke <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Tidak dapat memangkas : video target terlalu pendek"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Merender panorama"</string>
+    <string name="save" msgid="613976532235060516">"Simpan"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Memindai konten..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d item dipindai"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d item dipindai"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d item dipindai"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Menyortir…"</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Pemindaian selesai"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Mengimpor..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Tidak ada konten yang dapat diimpor pada perangkat ini."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Tidak ada perangkat MTP yang tersambung"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kesalahan kamera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Tidak dapat terhubung ke kamera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kamera telah dinonaktifkan karena kebijakan keamanan."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Perekam video"</string>
+    <string name="wait" msgid="8600187532323801552">"Harap tunggu…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Pasang penyimpanan USB sebelum menggunakan kamera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Masukkan kartu SD sebelum menggunakan kamera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Menyiapkan penyimpanan USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Menyiapkan kartu SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Tidak dapat mengakses penyimpanan USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Tidak dapat mengakses kartu SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"BATAL"</string>
+    <string name="review_ok" msgid="1156261588693116433">"SELESAI"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Perekaman time lapse"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Pilih kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Belakang"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Depan"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Lokasi penyimpanan"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Penghitung mundur"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 detik"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d detik"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Bip penghitungan"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Nonaktif"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Aktif"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Kualitas video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Tinggi"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Rendah"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Selang waktu"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Setelan kamera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Setelan perekam video"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Ukuran gambar"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M piksel"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 M piksel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 M piksel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 M piksel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 M piksel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 M piksel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Mode fokus"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Otomatis"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Ketakterbatasan"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Mode lampu kilat"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Otomatis"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Pada"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Mati"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Keseimbangan putih"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Otomatis"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Berpijar"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Siang Hari"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluoresens"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Berawan"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Mode adegan"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Otomatis"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Aksi"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Malam"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Matahari terbenam"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Pesta"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Tidak dapat dipilih dalam mode adegan."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Pencahayaan"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"Oke"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Penyimpanan USB Anda kehabisan ruang kosong. Ubah setelan kualitas atau hapus beberapa gambar atau file lain."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Kartu SD Anda kehabisan ruang kosong. Ubah setelan kualitas atau hapus beberapa gambar atau file lain."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Batas ukuran tercapai."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Terlalu cpt"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Menyiapkan panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Tidak dapat menyimpan panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Mengambil gambar panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Menunggu panorama sebelumnya"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Menyimpan…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Merender panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Sentuh untuk memfokuskan."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efek"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Tidak Ada"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Peras"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Mata besar"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Mulut besar"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Mulut kecil"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Hidung besar"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Mata kecil"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Luar angkasa"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Mthari trbenam"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Video Anda"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Letakkan perangkat Anda."\n"Menyingkirlah sejenak dari bidang bidik."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Sentuh untuk mengambil foto saat sedang merekam."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Perekaman video telah dimulai."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Perekaman video telah berhenti."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Cuplikan video dinonaktifkan bila efek khusus aktif."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Hapus efek"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"WAJAH KONYOL"</string>
+    <string name="effect_background" msgid="6579360207378171022">"LATAR BELAKANG"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Tombol rana"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Tombol menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Foto terbaru"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Beralih kamera depan dan belakang"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Pemilih kamera, video, atau panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Kontrol setelan lainnya"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Tutup kontrol setelan"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Kontrol perbesar/perkecil"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Kurangi %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Tingkatkan %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s kotak centang"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Alihkan ke foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Alihkan ke video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Alihkan ke panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Beralih ke panorama baru"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Tinjauan dibatalkan"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Tinjauan selesai"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Tinjau pengambilan ulang"</string>
+    <string name="capital_on" msgid="5491353494964003567">"NYALA"</string>
+    <string name="capital_off" msgid="7231052688467970897">"MATI"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Nonaktif"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 detik"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 menit"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 jam"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"detik"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"menit"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"jam"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Selesai"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Setel Interval Waktu"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Fitur selang waktu tidak aktif. Aktifkan untuk menyetel interval waktu."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Penghitung mundur tidak aktif. Aktifkan untuk menghitung mundur sebelum mengambil foto."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Setel durasi dalam detik"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Menghitung mundur untuk mengambil foto"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Ingat lokasi foto?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Beri tag foto dan video Anda dengan lokasi tempat pengambilannya."\n\n"Aplikasi lain dapat mengakses informasi ini beserta gambar Anda yang tersimpan."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Tidak, terima kasih"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ya"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Telusuri"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Foto"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Album"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d foto"</item>
+  </plurals>
+</resources>
diff --git a/res/values-it/filtershow_strings.xml b/res/values-it/filtershow_strings.xml
new file mode 100644
index 0000000..39edea7
--- /dev/null
+++ b/res/values-it/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor di foto"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Impossibile caricare l\'immagine."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Impostazione dello sfondo"</string>
+    <string name="original" msgid="3524493791230430897">"Originale"</string>
+    <string name="borders" msgid="2067345080568684614">"Bordi"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Annulla"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Ripeti"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Mostra cronologia"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Nascondi cronologia"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Mostra stato immagine"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Nascondi stato img"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Impostazioni"</string>
+    <string name="unsaved" msgid="8704442449002374375">"L\'immagine ha modifiche non salvate."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Vuoi salvare prima di uscire?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Salva ed esci"</string>
+    <string name="exit" msgid="242642957038770113">"Esci"</string>
+    <string name="history" msgid="455767361472692409">"Cronologia"</string>
+    <string name="reset" msgid="9013181350779592937">"Reimposta"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Effetti applicati"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Confronta"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Applica"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Reimposta"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspetto"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Nessuno"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fisse"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Pianetino"</string>
+    <string name="exposure" msgid="6526397045949374905">"Esposizione"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Nitidezza"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contrasto"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vividezza"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturazione"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtro BN"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Colore autom."</string>
+    <string name="hue" msgid="6231252147971086030">"Tonalità"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Ombre"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Alte luci"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curve"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignetta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Occhi rossi"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Disegna"</string>
+    <string name="straighten" msgid="26025591664983528">"Raddrizza"</string>
+    <string name="crop" msgid="5781263790107850771">"Ritaglia"</string>
+    <string name="rotate" msgid="2796802553793795371">"Ruota"</string>
+    <string name="mirror" msgid="5482518108154883096">"Specchio"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativo"</string>
+    <string name="none" msgid="6633966646410296520">"Nessuno"</string>
+    <string name="edge" msgid="7036064886242147551">"Bordi"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Sottocampionare"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rosso"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Verde"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blu"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stile"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Dimensioni"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Colore"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linee"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Evidenziatore"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Spruzzo"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Cancella"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Scegli colore personalizzato"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Seleziona colore"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Seleziona dimensioni"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
new file mode 100644
index 0000000..a3cbf32
--- /dev/null
+++ b/res/values-it/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Cornice immagine"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Video player"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Caricamento video..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Caricamento immagine..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Caricamento account..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Riprendi video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Riprendi riproduzione da %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Riprendi riproduzione"</string>
+    <string name="loading" msgid="7038208555304563571">"Caricamento..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Impossibile caricare"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Impossibile caricare l\'immagine"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Nessuna miniatura"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Ricomincia"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Tocca un viso per iniziare."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Salvataggio foto..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Salvataggio immagine in <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Impossibile salvare immagine ritagliata."</string>
+    <string name="crop_label" msgid="521114301871349328">"Ritaglia foto"</string>
+    <string name="trim_label" msgid="274203231381209979">"Ritaglia video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Seleziona foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Seleziona video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Seleziona elemento"</string>
+    <string name="select_album" msgid="1557063764849434077">"Seleziona album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Seleziona gruppo"</string>
+    <string name="set_image" msgid="2331476809308010401">"Imposta foto come"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Imposta sfondo"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Impostazione sfondo..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Sfondo"</string>
+    <string name="delete" msgid="2839695998251824487">"Elimina"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Eliminare elemento selezionato?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Eliminare elementi selezionati?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Conferma"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annulla"</string>
+    <string name="share" msgid="3619042788254195341">"Condividi"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Condividi panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Condividi come foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Eliminata"</string>
+    <string name="undo" msgid="2930873956446586313">"ANNULLA"</string>
+    <string name="select_all" msgid="3403283025220282175">"Seleziona tutti"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Deseleziona tutto"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Presentazione"</string>
+    <string name="details" msgid="8415120088556445230">"Dettagli"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d elementi su %2$d:"</string>
+    <string name="close" msgid="5585646033158453043">"Chiudi"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Passa a Fotocamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d selezionati"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d selezionato"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d selezionati"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d selezionati"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d selezionato"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d selezionati"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d selezionati"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d selezionato"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d selezionati"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostra sulla mappa"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Ruota a sinistra"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Ruota a destra"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Impossibile trovare l\'elemento."</string>
+    <string name="edit" msgid="1502273844748580847">"Modifica"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Elaborazione richieste memorizzazione cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Memorizzazione..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Ritaglia"</string>
+    <string name="trim_action" msgid="703098114452883524">"Ritaglia"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Disattiva audio"</string>
+    <string name="set_as" msgid="3636764710790507868">"Imposta come"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Imposs. disattiv. audio video."</string>
+    <string name="video_err" msgid="7003051631792271009">"Impossibile riprodurre il video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Per luogo"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Per data"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Per tag"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Per persone"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Per album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Per dimensioni"</string>
+    <string name="untagged" msgid="7281481064509590402">"Senza tag"</string>
+    <string name="no_location" msgid="4043624857489331676">"Nessun luogo"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Impossibile identificare alcune località a causa di problemi di rete."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Download delle foto in questo album non riuscito. Riprova più tardi."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Solo immagini"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Solo video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Immagini e video"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galleria fotografica"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Nessuna foto."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Immagine ritagliata salvata in <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nessun album disponibile."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Nessun video/immagine disponibile."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Post"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Rendi disponibili offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Aggiorna"</string>
+    <string name="done" msgid="217672440064436595">"Fine"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d su %2$d elementi :"</string>
+    <string name="title" msgid="7622928349908052569">"Titolo"</string>
+    <string name="description" msgid="3016729318096557520">"Descrizione"</string>
+    <string name="time" msgid="1367953006052876956">"Ora"</string>
+    <string name="location" msgid="3432705876921618314">"Luogo"</string>
+    <string name="path" msgid="4725740395885105824">"Percorso"</string>
+    <string name="width" msgid="9215847239714321097">"Larghezza"</string>
+    <string name="height" msgid="3648885449443787772">"Altezza"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientamento"</string>
+    <string name="duration" msgid="8160058911218541616">"Durata"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Tipo MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Dimensioni file"</string>
+    <string name="maker" msgid="7921835498034236197">"Autore"</string>
+    <string name="model" msgid="8240207064064337366">"Modello"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Diaframma"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Lungh. focale"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Bilanc. bianco"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Tempo esposiz."</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manuale"</string>
+    <string name="auto" msgid="4296941368722892821">"Autom."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash scattato"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Senza flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Sconosciuta"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Originale"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blu"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Bianco e nero"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Attivazione album offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Attivazione album offline."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Questo elemento è memorizzato localmente e disponibile offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Tutti gli album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Album locali"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Dispositivi MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Album di Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> liberi"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o minore"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o maggiore"</string>
+    <string name="size_between" msgid="8779660840898917208">"Da <xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importa"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importazione completata"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Importazione non riuscita"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Fotocamera collegata."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Fotocamera scollegata."</string>
+    <string name="click_import" msgid="6407959065464291972">"Tocca qui per importare"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Scegli un album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Visualizzazione casuale immagini"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Scegli un\'immagine"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Scegli immagini"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Presentazione"</string>
+    <string name="albums" msgid="7320787705180057947">"Album"</string>
+    <string name="times" msgid="2023033894889499219">"Volte"</string>
+    <string name="locations" msgid="6649297994083130305">"Luoghi"</string>
+    <string name="people" msgid="4114003823747292747">"Persone"</string>
+    <string name="tags" msgid="5539648765482935955">"Tag"</string>
+    <string name="group_by" msgid="4308299657902209357">"Raggruppa per"</string>
+    <string name="settings" msgid="1534847740615665736">"Impostazioni"</string>
+    <string name="add_account" msgid="4271217504968243974">"Aggiungi account"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotocamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Download"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Foto online modificate"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importate"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshot"</string>
+    <string name="help" msgid="7368960711153618354">"Guida"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nessun archivio"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nessun archivio esterno disponibile"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Visualizzazione sequenza"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Visualizzazione griglia"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Schermo intero"</string>
+    <string name="trimming" msgid="9122385768369143997">"Taglio in corso"</string>
+    <string name="muting" msgid="5094925919589915324">"Disattivazione audio"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Attendi"</string>
+    <string name="save_into" msgid="9155488424829609229">"Salvataggio del video in <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Impossibile tagliare: video di destinazione troppo breve"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Creazione panorama in corso"</string>
+    <string name="save" msgid="613976532235060516">"Salva"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Scansione dei contenuti..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elementi sottoposti a scansione"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d elemento sottoposto a scansione"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d elementi sottoposti a scansione"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Ordinamento..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Scansione eseguita"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importazione..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Sul dispositivo non sono presenti contenuti da importare."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nessun dispositivo MTP collegato"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Errore fotocamera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Impossibile collegarsi alla fotocamera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"La fotocamera è stata disattivata in base a norme di sicurezza."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Fotocamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videocamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Attendere..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Monta l\'archivio USB prima di utilizzare la fotocamera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Per usare la fotocamera devi inserire una scheda SD."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Preparazione archivio USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Preparazione scheda SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Accesso ad archivio USB non riuscito."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Accesso a scheda SD non riuscito."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ANNULLA"</string>
+    <string name="review_ok" msgid="1156261588693116433">"FINE"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Registrazione al rallentatore"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Scegli fotocamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Posteriore"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Frontale"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Registra località"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Timer conto alla rovescia"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 secondo"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d secondi"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Bip conto alla rov."</string>
+    <string name="setting_off" msgid="4480039384202951946">"Disattivata"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Attiva"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Qualità video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Alta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Bassa"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Rallentatore"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Impostazioni fotocamera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Impostazioni videocamera"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Dimensioni foto"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 MP"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 MP"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 MP"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 MP"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 MP"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 MP"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Messa a fuoco"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatica"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinito"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Modalità flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatica"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Attiva"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Non attiva"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Bilanciamento bianco"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatico"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Luce incandescenza"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Luce diurna"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Luce neon"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nuvoloso"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Modalità scena"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatica"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Movimento"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Notturna"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Tramonto"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Festa"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Non selezionabile in modalità scena."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Esposizione"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Lo spazio dell\'archivio USB si sta esaurendo. Cambia l\'impostazione di qualità o elimina alcune immagini o altri file."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Lo spazio della scheda SD si sta esaurendo. Cambia l\'impostazione di qualità o elimina alcune immagini o altri file."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Limite di dimensione raggiunto."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Troppo veloce"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Preparazione panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Salvataggio panorama non riuscito."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panoramica"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Acquisizione panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"In attesa di panorama precedente"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Salvataggio..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Creazione panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Tocca per mettere a fuoco."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effetti"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Nessuno"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Schiaccia"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Occhi grandi"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Bocca grande"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Bocca piccola"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Naso grande"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Occhi piccoli"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Nello spazio"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Tramonto"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Il tuo video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Posa il tuo dispositivo."\n"Esci dalla visualizzazione per un attimo."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Tocca per scattare foto durante la registrazione."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"La registrazione video è stata avviata."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"La registrazione video è stata interrotta."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Istantanea video disabilitata se gli effetti speciali sono attivi."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Cancella effetti"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"FACCINE"</string>
+    <string name="effect_background" msgid="6579360207378171022">"SFONDO"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Pulsante di scatto"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Pulsante Menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Foto più recente"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Interruttore fotocamera anteriore e posteriore"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Selettore fotocamera, video o panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Altri controlli"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Chiudi controlli"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Controllo zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Riduci %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Aumenta %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Casella di controllo %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Passa a foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Passa a video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Passa a panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Passa al nuovo panorama"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Annulla verifica"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Verifica terminata"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Scatta/Riprendi di nuovo per recensione"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ON"</string>
+    <string name="capital_off" msgid="7231052688467970897">"OFF"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Non attivo"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 secondo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 secondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 ora"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ore"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"secondi"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minuti"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ore"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Fine"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Imposta l\'intervallo di tempo"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"La funzione Rallentatore non è attiva. Attivala per impostare l\'intervallo di tempo."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Timer del conto alla rovescia non attivo. Attivalo per il conto alla rovescia prima di scattare una foto."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Imposta la durata in secondi"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Conto alla rovescia per scattare una foto"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Memorizzare i luoghi delle foto?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Aggiungi alle foto e ai video tag relativi alle località in cui sono stati ripresi."\n\n"Altre applicazioni possono accedere a queste informazioni e alle tue immagini salvate."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"No, grazie"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Sì"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Fotocamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Cerca"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Foto"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Album"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d foto"</item>
+  </plurals>
+</resources>
diff --git a/res/values-iw/filtershow_strings.xml b/res/values-iw/filtershow_strings.xml
new file mode 100644
index 0000000..79075a5
--- /dev/null
+++ b/res/values-iw/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"עורך תמונות"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"לא ניתן להעלות את התמונה!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"מגדיר טפט"</string>
+    <string name="original" msgid="3524493791230430897">"מקור"</string>
+    <string name="borders" msgid="2067345080568684614">"גבולות"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"בטל"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"בצע מחדש"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"הצג היסטוריה"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"הסתר היסטוריה"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"הצג מצב תמונה"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"הסתר מצב תמונה"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"הגדרות"</string>
+    <string name="unsaved" msgid="8704442449002374375">"יש בתמונה הזו שינויים שלא נשמרו."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"האם אתה רוצה לשמור לפני היציאה?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"שמור וצא"</string>
+    <string name="exit" msgid="242642957038770113">"צא"</string>
+    <string name="history" msgid="455767361472692409">"היסטוריה"</string>
+    <string name="reset" msgid="9013181350779592937">"אפס"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"אפקטים שהוחלו"</string>
+    <string name="compare_original" msgid="8140838959007796977">"השווה"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"החל"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"אפס"</string>
+    <string name="aspect" msgid="4025244950820813059">"יחס גובה-רוחב"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"ללא"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"מקובע"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"פלנטה קטנה"</string>
+    <string name="exposure" msgid="6526397045949374905">"חשיפה"</string>
+    <string name="sharpness" msgid="6463103068318055412">"חדות"</string>
+    <string name="contrast" msgid="2310908487756769019">"ניגודיות"</string>
+    <string name="vibrance" msgid="3326744578577835915">"חיוניות"</string>
+    <string name="saturation" msgid="7026791551032438585">"רוויה"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"מסנן ש/ל"</string>
+    <string name="wbalance" msgid="6346581563387083613">"צבע אוטומטי"</string>
+    <string name="hue" msgid="6231252147971086030">"גוון"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"צלליות"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"הדגשות"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"קימורים"</string>
+    <string name="vignette" msgid="934721068851885390">"עמעום קצוות"</string>
+    <string name="redeye" msgid="4508883127049472069">"עיניים אדומות"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"צייר"</string>
+    <string name="straighten" msgid="26025591664983528">"יישר"</string>
+    <string name="crop" msgid="5781263790107850771">"חתוך"</string>
+    <string name="rotate" msgid="2796802553793795371">"סובב"</string>
+    <string name="mirror" msgid="5482518108154883096">"מראה"</string>
+    <string name="negative" msgid="6998313764388022201">"נגטיב"</string>
+    <string name="none" msgid="6633966646410296520">"ללא"</string>
+    <string name="edge" msgid="7036064886242147551">"קצוות"</string>
+    <string name="kmeans" msgid="1630263230946107457">"וורהול"</string>
+    <string name="downsample" msgid="3552938534146980104">"הקטן תמונה"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"אדום"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"ירוק"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"כחול"</string>
+    <string name="draw_style" msgid="2036125061987325389">"סגנון"</string>
+    <string name="draw_size" msgid="4360005386104151209">"גודל"</string>
+    <string name="draw_color" msgid="2119030386987211193">"צבע"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"קווים"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"עט סימון"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"התזה"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"נקה"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"בחר צבע מותאם אישית"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"בחר צבע"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"בחר גודל"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"אישור"</string>
+</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
new file mode 100644
index 0000000..2a73e42
--- /dev/null
+++ b/res/values-iw/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"גלריה"</string>
+    <string name="gadget_title" msgid="259405922673466798">"מסגרת תמונה"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"נגן סרטוני וידאו"</string>
+    <string name="loading_video" msgid="4013492720121891585">"טוען  וידאו…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"טוען תמונה…"</string>
+    <string name="loading_account" msgid="928195413034552034">"טוען חשבון..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"המשך את הקרנת הווידאו"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"להמשיך להפעיל מ-%s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"המשך את ההפעלה"</string>
+    <string name="loading" msgid="7038208555304563571">"טוען..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"לא ניתן להעלות"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"לא היתה אפשרות לטעון את התמונה"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"ללא תמונה ממוזערת"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"התחל מחדש"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"אישור"</string>
+    <string name="ok" msgid="5296833083983263293">"אישור"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"גע בפנים כלשהם כדי להתחיל."</string>
+    <string name="saving_image" msgid="7270334453636349407">"שומר תמונה..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"שומר את התמונה ב-<xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"לא ניתן לשמור את התמונה החתוכה."</string>
+    <string name="crop_label" msgid="521114301871349328">"חתוך תמונה"</string>
+    <string name="trim_label" msgid="274203231381209979">"חתוך סרטון"</string>
+    <string name="select_image" msgid="7841406150484742140">"בחר תמונה"</string>
+    <string name="select_video" msgid="4859510992798615076">"בחר סרטון"</string>
+    <string name="select_item" msgid="2816923896202086390">"בחר פריט"</string>
+    <string name="select_album" msgid="1557063764849434077">"בחר אלבום"</string>
+    <string name="select_group" msgid="6744208543323307114">"בחר קבוצה"</string>
+    <string name="set_image" msgid="2331476809308010401">"הגדר תמונה בתור"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"הגדר טפט"</string>
+    <string name="wallpaper" msgid="140165383777262070">"מגדיר טפט..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"טפט"</string>
+    <string name="delete" msgid="2839695998251824487">"מחק"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"האם למחוק את הפריט שנבחר?"</item>
+    <item quantity="other" msgid="5874316486520635333">"האם למחוק את הפריטים שנבחרו?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"אשר"</string>
+    <string name="cancel" msgid="3637516880917356226">"ביטול"</string>
+    <string name="share" msgid="3619042788254195341">"שיתוף"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"שתף פנורמה"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"שתף כתמונה"</string>
+    <string name="deleted" msgid="6795433049119073871">"נמחק"</string>
+    <string name="undo" msgid="2930873956446586313">"בטל"</string>
+    <string name="select_all" msgid="3403283025220282175">"בחר הכול"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"בטל בחירה של הכל"</string>
+    <string name="slideshow" msgid="4355906903247112975">"מצגת"</string>
+    <string name="details" msgid="8415120088556445230">"פרטים"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d מתוך %2$d פריטים:"</string>
+    <string name="close" msgid="5585646033158453043">"סגור"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"עבור למצלמה"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d נבחרו"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d נבחר"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d נבחרו"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d נבחרו"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d נבחר"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d נבחרו"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d נבחרו"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d נבחרו"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d נבחרו"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"הצג במפה"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"סובב שמאלה"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"סובב ימינה"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"הפריט לא נמצא."</string>
+    <string name="edit" msgid="1502273844748580847">"ערוך"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"מעבד בקשות העברה למטמון"</string>
+    <string name="caching_label" msgid="4521059045896269095">"מעביר למטמון..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"חתוך"</string>
+    <string name="trim_action" msgid="703098114452883524">"חתוך"</string>
+    <string name="mute_action" msgid="5296241754753306251">"השתק"</string>
+    <string name="set_as" msgid="3636764710790507868">"הגדר בתור"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"לא ניתן להשתיק את הסרטון."</string>
+    <string name="video_err" msgid="7003051631792271009">"לא ניתן להפעיל את סרטון הווידאו."</string>
+    <string name="group_by_location" msgid="316641628989023253">"לפי מיקום"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"לפי שעה"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"לפי תגים"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"לפי אנשים"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"לפי אלבום"</string>
+    <string name="group_by_size" msgid="153766174950394155">"לפי גודל"</string>
+    <string name="untagged" msgid="7281481064509590402">"ללא תג"</string>
+    <string name="no_location" msgid="4043624857489331676">"ללא מיקום"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"לא ניתן לזהות מיקומים מסוימים בשל בעיות ברשת."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"לא ניתן להוריד את התמונות באלבום זה. נסה שוב מאוחר יותר."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"תמונות בלבד"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"סרטונים בלבד"</string>
+    <string name="show_all" msgid="6963292714584735149">"תמונות וסרטונים"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"גלריית תמונות"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"אין תמונות."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"התמונה שנחתכה נשמרה ב-<xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"אין אלבומים זמינים."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O תמונות/סרטוני וידאו זמינים."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"פוסטים"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"הפוך לזמין במצב לא מקוון"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"רענן"</string>
+    <string name="done" msgid="217672440064436595">"סיום"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d מתוך %2$d פריטים:"</string>
+    <string name="title" msgid="7622928349908052569">"כותרת"</string>
+    <string name="description" msgid="3016729318096557520">"תיאור"</string>
+    <string name="time" msgid="1367953006052876956">"שעה"</string>
+    <string name="location" msgid="3432705876921618314">"מיקום"</string>
+    <string name="path" msgid="4725740395885105824">"נתיב"</string>
+    <string name="width" msgid="9215847239714321097">"רוחב"</string>
+    <string name="height" msgid="3648885449443787772">"גובה"</string>
+    <string name="orientation" msgid="4958327983165245513">"כיוון"</string>
+    <string name="duration" msgid="8160058911218541616">"משך"</string>
+    <string name="mimetype" msgid="8024168704337990470">"סוג MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"גודל הקובץ"</string>
+    <string name="maker" msgid="7921835498034236197">"יוצר"</string>
+    <string name="model" msgid="8240207064064337366">"דגם"</string>
+    <string name="flash" msgid="2816779031261147723">"פלאש"</string>
+    <string name="aperture" msgid="5920657630303915195">"צמצם"</string>
+    <string name="focal_length" msgid="1291383769749877010">"רוחק מוקד"</string>
+    <string name="white_balance" msgid="1582509289994216078">"איזון לבן"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"זמן חשיפה"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"מ\"מ"</string>
+    <string name="manual" msgid="6608905477477607865">"ידנית"</string>
+    <string name="auto" msgid="4296941368722892821">"אוטומטי"</string>
+    <string name="flash_on" msgid="7891556231891837284">"צילום עם פלאש"</string>
+    <string name="flash_off" msgid="1445443413822680010">"ללא פלאש"</string>
+    <string name="unknown" msgid="3506693015896912952">"לא ידוע"</string>
+    <string name="ffx_original" msgid="372686331501281474">"מקור"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"וינטאג\'"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"מיידי"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"הלבנה"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"כחול"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"ש/ל"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"פונץ\'"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"תהליך X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"לאטה"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"ליתו"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"הופך את האלבום לזמין במצב לא מקוון."</item>
+    <item quantity="other" msgid="4948604338155959389">"הופך את האלבומים לזמינים באופן לא מקוון."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"פריט זה מאוחסן באופן מקומי וזמין במצב לא מקוון."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"כל האלבומים"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"אלבומים מקומיים"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"מכשירי MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"אלבומי Google"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> של שטח פנוי"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ומטה"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ומעלה"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> עד <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"ייבא"</string>
+    <string name="import_complete" msgid="3875040287486199999">"היבוא הושלם"</string>
+    <string name="import_fail" msgid="8497942380703298808">"הייבוא נכשל"</string>
+    <string name="camera_connected" msgid="916021826223448591">"יש מצלמה מחוברת."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"המצלמה מנותקת."</string>
+    <string name="click_import" msgid="6407959065464291972">"גע כאן כדי לייבא"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"בחר אלבום"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"ערבב את כל התמונות"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"בחר תמונה"</string>
+    <string name="widget_type" msgid="1364653978966343448">"בחר תמונות"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"מצגת"</string>
+    <string name="albums" msgid="7320787705180057947">"אלבומים"</string>
+    <string name="times" msgid="2023033894889499219">"פעמים"</string>
+    <string name="locations" msgid="6649297994083130305">"מיקומים"</string>
+    <string name="people" msgid="4114003823747292747">"אנשים"</string>
+    <string name="tags" msgid="5539648765482935955">"תגים"</string>
+    <string name="group_by" msgid="4308299657902209357">"קבץ לפי"</string>
+    <string name="settings" msgid="1534847740615665736">"הגדרות"</string>
+    <string name="add_account" msgid="4271217504968243974">"הוסף חשבון"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"מצלמה"</string>
+    <string name="folder_download" msgid="7186215137642323932">"הורד"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"תמונות מקוונות ערוכות"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"מיובאות"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"צילום מסך"</string>
+    <string name="help" msgid="7368960711153618354">"עזרה"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"אין אחסון"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"אחסון חיצוני לא זמין"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"תצוגה בסרט שקופיות"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"תצוגת רשת"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"תצוגה במסך מלא"</string>
+    <string name="trimming" msgid="9122385768369143997">"קיצור"</string>
+    <string name="muting" msgid="5094925919589915324">"משתיק"</string>
+    <string name="please_wait" msgid="7296066089146487366">"המתן"</string>
+    <string name="save_into" msgid="9155488424829609229">"שומר את הסרטון ב-<xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"לא ניתן לקצר: סרטון היעד קצר מדי"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"יוצר פנורמה"</string>
+    <string name="save" msgid="613976532235060516">"שמור"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"סורק תוכן..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d פריטים נסרקו"</item>
+    <item quantity="one" msgid="4340019444460561648">"פריט %1$d נסרק"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d פריטים נסרקו"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"ממיין..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"הסריקה הסתיימה"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"מייבא..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"אין תוכן זמין לייבוא במכשיר זה."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"לא מחובר אף מכשיר MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"שגיאת מצלמה"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"לא ניתן להתחבר למצלמה."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"המצלמה הושבתה בשל מדיניות אבטחה."</string>
+    <string name="camera_label" msgid="6346560772074764302">"מצלמה"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"מצלמת וידאו"</string>
+    <string name="wait" msgid="8600187532323801552">"המתן..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"טען אחסון USB לפני השימוש במצלמה."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"הכנס כרטיס SD לפני השימוש במצלמה."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"מכין אחסון USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"מכין כרטיס SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"לא ניתן לגשת לאחסון ה-USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"לא ניתן לגשת לכרטיס ה-SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ביטול"</string>
+    <string name="review_ok" msgid="1156261588693116433">"בוצע"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"הקלטה של מעבר זמן"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"בחר מצלמה"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"הקודם"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"חזיתית"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"מיקום אחסון"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"טיימר לספירה לאחור"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"שנייה אחת"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d שניות"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"צפצף בעת ספירה לאחור"</string>
+    <string name="setting_off" msgid="4480039384202951946">"כבוי"</string>
+    <string name="setting_on" msgid="8602246224465348901">"מופעל"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"איכות סרטון"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"גבוהה"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"נמוכה"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"הילוך מהיר"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"הגדרות מצלמה"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"הגדרות מצלמת וידאו"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"גודל תמונה"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 מגה פיקסל"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 מגה פיקסלים"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 מגה פיקסלים"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 מגה פיקסלים"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3 מגה פיקסלים"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"מגה פיקסל אחד"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"מצב מיקוד"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"אוטומטי"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"אינסוף"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"מאקרו"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"מצב Flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"אוטומטי"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"מופעל"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"כבוי"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"איזון לבן"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"אוטומטי"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"זוהר"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"אור יום"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"פלואורסנט"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"מעונן"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"מצב נוף"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"אוטומטי"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"פעולה"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"לילה"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"שקיעה"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"מסיבה"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"לא ניתן לבחירה במצב נוף."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"חשיפה"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"אישור"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"אוזל המקום באמצעי האחסון מסוג USB. שנה את הגדרת האיכות או מחק כמה תמונות או קבצים אחרים."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"השטח בכרטיס SD אוזל. שנה את הגדרת האיכות או מחק חלק מהתמונות או קבצים אחרים."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"הגעת למגבלת הגודל."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"מהר מדי"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"מכין פנורמה"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"לא ניתן לשמור את הפנורמה."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"פנורמה"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"מבצע צילום פנורמה"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"ממתין לפנורמה הקודמת"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"שומר…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"יוצר פנורמה"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"גע כדי להתמקד."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"אפקטים"</string>
+    <string name="effect_none" msgid="3601545724573307541">"ללא"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"כיווץ"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"עיניים גדולות"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"פה גדול"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"פה קטן"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"אף גדול"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"עיניים קטנות"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"בחלל"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"שקיעה"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"הסרטון שלך"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"הנח את המכשיר."\n"צא לרגע מהתמונה."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"גע כדי לצלם תמונה במהלך ההקלטה."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"הקלטת וידאו החלה."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"הקלטת וידאו הופסקה."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"האפשרות לצילום תמונה מסרטון וידאו מושבתת כאשר אפקטים מיוחדים מופעלים."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"נקה אפקטים"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"פרצופים מצחיקים"</string>
+    <string name="effect_background" msgid="6579360207378171022">"רקע"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"לחצן הצמצם."</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"לחצן תפריט"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"התמונה האחרונה"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"מתג המצלמה הקדמית והאחורית"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"בורר מצב מצלמה, וידאו או פנורמה"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"פקדי הגדרות נוספות"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"סגור פקדי הגדרה"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"פקד \'הגדל/הקטן\'"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"הקטן %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"הגדל %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"תיבת סימון %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"עבור לצילום תמונות"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"עבור לווידאו"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"עבור לפנורמה"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"עבור לפנורמה חדשה"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"ביטול בדיקה"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"בדיקה בוצעה"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"הצג צילום חוזר"</string>
+    <string name="capital_on" msgid="5491353494964003567">"פועל"</string>
+    <string name="capital_off" msgid="7231052688467970897">"כבוי"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"כבוי"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 שנייה"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"שנייה אחת"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 שניות"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 דקה"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"דקה אחת"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 דקות"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 שעה"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"שעה אחת"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"שעתיים"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 שעות"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 שעות"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"שניות"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"דקות"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"שעות"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"בוצע"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"הגדר מרווח זמן"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"תכונת ההילוך המהיר כבויה. הפעל אותה כדי להגדיר מרווח זמן."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"טיימר הספירה לאחור כבוי. הפעל אותו כדי לספור לאחור לפני צילום תמונה."</string>
+    <string name="set_duration" msgid="5578035312407161304">"הגדר משך זמן בשניות"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"סופר לאחור עד לצילום תמונה"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"האם לזכור מיקומי תמונות?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"מתייג את התמונות והסרטונים שלך לציון המקומות שבהם צולמו."\n\n"יישומים אחרים יכולים לגשת למידע זה, כולל תמונות שנשמרו."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"לא, תודה"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"כן"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"מצלמה"</string>
+    <string name="menu_search" msgid="7580008232297437190">"חיפוש"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"תמונות"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"אלבומים"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"תמונה %1$d"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d תמונות"</item>
+  </plurals>
+</resources>
diff --git a/res/values-ja/filtershow_strings.xml b/res/values-ja/filtershow_strings.xml
new file mode 100644
index 0000000..447ca7a
--- /dev/null
+++ b/res/values-ja/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"フォトエディタ"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"画像を読み込めません"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"壁紙を設定しています"</string>
+    <string name="original" msgid="3524493791230430897">"元の画像"</string>
+    <string name="borders" msgid="2067345080568684614">"境界"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"元に戻す"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"やり直し"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"履歴を表示する"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"履歴を表示しない"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"画像ステータスを表示"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"画像ステータスを非表示"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"設定"</string>
+    <string name="unsaved" msgid="8704442449002374375">"この画像には保存されていない変更があります。"</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"終了する前に保存しますか?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"保存して終了"</string>
+    <string name="exit" msgid="242642957038770113">"終了"</string>
+    <string name="history" msgid="455767361472692409">"履歴"</string>
+    <string name="reset" msgid="9013181350779592937">"リセット"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"適用済みの効果"</string>
+    <string name="compare_original" msgid="8140838959007796977">"比較"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"適用"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"リセット"</string>
+    <string name="aspect" msgid="4025244950820813059">"アスペクト比"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"なし"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"固定"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"小さな惑星"</string>
+    <string name="exposure" msgid="6526397045949374905">"露出"</string>
+    <string name="sharpness" msgid="6463103068318055412">"シャープネス"</string>
+    <string name="contrast" msgid="2310908487756769019">"コントラスト"</string>
+    <string name="vibrance" msgid="3326744578577835915">"自然な彩度"</string>
+    <string name="saturation" msgid="7026791551032438585">"彩度"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"モノクロ"</string>
+    <string name="wbalance" msgid="6346581563387083613">"自動色補正"</string>
+    <string name="hue" msgid="6231252147971086030">"色彩"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"影"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"ハイライト"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"カーブ"</string>
+    <string name="vignette" msgid="934721068851885390">"周辺減光"</string>
+    <string name="redeye" msgid="4508883127049472069">"赤目処理"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"描画"</string>
+    <string name="straighten" msgid="26025591664983528">"傾き調整"</string>
+    <string name="crop" msgid="5781263790107850771">"トリミング"</string>
+    <string name="rotate" msgid="2796802553793795371">"回転"</string>
+    <string name="mirror" msgid="5482518108154883096">"鏡"</string>
+    <string name="negative" msgid="6998313764388022201">"白黒反転"</string>
+    <string name="none" msgid="6633966646410296520">"なし"</string>
+    <string name="edge" msgid="7036064886242147551">"エッジ"</string>
+    <string name="kmeans" msgid="1630263230946107457">"ウォーホル"</string>
+    <string name="downsample" msgid="3552938534146980104">"縮小"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"赤"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"緑"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"青"</string>
+    <string name="draw_style" msgid="2036125061987325389">"スタイル"</string>
+    <string name="draw_size" msgid="4360005386104151209">"サイズ"</string>
+    <string name="draw_color" msgid="2119030386987211193">"色"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"線"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"マーカー"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"スパッタリング"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"消去"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"ユーザー定義の色を選択"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"色の選択"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"サイズの選択"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
new file mode 100644
index 0000000..c86d932
--- /dev/null
+++ b/res/values-ja/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"ギャラリー"</string>
+    <string name="gadget_title" msgid="259405922673466798">"写真フレーム"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"動画プレーヤー"</string>
+    <string name="loading_video" msgid="4013492720121891585">"動画を読み込み中..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"画像を読み込み中..."</string>
+    <string name="loading_account" msgid="928195413034552034">"アカウントを読み込み中..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"動画の再開"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"再生を%sから再開しますか?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"再生を再開"</string>
+    <string name="loading" msgid="7038208555304563571">"読み込み中..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"読み込めませんでした"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"画像を読み込めませんでした"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"サムネイルなし"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"最初から再生"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"始めるには顔をタップします。"</string>
+    <string name="saving_image" msgid="7270334453636349407">"写真を保存中…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"画像を<xliff:g id="ALBUM_NAME">%1$s</xliff:g>に保存中…"</string>
+    <string name="save_error" msgid="6857408774183654970">"トリミングした画像を保存できません。"</string>
+    <string name="crop_label" msgid="521114301871349328">"トリミング"</string>
+    <string name="trim_label" msgid="274203231381209979">"動画をトリミング"</string>
+    <string name="select_image" msgid="7841406150484742140">"写真を選択"</string>
+    <string name="select_video" msgid="4859510992798615076">"動画を選択"</string>
+    <string name="select_item" msgid="2816923896202086390">"項目を選択"</string>
+    <string name="select_album" msgid="1557063764849434077">"アルバムを選択"</string>
+    <string name="select_group" msgid="6744208543323307114">"グループの選択"</string>
+    <string name="set_image" msgid="2331476809308010401">"登録"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"壁紙を設定"</string>
+    <string name="wallpaper" msgid="140165383777262070">"壁紙を設定しています..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁紙"</string>
+    <string name="delete" msgid="2839695998251824487">"削除"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"アイテムを削除しますか?"</item>
+    <item quantity="other" msgid="5874316486520635333">"アイテムを削除しますか?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"確認"</string>
+    <string name="cancel" msgid="3637516880917356226">"キャンセル"</string>
+    <string name="share" msgid="3619042788254195341">"共有"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"パノラマ写真を共有"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"写真として共有"</string>
+    <string name="deleted" msgid="6795433049119073871">"削除済み"</string>
+    <string name="undo" msgid="2930873956446586313">"元に戻す"</string>
+    <string name="select_all" msgid="3403283025220282175">"すべて選択"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"選択をすべて解除"</string>
+    <string name="slideshow" msgid="4355906903247112975">"スライドショー"</string>
+    <string name="details" msgid="8415120088556445230">"詳細情報"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d/%2$d件のアイテム:"</string>
+    <string name="close" msgid="5585646033158453043">"閉じる"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"カメラに切り替え"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d件選択済み"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d件選択済み"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d件選択済み"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d件選択済み"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d件選択済み"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d件選択済み"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d件選択済み"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d件選択済み"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d件選択済み"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"地図に表示"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"左に回転"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"右に回転"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"アイテムが見つかりませんでした。"</string>
+    <string name="edit" msgid="1502273844748580847">"編集"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"キャッシュリクエストを処理しています"</string>
+    <string name="caching_label" msgid="4521059045896269095">"キャッシュ中..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"トリミング"</string>
+    <string name="trim_action" msgid="703098114452883524">"トリミング"</string>
+    <string name="mute_action" msgid="5296241754753306251">"ミュート"</string>
+    <string name="set_as" msgid="3636764710790507868">"登録"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"動画をミュートできません。"</string>
+    <string name="video_err" msgid="7003051631792271009">"動画を再生できません。"</string>
+    <string name="group_by_location" msgid="316641628989023253">"地域別"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"時間別"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"タグ別"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"人物別"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"アルバム別"</string>
+    <string name="group_by_size" msgid="153766174950394155">"サイズ別"</string>
+    <string name="untagged" msgid="7281481064509590402">"タグなし"</string>
+    <string name="no_location" msgid="4043624857489331676">"位置情報なし"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"ネットワークの問題により一部の位置情報を特定できませんでした。"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"このアルバムの画像をダウンロードできませんでした。しばらくしてからもう一度お試しください。"</string>
+    <string name="show_images_only" msgid="7263218480867672653">"画像のみ"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"動画のみ"</string>
+    <string name="show_all" msgid="6963292714584735149">"画像と動画"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"フォトギャラリー"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"画像がありません。"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"トリミングした画像を<xliff:g id="FOLDER_NAME">%s</xliff:g>に保存しました。"</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"使用できるアルバムはありません。"</string>
+    <string name="empty_album" msgid="4542880442593595494">"使用できる画像/動画: 0件"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"投稿"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"オフラインで使用する"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"更新"</string>
+    <string name="done" msgid="217672440064436595">"完了"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d件:"</string>
+    <string name="title" msgid="7622928349908052569">"タイトル"</string>
+    <string name="description" msgid="3016729318096557520">"説明"</string>
+    <string name="time" msgid="1367953006052876956">"時刻"</string>
+    <string name="location" msgid="3432705876921618314">"場所"</string>
+    <string name="path" msgid="4725740395885105824">"パス"</string>
+    <string name="width" msgid="9215847239714321097">"幅"</string>
+    <string name="height" msgid="3648885449443787772">"高さ"</string>
+    <string name="orientation" msgid="4958327983165245513">"画面の向き"</string>
+    <string name="duration" msgid="8160058911218541616">"長さ"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIMEタイプ"</string>
+    <string name="file_size" msgid="8486169301588318915">"ファイルサイズ"</string>
+    <string name="maker" msgid="7921835498034236197">"メーカー"</string>
+    <string name="model" msgid="8240207064064337366">"モデル"</string>
+    <string name="flash" msgid="2816779031261147723">"フラッシュ"</string>
+    <string name="aperture" msgid="5920657630303915195">"絞り"</string>
+    <string name="focal_length" msgid="1291383769749877010">"レンズ焦点距離"</string>
+    <string name="white_balance" msgid="1582509289994216078">"ホワイトバランス"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"露出時間"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"マニュアル"</string>
+    <string name="auto" msgid="4296941368722892821">"オート"</string>
+    <string name="flash_on" msgid="7891556231891837284">"フラッシュON"</string>
+    <string name="flash_off" msgid="1445443413822680010">"フラッシュOFF"</string>
+    <string name="unknown" msgid="3506693015896912952">"不明"</string>
+    <string name="ffx_original" msgid="372686331501281474">"オリジナル"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"ヴィンテージ"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"インスタント"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"ブリーチ"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"青"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"モノクロ"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"パンチ"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Xプロセス"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"ラテ"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"リトグラフ"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"アルバムをオフラインで利用できるようにしています。"</item>
+    <item quantity="other" msgid="4948604338155959389">"アルバムをオフラインで利用できるようにしています。"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"このアイテムは端末に保存され、オフラインで利用できます。"</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"すべてのアルバム"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"ローカルアルバム"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTPデバイス"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasaのアルバム"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g>空き"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g>以下"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g>以上"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g>~<xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"インポート"</string>
+    <string name="import_complete" msgid="3875040287486199999">"インポート完了"</string>
+    <string name="import_fail" msgid="8497942380703298808">"インポートできません"</string>
+    <string name="camera_connected" msgid="916021826223448591">"カメラが接続されました。"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"カメラの接続が解除されました。"</string>
+    <string name="click_import" msgid="6407959065464291972">"インポートするにはここをタップします"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"アルバムを選択"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"すべての画像をシャッフル"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"画像を選択"</string>
+    <string name="widget_type" msgid="1364653978966343448">"画像の選択"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"スライドショー"</string>
+    <string name="albums" msgid="7320787705180057947">"アルバム"</string>
+    <string name="times" msgid="2023033894889499219">"時間"</string>
+    <string name="locations" msgid="6649297994083130305">"ロケーション"</string>
+    <string name="people" msgid="4114003823747292747">"人物"</string>
+    <string name="tags" msgid="5539648765482935955">"タグ"</string>
+    <string name="group_by" msgid="4308299657902209357">"グループ化"</string>
+    <string name="settings" msgid="1534847740615665736">"設定"</string>
+    <string name="add_account" msgid="4271217504968243974">"アカウントを追加"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"カメラ"</string>
+    <string name="folder_download" msgid="7186215137642323932">"ダウンロード"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"編集済みのオンライン画像"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"インポート済み"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"スクリーンショット"</string>
+    <string name="help" msgid="7368960711153618354">"ヘルプ"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"ストレージがありません"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"利用できる外部ストレージがありません"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"フィルムストリップ表示"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"グリッド表示"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"全画面表示"</string>
+    <string name="trimming" msgid="9122385768369143997">"トリミング"</string>
+    <string name="muting" msgid="5094925919589915324">"ミュートしています"</string>
+    <string name="please_wait" msgid="7296066089146487366">"お待ちください"</string>
+    <string name="save_into" msgid="9155488424829609229">"動画を<xliff:g id="ALBUM_NAME">%1$s</xliff:g>に保存しています…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"トリミングできません: 動画が短すぎます"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"パノラマをレンダリング中"</string>
+    <string name="save" msgid="613976532235060516">"保存"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"コンテンツをスキャンしています..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d件のアイテムをスキャン済み"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d件のアイテムをスキャン済み"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d件のアイテムをスキャン済み"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"並べ替えています..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"スキャンが終了しました"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"インポートしています..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"この端末にインポートできるコンテンツがありません。"</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"接続されているMTPデバイスがありません"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"カメラエラー"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"カメラに接続できません。"</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"カメラはセキュリティポリシーにより無効になっています。"</string>
+    <string name="camera_label" msgid="6346560772074764302">"カメラ"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"ビデオ録画"</string>
+    <string name="wait" msgid="8600187532323801552">"お待ちください..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"カメラを使用する前にUSBストレージをマウントしてください。"</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"カメラを使用する前にSDカードを挿入してください。"</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USBストレージの準備中..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SDカードの準備中..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"USBストレージにアクセスできませんでした。"</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"SDカードにアクセスできませんでした。"</string>
+    <string name="review_cancel" msgid="8188009385853399254">"キャンセル"</string>
+    <string name="review_ok" msgid="1156261588693116433">"完了"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"低速度撮影"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"カメラを選択"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"背面"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"前面"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"位置情報を記録する"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"カウントダウンタイマー"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1秒"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d秒"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"カウントダウン時にビープ音"</string>
+    <string name="setting_off" msgid="4480039384202951946">"OFF"</string>
+    <string name="setting_on" msgid="8602246224465348901">"ON"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"動画の画質"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"高"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"低"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"低速度撮影"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"カメラ設定"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"ビデオ録画設定"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"画像サイズ"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8Mピクセル"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5メガピクセル"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3メガピクセル"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2メガピクセル"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3メガピクセル"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1メガピクセル"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"フォーカスモード"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"オート"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"無限遠"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"マクロ"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"フラッシュモード"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"オート"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"ON"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"OFF"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"ホワイトバランス"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"オート"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"白熱灯"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"昼光"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"蛍光灯"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"曇り"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"撮影モード"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"オート"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"スポーツ"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"夜景"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"夕焼け"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"パーティー"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"撮影モードでは選択できません。"</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"露出"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USBストレージの空き領域が少なくなっています。画質設定を変更するか、画像など一部のファイルを削除してください。"</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"SDカードの空き領域が少なくなっています。画質設定を変更するか、画像などのファイルを一部削除してください。"</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"サイズ制限に達しました。"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"速すぎます"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"パノラマを準備しています"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"パノラマを保存できませんでした。"</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"パノラマ"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"パノラマを撮影中"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"前のパノラマを待機中"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"保存中..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"パノラマをレンダリング中"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"タップしてフォーカスします。"</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"効果"</string>
+    <string name="effect_none" msgid="3601545724573307541">"なし"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"スクイーズ"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"大きな目"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"大きな口"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"小さな口"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"大きな鼻"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"小さな目"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"宇宙空間"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"夕焼け"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"あなたの動画"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"端末をセッティングします。"\n"少しの間フレームの外に出ます。"</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"録画中にタップして静止画を撮影できます。"</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"録画を開始しました。"</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"録画を停止しました。"</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"動画スナップショットは特殊効果がONのときは無効です。"</string>
+    <string name="clear_effects" msgid="5485339175014139481">"効果設定をクリア"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"変な顔"</string>
+    <string name="effect_background" msgid="6579360207378171022">"背景"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"シャッターボタン"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"メニューボタン"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"最近の写真"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"前後カメラの切り替え"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"カメラ、ビデオ、パノラマの切り替え"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"その他の設定コントロール"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"設定コントロールを閉じる"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"ズームコントロール"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"-%1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"+%1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$sチェックボックス"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"カメラに切り替え"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"動画に切り替え"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"パノラマに切り替え"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"新しいパノラマに切り替える"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"レビュー - キャンセル"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"レビュー - 完了"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"撮り直しを確認"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ON"</string>
+    <string name="capital_off" msgid="7231052688467970897">"OFF"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"OFF"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5分"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1分"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5分"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2分"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5分"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3分"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4分"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5分"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6分"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10分"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12分"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15分"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24分"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15時間"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24時間"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"秒"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"分"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"時間"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"完了"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"間隔を設定"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"低速度撮影機能がOFFになっています。間隔を設定するにはONにしてください。"</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"カウントダウンタイマーがオフです。画像を撮影するまでカウントダウンするには、このタイマーをオンにしてください。"</string>
+    <string name="set_duration" msgid="5578035312407161304">"設定時間(秒数)"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"画像を撮影するまでカウントダウンします"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"撮影場所を記録しますか?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"画像や動画に撮影場所のタグを付けることができます。"\n\n"他のアプリから、保存された画像とともに撮影場所の情報にもアクセスできるようになります。"</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"いいえ"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"はい"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"カメラ"</string>
+    <string name="menu_search" msgid="7580008232297437190">"検索"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"写真"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"アルバム"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d枚の画像"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d枚の画像"</item>
+  </plurals>
+</resources>
diff --git a/res/values-ko/filtershow_strings.xml b/res/values-ko/filtershow_strings.xml
new file mode 100644
index 0000000..6e9f29b
--- /dev/null
+++ b/res/values-ko/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"사진 편집기"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"이미지를 로드할 수 없습니다."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"배경화면 설정 중"</string>
+    <string name="original" msgid="3524493791230430897">"원본"</string>
+    <string name="borders" msgid="2067345080568684614">"테두리"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"실행취소"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"다시실행"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"기록 표시"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"기록 숨기기"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"이미지 상태 표시"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"이미지 상태 숨기기"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"설정"</string>
+    <string name="unsaved" msgid="8704442449002374375">"이 이미지에 저장하지 않은 변경사항이 있습니다."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"저장하고 나서 종료하시겠습니까?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"저장 및 종료"</string>
+    <string name="exit" msgid="242642957038770113">"종료"</string>
+    <string name="history" msgid="455767361472692409">"기록"</string>
+    <string name="reset" msgid="9013181350779592937">"초기화"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"적용된 효과"</string>
+    <string name="compare_original" msgid="8140838959007796977">"비교하기"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"적용"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"초기화"</string>
+    <string name="aspect" msgid="4025244950820813059">"가로 세로 비율"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"선택 안 함"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"고정"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"작은 행성"</string>
+    <string name="exposure" msgid="6526397045949374905">"노출"</string>
+    <string name="sharpness" msgid="6463103068318055412">"선명도"</string>
+    <string name="contrast" msgid="2310908487756769019">"대비"</string>
+    <string name="vibrance" msgid="3326744578577835915">"생동감"</string>
+    <string name="saturation" msgid="7026791551032438585">"채도"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"흑백 필터"</string>
+    <string name="wbalance" msgid="6346581563387083613">"자동 색상"</string>
+    <string name="hue" msgid="6231252147971086030">"색조"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"그림자"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"하이라이트"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"곡선"</string>
+    <string name="vignette" msgid="934721068851885390">"비네트"</string>
+    <string name="redeye" msgid="4508883127049472069">"적목현상 없애기"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"그리기"</string>
+    <string name="straighten" msgid="26025591664983528">"수평조절"</string>
+    <string name="crop" msgid="5781263790107850771">"자르기"</string>
+    <string name="rotate" msgid="2796802553793795371">"회전"</string>
+    <string name="mirror" msgid="5482518108154883096">"거울"</string>
+    <string name="negative" msgid="6998313764388022201">"네거티브"</string>
+    <string name="none" msgid="6633966646410296520">"선택 안 함"</string>
+    <string name="edge" msgid="7036064886242147551">"가장자리"</string>
+    <string name="kmeans" msgid="1630263230946107457">"워홀"</string>
+    <string name="downsample" msgid="3552938534146980104">"다운샘플"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"빨간색"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"녹색"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"파란색"</string>
+    <string name="draw_style" msgid="2036125061987325389">"스타일"</string>
+    <string name="draw_size" msgid="4360005386104151209">"크기"</string>
+    <string name="draw_color" msgid="2119030386987211193">"색상"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"선"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"마커"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"튀기기"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"삭제"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"맞춤 색상 선택"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"색상 선택"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"크기 선택"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"확인"</string>
+</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
new file mode 100644
index 0000000..a6d4a5d
--- /dev/null
+++ b/res/values-ko/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"갤러리"</string>
+    <string name="gadget_title" msgid="259405922673466798">"사진 액자"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"동영상 플레이어"</string>
+    <string name="loading_video" msgid="4013492720121891585">"동영상 로드 중..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"이미지 로드 중…"</string>
+    <string name="loading_account" msgid="928195413034552034">"계정 로드 중..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"동영상 다시 시작"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"%s부터 이어서 보시겠습니까?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"이어서 보기"</string>
+    <string name="loading" msgid="7038208555304563571">"로드 중..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"로드하지 못했습니다."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"이미지를 로드할 수 없습니다."</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"미리보기 이미지 없음"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"처음부터 보기"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"확인"</string>
+    <string name="ok" msgid="5296833083983263293">"확인"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"시작하려면 얼굴을 터치하세요."</string>
+    <string name="saving_image" msgid="7270334453636349407">"사진 저장 중..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"사진을 <xliff:g id="ALBUM_NAME">%1$s</xliff:g>에 저장 중…"</string>
+    <string name="save_error" msgid="6857408774183654970">"잘린 이미지를 저장하지 못했습니다."</string>
+    <string name="crop_label" msgid="521114301871349328">"사진 자르기"</string>
+    <string name="trim_label" msgid="274203231381209979">"동영상 다듬기"</string>
+    <string name="select_image" msgid="7841406150484742140">"사진 선택"</string>
+    <string name="select_video" msgid="4859510992798615076">"동영상 선택"</string>
+    <string name="select_item" msgid="2816923896202086390">"항목 선택"</string>
+    <string name="select_album" msgid="1557063764849434077">"앨범 선택"</string>
+    <string name="select_group" msgid="6744208543323307114">"그룹 선택"</string>
+    <string name="set_image" msgid="2331476809308010401">"사진을 다음으로 설정"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"배경화면 설정"</string>
+    <string name="wallpaper" msgid="140165383777262070">"배경화면을 설정하는 중..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"배경화면"</string>
+    <string name="delete" msgid="2839695998251824487">"삭제"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"선택한 항목을 삭제할까요?"</item>
+    <item quantity="other" msgid="5874316486520635333">"선택한 항목을 삭제할까요?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"확인"</string>
+    <string name="cancel" msgid="3637516880917356226">"취소"</string>
+    <string name="share" msgid="3619042788254195341">"공유"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"파노라마 공유"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"사진으로 공유"</string>
+    <string name="deleted" msgid="6795433049119073871">"삭제됨"</string>
+    <string name="undo" msgid="2930873956446586313">"실행취소"</string>
+    <string name="select_all" msgid="3403283025220282175">"모두 선택"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"모두 선택취소"</string>
+    <string name="slideshow" msgid="4355906903247112975">"슬라이드쇼"</string>
+    <string name="details" msgid="8415120088556445230">"세부정보"</string>
+    <string name="details_title" msgid="2611396603977441273">"%2$d개 중 %1$d번째 항목:"</string>
+    <string name="close" msgid="5585646033158453043">"닫기"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"카메라로 전환"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d개 선택됨"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d개 선택됨"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d개 선택됨"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d개 선택됨"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d개 선택됨"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d개 선택됨"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d개 선택됨"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d개 선택됨"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d개 선택됨"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"지도에 표시"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"왼쪽으로 회전"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"오른쪽으로 회전"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"항목을 찾을 수 없습니다."</string>
+    <string name="edit" msgid="1502273844748580847">"수정"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"캐시 요청을 처리하는 중"</string>
+    <string name="caching_label" msgid="4521059045896269095">"캐시하는 중..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"자르기"</string>
+    <string name="trim_action" msgid="703098114452883524">"다듬기"</string>
+    <string name="mute_action" msgid="5296241754753306251">"음소거"</string>
+    <string name="set_as" msgid="3636764710790507868">"다음으로 설정"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"동영상을 음소거할 수 없습니다."</string>
+    <string name="video_err" msgid="7003051631792271009">"동영상을 재생할 수 없습니다."</string>
+    <string name="group_by_location" msgid="316641628989023253">"위치별"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"시간별"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"태그별"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"인물 기준"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"앨범별"</string>
+    <string name="group_by_size" msgid="153766174950394155">"크기별"</string>
+    <string name="untagged" msgid="7281481064509590402">"태그 지정 안함"</string>
+    <string name="no_location" msgid="4043624857489331676">"위치 없음"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"네트워크 문제로 인해 일부 위치를 식별하지 못했습니다."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"앨범 사진을 다운로드하지 못했습니다. 나중에 다시 시도해 주세요."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"이미지"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"동영상"</string>
+    <string name="show_all" msgid="6963292714584735149">"이미지 및 동영상"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"사진 갤러리"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"사진이 없습니다."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"잘린 이미지가 <xliff:g id="FOLDER_NAME">%s</xliff:g>에 저장되었습니다."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"사용할 수 있는 앨범이 없습니다."</string>
+    <string name="empty_album" msgid="4542880442593595494">"사용할 수 있는 이미지/동영상이 없습니다."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"소식"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"오프라인 사용 설정"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"새로고침"</string>
+    <string name="done" msgid="217672440064436595">"완료"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%2$d개 중 %1$d번째 항목:"</string>
+    <string name="title" msgid="7622928349908052569">"제목"</string>
+    <string name="description" msgid="3016729318096557520">"설명"</string>
+    <string name="time" msgid="1367953006052876956">"시간"</string>
+    <string name="location" msgid="3432705876921618314">"위치"</string>
+    <string name="path" msgid="4725740395885105824">"경로"</string>
+    <string name="width" msgid="9215847239714321097">"너비"</string>
+    <string name="height" msgid="3648885449443787772">"높이"</string>
+    <string name="orientation" msgid="4958327983165245513">"방향"</string>
+    <string name="duration" msgid="8160058911218541616">"길이"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME 유형"</string>
+    <string name="file_size" msgid="8486169301588318915">"파일 크기"</string>
+    <string name="maker" msgid="7921835498034236197">"제조업체"</string>
+    <string name="model" msgid="8240207064064337366">"모델"</string>
+    <string name="flash" msgid="2816779031261147723">"플래시"</string>
+    <string name="aperture" msgid="5920657630303915195">"조리개"</string>
+    <string name="focal_length" msgid="1291383769749877010">"초점 거리"</string>
+    <string name="white_balance" msgid="1582509289994216078">"화이트 밸런스"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"노출 시간"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"수동"</string>
+    <string name="auto" msgid="4296941368722892821">"자동"</string>
+    <string name="flash_on" msgid="7891556231891837284">"플래시 터짐"</string>
+    <string name="flash_off" msgid="1445443413822680010">"플래시 없음"</string>
+    <string name="unknown" msgid="3506693015896912952">"알 수 없음"</string>
+    <string name="ffx_original" msgid="372686331501281474">"원본"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"빈티지"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"인스턴트"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"블리치"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"파란색"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"흑백"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"펀치"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X 처리"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"라테"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"리소"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"오프라인에서 앨범을 사용하도록 설정합니다."</item>
+    <item quantity="other" msgid="4948604338155959389">"오프라인에서 앨범을 사용하도록 설정합니다."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"이 항목은 로컬에 저장되어 있으며 오프라인에서 사용할 수 있습니다."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"모든 앨범"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"로컬 앨범"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP 기기"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa 앨범"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> 사용 가능"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> 이하"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> 이상"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> - <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"가져오기"</string>
+    <string name="import_complete" msgid="3875040287486199999">"가져오기 완료"</string>
+    <string name="import_fail" msgid="8497942380703298808">"가져오지 못했습니다."</string>
+    <string name="camera_connected" msgid="916021826223448591">"카메라가 연결되었습니다."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"카메라 연결이 끊어졌습니다."</string>
+    <string name="click_import" msgid="6407959065464291972">"가져오려면 여기를 터치하세요."</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"앨범 선택"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"모든 이미지 셔플"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"이미지 선택"</string>
+    <string name="widget_type" msgid="1364653978966343448">"이미지 선택"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"슬라이드쇼"</string>
+    <string name="albums" msgid="7320787705180057947">"앨범"</string>
+    <string name="times" msgid="2023033894889499219">"시간"</string>
+    <string name="locations" msgid="6649297994083130305">"위치"</string>
+    <string name="people" msgid="4114003823747292747">"사용자"</string>
+    <string name="tags" msgid="5539648765482935955">"태그"</string>
+    <string name="group_by" msgid="4308299657902209357">"그룹화 기준"</string>
+    <string name="settings" msgid="1534847740615665736">"설정"</string>
+    <string name="add_account" msgid="4271217504968243974">"계정 추가"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"카메라"</string>
+    <string name="folder_download" msgid="7186215137642323932">"다운로드"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"수정된 온라인 사진"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"가져옴"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"스크린샷"</string>
+    <string name="help" msgid="7368960711153618354">"도움말"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"저장소 없음"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"사용할 수 있는 외부 저장소 없음"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"슬라이드 보기"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"바둑판식 보기"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"전체화면 보기"</string>
+    <string name="trimming" msgid="9122385768369143997">"자르기"</string>
+    <string name="muting" msgid="5094925919589915324">"음소거 중"</string>
+    <string name="please_wait" msgid="7296066089146487366">"잠시 기다려 주세요."</string>
+    <string name="save_into" msgid="9155488424829609229">"<xliff:g id="ALBUM_NAME">%1$s</xliff:g>에 동영상 저장 중 …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"잘라낼 수 없음 : 동영상이 너무 짧음"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"파노라마 렌더링"</string>
+    <string name="save" msgid="613976532235060516">"저장"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"콘텐츠를 스캔하는 중..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d개 항목을 스캔함"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d개 항목을 스캔함"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d개 항목을 스캔함"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"정렬 중..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"스캔 완료"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"가져오는 중..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"이 기기로 가져올 수 있는 콘텐츠가 없습니다."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"연결된 MTP 기기가 없습니다."</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"카메라 오류"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"카메라에 연결할 수 없습니다."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"보안 정책으로 인해 카메라 사용이 중지되었습니다."</string>
+    <string name="camera_label" msgid="6346560772074764302">"카메라"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"캠코더"</string>
+    <string name="wait" msgid="8600187532323801552">"잠시 기다려 주세요..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"카메라를 사용하기 전에 USB 저장소를 마운트하세요."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"카메라를 사용하기 전에 SD 카드를 삽입하세요."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB 저장소 준비 중…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD 카드 준비중..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"USB 저장소에 액세스하지 못했습니다."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"SD 카드에 액세스하지 못했습니다."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"취소"</string>
+    <string name="review_ok" msgid="1156261588693116433">"완료"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"저속 촬영"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"카메라 선택"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"후방"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"전방"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"위치 저장"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"카운트다운 타이머"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1초"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d초"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"카운트다운하는 동안 신호음 울리기"</string>
+    <string name="setting_off" msgid="4480039384202951946">"OFF"</string>
+    <string name="setting_on" msgid="8602246224465348901">"ON"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"동영상 화질"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"고화질"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"저화질"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"시간 경과"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"카메라 설정"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"캠코더 설정"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"사진 크기"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M 픽셀"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M 픽셀"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M 픽셀"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M 픽셀"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3M 픽셀"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M 픽셀"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"초점 모드"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"자동"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"무한"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"매크로"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"플래시 모드"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"자동"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"ON"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"OFF"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"화이트 밸런스"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"자동"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"백열"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"일광"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"형광"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"흐림"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"장면 모드"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"자동"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"동작"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"야간"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"일몰"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"파티"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"장면 모드에서 선택할 수 없습니다."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"노출"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"확인"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USB 저장장치의 공간이 부족합니다. 화질 설정을 변경하거나 일부 이미지 또는 기타 파일을 삭제하세요."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"SD 카드의 공간이 부족합니다. 화질 설정을 변경하거나 일부 이미지 또는 기타 파일을 삭제하세요."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"크기 한도에 도달했습니다."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"너무 빠름"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"파노라마 준비 중"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"파노라마를 저장하지 못했습니다."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"파노라마"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"파노라마 캡처 중"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"이전 파노라마가 완료되기를 기다리는 중"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"저장 중…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"파노라마 렌더링"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"초점을 맞추려면 터치하세요."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"효과"</string>
+    <string name="effect_none" msgid="3601545724573307541">"없음"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"찌그러뜨리기"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"눈 크게"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"입 크게"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"입 작게"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"코 크게"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"눈 작게"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"우주"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"일몰"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"내 동영상"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"기기를 내려놓습니다."\n"잠시 동안 시야를 벗어납니다."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"녹화 중에 사진을 찍으려면 터치하세요."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"동영상 녹화가 시작되었습니다."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"동영상 녹화가 중지되었습니다."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"특수 효과가 설정되어 있으면 동영상 스냅샷이 사용 중지됩니다."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"효과 제거"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"웃긴 얼굴"</string>
+    <string name="effect_background" msgid="6579360207378171022">"배경"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"셔터 버튼"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"메뉴 버튼"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"최근 사진"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"전방 및 후방 카메라 전환"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"카메라, 동영상 또는 파노라마 선택기"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"설정 컨트롤 더보기"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"설정 컨트롤 닫기"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"확대/축소 컨트롤"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"%1$s 축소"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"%1$s 확대"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s 확인란"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"사진으로 전환"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"동영상으로 전환"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"파노라마로 전환"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"새 파노라마로 전환"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"리뷰 취소"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"리뷰 완료"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"다시 찍기 검토"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ON"</string>
+    <string name="capital_off" msgid="7231052688467970897">"OFF"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"사용 안함"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5초"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1초"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5초"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2초"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5초"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3초"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4초"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5초"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6초"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10초"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12초"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15초"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24초"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5분"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1분"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5분"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2분"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5분"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3분"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4분"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5분"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6분"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10분"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12분"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15분"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24분"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15시간"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24시간"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"초"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"분"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"시간"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"완료"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"시간 간격 설정"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"시간 경과 기능을 사용하지 않고 있습니다. 시간 간격을 설정하려면 \'사용\'으로 변경해 주세요."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"카운트다운 타이머가 꺼져 있습니다. 사진을 찍기 전에 카운트다운을 켜세요."</string>
+    <string name="set_duration" msgid="5578035312407161304">"시간 설정(초)"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"사진을 찍으려면 카운트다운하세요."</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"사진 위치를 기록하시겠습니까?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"촬영한 위치로 사진과 동영상에 태그를 지정하세요."\n\n"다른 앱이 저장된 이미지와 더불어 이 정보에 액세스할 수 있습니다."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"아니요"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"예"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"카메라"</string>
+    <string name="menu_search" msgid="7580008232297437190">"검색"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"사진"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"앨범"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"사진 %1$d장"</item>
+    <item quantity="other" msgid="3813306834113858135">"사진 %1$d장"</item>
+  </plurals>
+</resources>
diff --git a/res/values-land/dimensions.xml b/res/values-land/dimensions.xml
new file mode 100644
index 0000000..3eae856
--- /dev/null
+++ b/res/values-land/dimensions.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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>
+    <!-- for manage cache bar -->
+    <dimen name="manage_cache_bottom_height">39dp</dimen>
+    <dimen name="capture_top_margin">0dip</dimen>
+</resources>
diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml
new file mode 100644
index 0000000..6ca7e91
--- /dev/null
+++ b/res/values-land/styles.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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">
+    <!-- Spinner primary text is smaller than usual due to extra vertical padding in spinner asset -->
+    <style name="ActionBarTwoLinePrimary" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title">
+        <item name="android:textSize">14sp</item>
+    </style>
+
+    <!-- Camera resources below -->
+
+    <style name="ReviewControlIcon">
+        <item name="android:layout_height">@dimen/switcher_size</item>
+        <item name="android:layout_width">@dimen/switcher_size</item>
+        <item name="android:gravity">center</item>
+        <item name="android:layout_centerHorizontal">true</item>
+        <item name="android:clickable">true</item>
+        <item name="android:focusable">true</item>
+        <item name="android:background">@drawable/bg_pressed</item>
+    </style>
+    <style name="SettingPopupWindow">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_centerVertical">true</item>
+        <item name="android:layout_marginRight">@dimen/setting_popup_right_margin</item>
+        <item name="android:visibility">gone</item>
+    </style>
+    <style name="PopupTitleText">
+        <item name="android:textSize">@dimen/popup_title_text_size</item>
+        <item name="android:layout_gravity">left|center_vertical</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textColor">@color/popup_title_color</item>
+        <item name="android:layout_marginLeft">10dp</item>
+        <item name="android:paddingLeft">16dp</item>
+    </style>
+    <style name="ViewfinderLabelLayout">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:layout_marginLeft">13dp</item>
+        <item name="android:layout_marginRight">@dimen/indicator_bar_width</item>
+        <item name="android:layout_marginBottom">13dp</item>
+        <item name="android:layout_marginTop">13dp</item>
+    </style>
+        <style name="PanoViewHorizontalBar">
+        <item name="android:background">#000000</item>
+        <item name="android:alpha">1.0</item>
+        <item name="android:layout_height">0dp</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_weight">1.5</item>
+    </style>
+    <style name="SettingPopupWindow_xlarge">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_centerVertical">true</item>
+        <item name="android:layout_alignParentRight">true</item>
+        <item name="android:layout_marginRight">@dimen/setting_popup_right_margin</item>
+        <item name="android:visibility">gone</item>
+    </style>
+
+</resources>
diff --git a/res/values-large-hdpi/drawable.xml b/res/values-large-hdpi/drawable.xml
new file mode 100644
index 0000000..b810347
--- /dev/null
+++ b/res/values-large-hdpi/drawable.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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 name="btn_video_shutter_recording_holo" type="drawable">@drawable/btn_video_shutter_recording_holo_large</item>
+    <item name="btn_video_shutter_recording_pressed_holo" type="drawable">@drawable/btn_video_shutter_recording_pressed_holo_large</item>
+    <item name="ic_effects_holo_light" type="drawable">@drawable/ic_effects_holo_light_large</item>
+    <item name="ic_pan_border_fast" type="drawable">@drawable/ic_pan_border_fast_large</item>
+    <item name="ic_pan_left_indicator_fast" type="drawable">@drawable/ic_pan_left_indicator_fast_large</item>
+    <item name="ic_pan_left_indicator" type="drawable">@drawable/ic_pan_left_indicator_large</item>
+    <item name="ic_pan_progression" type="drawable">@drawable/ic_pan_progression_large</item>
+    <item name="ic_pan_right_indicator_fast" type="drawable">@drawable/ic_pan_right_indicator_fast_large</item>
+    <item name="ic_pan_right_indicator" type="drawable">@drawable/ic_pan_right_indicator_large</item>
+    <item name="ic_scn_holo_light" type="drawable">@drawable/ic_scn_holo_light_large</item>
+    <item name="ic_snapshot_border" type="drawable">@drawable/ic_snapshot_border_large</item>
+    <item name="ic_switch_photo_facing_holo_light" type="drawable">@drawable/ic_switch_photo_facing_holo_light_large</item>
+    <item name="ic_switch_video_facing_holo_light" type="drawable">@drawable/ic_switch_video_facing_holo_light_large</item>
+    <item name="ic_timelapse_none" type="drawable">@drawable/ic_timelapse_none_large</item>
+    <item name="list_divider" type="drawable">@drawable/list_divider_large</item>
+</resources>
diff --git a/res/values-large/dimens.xml b/res/values-large/dimens.xml
new file mode 100644
index 0000000..d663a9d
--- /dev/null
+++ b/res/values-large/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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>
+    <dimen name="setting_popup_right_margin">@dimen/setting_popup_right_margin_large</dimen>
+    <dimen name="setting_row_height">@dimen/setting_row_height_large</dimen>
+    <dimen name="setting_popup_window_width">@dimen/setting_popup_window_width_large</dimen>
+    <dimen name="setting_item_icon_width">@dimen/setting_item_icon_width_large</dimen>
+    <dimen name="onscreen_indicators_height">@dimen/onscreen_indicators_height_large</dimen>
+</resources>
diff --git a/res/values-large/filtershow_values.xml b/res/values-large/filtershow_values.xml
new file mode 100644
index 0000000..33b8642
--- /dev/null
+++ b/res/values-large/filtershow_values.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <!-- Specify the screen orientation -->
+    <bool name="only_use_portrait">false</bool>
+
+    <!-- Category Panel Height -->
+    <dimen name="category_panel_height">94dip</dimen>
+
+    <!-- Category Panel Icon Size -->
+    <dimen name="category_panel_icon_size">72dip</dimen>
+
+    <!-- Category Panel Text Size -->
+    <dimen name="category_panel_text_size">14dip</dimen>
+
+    <!-- Category Panel Text Size -->
+    <dimen name="category_panel_margin">4dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-lt/filtershow_strings.xml b/res/values-lt/filtershow_strings.xml
new file mode 100644
index 0000000..6a884b9
--- /dev/null
+++ b/res/values-lt/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Nuotraukų redagavimo priemonė"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Nepavyksta įkelti vaizdo!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Nustatomas ekrano fonas"</string>
+    <string name="original" msgid="3524493791230430897">"Originalas"</string>
+    <string name="borders" msgid="2067345080568684614">"Kraštinės"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Anuliuoti"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Grąžinti"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Rodyti istoriją"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Slėpti istoriją"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Rodyti vaizdo būseną"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Slėpti vaizdo būseną"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Nustatymai"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Yra neišsaugotų šio vaizdo pakeitimų."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Ar norite išsaugoti prieš išeidami?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Išsaugoti ir išeiti"</string>
+    <string name="exit" msgid="242642957038770113">"Išeiti"</string>
+    <string name="history" msgid="455767361472692409">"Istorija"</string>
+    <string name="reset" msgid="9013181350779592937">"Nust. iš naujo"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Pritaikyti efektai"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Palyginti"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Taikyti"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Nust. iš naujo"</string>
+    <string name="aspect" msgid="4025244950820813059">"Vaizdo santykis"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Nėra"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fiksuota"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Maža plan."</string>
+    <string name="exposure" msgid="6526397045949374905">"Išlaikymas"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ryškumas"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrastas"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Gyvumas"</string>
+    <string name="saturation" msgid="7026791551032438585">"Spalvų sodrumas"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Nesp. filtras"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autom. spalva"</string>
+    <string name="hue" msgid="6231252147971086030">"Atspalvis"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Šešėliai"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Paryškinimai"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kreivės"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinjetė"</string>
+    <string name="redeye" msgid="4508883127049472069">"Raudonos akys"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Piešti"</string>
+    <string name="straighten" msgid="26025591664983528">"Ištiesinti"</string>
+    <string name="crop" msgid="5781263790107850771">"Apkarpyti"</string>
+    <string name="rotate" msgid="2796802553793795371">"Sukti"</string>
+    <string name="mirror" msgid="5482518108154883096">"Veidrod. atsp."</string>
+    <string name="negative" msgid="6998313764388022201">"Negatyvas"</string>
+    <string name="none" msgid="6633966646410296520">"Nėra"</string>
+    <string name="edge" msgid="7036064886242147551">"Kraštai"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Vorholas"</string>
+    <string name="downsample" msgid="3552938534146980104">"Mažinimas"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Raudona"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Žalia"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Mėlyna"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stilius"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Dydis"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Spalva"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linijos"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Žymeklis"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Taškymas"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Išvalyti"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Pasirinkti tinkintą spalvą"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Pasirinkti spalvą"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Pasirinkti dydį"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"Gerai"</string>
+</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
new file mode 100644
index 0000000..b36c6e7
--- /dev/null
+++ b/res/values-lt/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerija"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Paveikslėlio rėmelis"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Vaizdo įrašų grotuvas"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Įkeliamas vaizdo įrašas..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Įkeliamas vaizdas..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Įkeliama paskyra…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Atnaujinti vaizdo įrašą"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Atnaujinti leidimą nuo %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Atnaujinti grojimą"</string>
+    <string name="loading" msgid="7038208555304563571">"Įkeliama…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nepavyko įkelti"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nepavyko įkelti vaizdo"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Nėra miniatiūros"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Pradėti iš naujo"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Gerai"</string>
+    <string name="ok" msgid="5296833083983263293">"Gerai"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Jei norite pradėti, palieskite veidą."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Išsaugomas paveikslėlis..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Nuotrauka išsaugoma albume „<xliff:g id="ALBUM_NAME">%1$s</xliff:g>“…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Nepavyko išsaugoti apkarpyto vaizdo."</string>
+    <string name="crop_label" msgid="521114301871349328">"Apkarpyti paveikslėlį"</string>
+    <string name="trim_label" msgid="274203231381209979">"Apkarpyti vaizdo įrašą"</string>
+    <string name="select_image" msgid="7841406150484742140">"Pasirinkti nuotrauką"</string>
+    <string name="select_video" msgid="4859510992798615076">"Pasir. vaizdo įrašą"</string>
+    <string name="select_item" msgid="2816923896202086390">"Pasirinkti elementą"</string>
+    <string name="select_album" msgid="1557063764849434077">"Pasirinkti albumą"</string>
+    <string name="select_group" msgid="6744208543323307114">"Pasirinkti grupę"</string>
+    <string name="set_image" msgid="2331476809308010401">"Nustatyti paveikslėlį kaip"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nustat. darbal. foną"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Nustatomas darbalaukio fonas…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Darbalaukio fonas"</string>
+    <string name="delete" msgid="2839695998251824487">"Ištrinti"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ištrinti pasirinktą elementą?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Ištrinti pasirink. elementus?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Patvirtinti"</string>
+    <string name="cancel" msgid="3637516880917356226">"Atšaukti"</string>
+    <string name="share" msgid="3619042788254195341">"Bendrinti"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Bendrinti panoramą"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Bendrinti kaip nuotrauką"</string>
+    <string name="deleted" msgid="6795433049119073871">"Ištrinta"</string>
+    <string name="undo" msgid="2930873956446586313">"ANULIUOTI"</string>
+    <string name="select_all" msgid="3403283025220282175">"Pasirinkti viską"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Panaikinti visus žymėjimus"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Skaidrių demonstracija"</string>
+    <string name="details" msgid="8415120088556445230">"Išsami informacija"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d iš %2$d element.:"</string>
+    <string name="close" msgid="5585646033158453043">"Uždaryti"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Perjungti į fotoaparatą"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Pasirinkta: %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Pasirinkta: %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Pasirinkta: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Pasirinkta: %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Pasirinkta: %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Pasirinkta: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Pasirinkta: %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Pasirinkta: %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Pasirinkta: %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Rodyti žemėlapyje"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Sukti į kairę"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Sukti į dešinę"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Nepavyko surasti elemento."</string>
+    <string name="edit" msgid="1502273844748580847">"Redaguoti"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Talpinimo užklausų apdorojimas"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Talpinama…"</string>
+    <string name="crop_action" msgid="3427470284074377001">"Apkarpyti"</string>
+    <string name="trim_action" msgid="703098114452883524">"Apkarpyti"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Nutildyti"</string>
+    <string name="set_as" msgid="3636764710790507868">"Nustatyti kaip"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Negalima nutildyti vaizdo įr."</string>
+    <string name="video_err" msgid="7003051631792271009">"Negalima paleisti vaizdo įrašo."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Pagal vietą"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Pagal laiką"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Pagal žymas"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Pagal žmones"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Pagal albumą"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Pagal dydį"</string>
+    <string name="untagged" msgid="7281481064509590402">"Nepažymėta"</string>
+    <string name="no_location" msgid="4043624857489331676">"Nėra vietos"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Kai kurių vietų nepavyko nustatyti dėl tinklo problemų."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Šio albumo nuotraukų atsisiųsti nepavyko. Bandykite dar kartą vėliau."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Tik vaizdai"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Tik vaizdo įrašai"</string>
+    <string name="show_all" msgid="6963292714584735149">"Vaizdai ir vaizdo įrašai"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Nuotraukų galerija"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Nėra nuotraukų."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Apkarpytas vaizdas išsaugotas aplanke <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nėra pasiekiamų albumų."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Nepasiekiami jokie vaizdai / vaizdo įrašai."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Įrašai"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Padaryti pasiekiamą neprisij."</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Atnaujinti"</string>
+    <string name="done" msgid="217672440064436595">"Atlikta"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d iš %2$d element.:"</string>
+    <string name="title" msgid="7622928349908052569">"Pareigos"</string>
+    <string name="description" msgid="3016729318096557520">"Apibūdinimas"</string>
+    <string name="time" msgid="1367953006052876956">"Laikas"</string>
+    <string name="location" msgid="3432705876921618314">"Vieta"</string>
+    <string name="path" msgid="4725740395885105824">"Kelias"</string>
+    <string name="width" msgid="9215847239714321097">"Plotis"</string>
+    <string name="height" msgid="3648885449443787772">"Aukštis"</string>
+    <string name="orientation" msgid="4958327983165245513">"Padėtis"</string>
+    <string name="duration" msgid="8160058911218541616">"Trukmė"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME tipas"</string>
+    <string name="file_size" msgid="8486169301588318915">"Failo dydis"</string>
+    <string name="maker" msgid="7921835498034236197">"Kūrėjas"</string>
+    <string name="model" msgid="8240207064064337366">"Modelis"</string>
+    <string name="flash" msgid="2816779031261147723">"Blykstė"</string>
+    <string name="aperture" msgid="5920657630303915195">"Diafragma"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Židinio nuotolis"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balt. sp. bal."</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Poveik. trukmė"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Rankiniu būdu"</string>
+    <string name="auto" msgid="4296941368722892821">"Automobiliai"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Blykstė suveikė"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Be blykstės"</string>
+    <string name="unknown" msgid="3506693015896912952">"Nežinoma"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Originalas"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Senovinis"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Momentinis"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Balinti"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Mėlyna"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Nespalvota"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X procesas"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latės atsp."</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografija"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Nustatoma, kad albumas būtų pasiekiamas neprisij."</item>
+    <item quantity="other" msgid="4948604338155959389">"Nustatoma, kad albumai būtų pasiekiami neprisij."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ši prekė saugoma vietinėje atmintinėje ir yra pasiekiama neprisijungus."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Visi albumai"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Vietiniai albumai"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP įrenginiai"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"„Picasa“ albumai"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> laisvos vietos"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ar mažiau"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ar daugiau"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g>–<xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importuoti"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importavimas baigtas"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Importuoti nepavyko"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Fotoaparatas prijungtas."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Fotoaparatas atjungtas."</string>
+    <string name="click_import" msgid="6407959065464291972">"Jei norite importuoti, palieskite čia"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Pasirinkti albumą"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Maišyti visus vaizdus"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Pasirinkite vaizdą"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Pasirinkti vaizdų"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Skaidrių demonstrac."</string>
+    <string name="albums" msgid="7320787705180057947">"Albumai"</string>
+    <string name="times" msgid="2023033894889499219">"Kartai"</string>
+    <string name="locations" msgid="6649297994083130305">"Vietos"</string>
+    <string name="people" msgid="4114003823747292747">"Žmonės"</string>
+    <string name="tags" msgid="5539648765482935955">"Žymos"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupuoti pagal"</string>
+    <string name="settings" msgid="1534847740615665736">"Nustatymai"</string>
+    <string name="add_account" msgid="4271217504968243974">"Pridėti paskyrą"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparatas"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Atsisiųsti"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Redaguotos nuotraukos internete"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importuota"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekrano kopija"</string>
+    <string name="help" msgid="7368960711153618354">"Pagalba"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nėra atmintinės"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nepasiekiama jokia išorinė atmintinė"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmo juostos rodinys"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Tinklelio rodinys"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Viso ekrano rodinys"</string>
+    <string name="trimming" msgid="9122385768369143997">"Apkarpymas"</string>
+    <string name="muting" msgid="5094925919589915324">"Nutildoma"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Palaukite"</string>
+    <string name="save_into" msgid="9155488424829609229">"Vaizdo įrašas išsaugomas albume „<xliff:g id="ALBUM_NAME">%1$s</xliff:g>“…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Negalima apkarpyti: tikslinis vaizdo įrašas per trumpas"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Atvaizduojama panorama"</string>
+    <string name="save" msgid="613976532235060516">"Išsaugoti"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Nuskaitomas turinys..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Nuskaityta elementų: %1$d"</item>
+    <item quantity="one" msgid="4340019444460561648">"Nuskaityta elementų: %1$d"</item>
+    <item quantity="other" msgid="3138021473860555499">"Nuskaityta elementų: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Rūšiuojama..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Nuskaityta"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importuojama..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Nėra turinio, kurį galima importuoti į šį įrenginį."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nėra prijungto MTP įrenginio"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Fotoaparato klaida"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Nepavyksta prisijungti prie kameros."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Dėl saugumo politikos kamera neleidžiama."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Fotoaparatas"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Nešiojamoji vaizdo kamera ir magnetofonas"</string>
+    <string name="wait" msgid="8600187532323801552">"Palaukite..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Prieš naudodami fotoaparatą įrenkite USB atmintį."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Prieš naudodami fotoaparatą įdėkite SD kortelę."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Ruošiama USB atmintinė..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Ruošiama SD kortelė..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nepavyko pasiekti USB atminties."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nepavyko pasiekti SD kortelės."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ATŠAUKTI"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ATLIKTA"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Laiko tarpo įrašymas"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Pasirin. fotoaparatą"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Atgal"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Šriftas"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Išsaugoti vietą"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Atvirkštinis laikmatis"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sek."</item>
+    <item quantity="other" msgid="6455381617076792481">"%d sek."</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Laikmačio pyps."</string>
+    <string name="setting_off" msgid="4480039384202951946">"Išjungta"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Įjungta"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Vaizdo įrašo kokybė"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Aukšta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Prasta"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Laiko intervalas"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Fotoaparato nustatymai"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Nešiojamosios vaizdo kameros ir magnetofono nustatymai"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Paveikslėlio dydis"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapiks."</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 mln. piks."</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 mln. piks."</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 mln. piks."</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 mln. piks."</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 mln. piks."</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fokusavimo režimas"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatiškai"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Begalybė"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makrokomanda"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"„Flash“ režimas"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatiškai"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Įjungta"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Išjungta"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Baltos spalvos balansas"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatiškai"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Akinančiai skaistus"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Dienos šviesa"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescencinis"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Debesuota"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scenų režimas"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatiškai"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Veiksmas"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Naktis"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Saulėlydis"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Vakarėlis"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Negalima pasirinkti, kai veikia scenos režimas."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Išlaikymas"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"Gerai"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USB atmintinėje trūksta vietos. Pakeiskite kokybės nustatymą arba ištrinkite kelis vaizdus ar kitus failus."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"SD kortelėje trūksta vietos. Pakeiskite kokybės nustatymą arba ištrinkite kelis vaizdus ar kitus failus."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Pasiekta dydžio riba."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Per greitai"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Ruošiama panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Nepavyko išsaugoti panoramos."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Fiksuojama panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Laukiama ankstesnės panoramos"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Išsaugoma..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Atvaizduojama panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Palieskite, kad fokusuot."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efektai"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Joks"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Suspausti"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Didelės akys"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Didelė burna"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Maža burna"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Didelė nosis"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Mažos akys"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Kosmose"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Saulėlydis"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Vaizdo įr."</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Išjunkite įrenginį."\n"Trumpam išeikite iš rodinio."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Palieskite, kad įrašydami nufotografuotumėte."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Prasidėjo vaizdo įrašo įrašymas."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Vaizdo įrašo įrašymas sustabdytas."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Moment. vaizdo įrašo vaizdas neleidž., kai įjungti spec. efektai."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Išvalyti efektus"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"JUOKINGI VEIDAI"</string>
+    <string name="effect_background" msgid="6579360207378171022">"FONAS"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Užrakto mygtukas"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Meniu mygtukas"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Naujausia nuotrauka"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Priekinis ir galinis fotoaparato jungikliai"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kameros, vaizdo įrašo ar panoramos parinkiklis"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Daugiau nustatymų valdiklių"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Uždaryti nustatymų valdiklius"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Mastelio keitimo valdymas"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Sumažinti %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Padidinti %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s žymimasis laukelis"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Perjungti į fotografavimo režimą"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Perjungti į vaizdo įrašo režimą"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Perjungti į panoramos režimą"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Perjungti į naują panoramą"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Atšaukimas peržiūros režimu"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Atlikta peržiūros režimu"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Peržiūrėti / fotografuoti arba filmuoti iš naujo"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ĮJUNGTA"</string>
+    <string name="capital_off" msgid="7231052688467970897">"IŠJUNGTA"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Išj."</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sek."</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 min."</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 val."</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 val."</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekundės"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutės"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"valandos"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Atlikta"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Nustatyti laiko intervalą"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Laiko intervalo funkcija išjungta. Įjunkite ją, kad nustatytumėte laiko intervalą."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Atvirkštinis laikmatis išjungtas. Įjunkite jį prieš fotografuodami."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Nustatyti trukmę sekundėmis"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Skaičiuojamas laikas iki fotografavimo"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Atsiminti nuotraukų vietas?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Žymėkite nuotraukas ir vaizdo įrašus nurodydami vietas, kur jie buvo sukurti."\n\n"Kitos programos gali pasiekti šią informaciją kartu su išsaugotais vaizdais."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Ne, ačiū"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Taip"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Fotoaparatas"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Paieška"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Nuotraukos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumai"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"Nuotraukų: %1$d"</item>
+    <item quantity="other" msgid="3813306834113858135">"Nuotraukų: %1$d"</item>
+  </plurals>
+</resources>
diff --git a/res/values-lv/filtershow_strings.xml b/res/values-lv/filtershow_strings.xml
new file mode 100644
index 0000000..cf2ffc1
--- /dev/null
+++ b/res/values-lv/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fotoattēlu redaktors"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Nevar ielādēt attēlu."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Notiek fona tapetes iestatīšana"</string>
+    <string name="original" msgid="3524493791230430897">"Oriģināls"</string>
+    <string name="borders" msgid="2067345080568684614">"Robežas"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Atsaukt"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Atcelt atsaukšanu"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Rādīt vēsturi"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Slēpt vēsturi"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Rādīt attēla statusu"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Slēpt attēla statusu"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Iestatījumi"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Šajā attēlā ir veiktas izmaiņas, kas nav saglabātas."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Vai vēlaties saglabāt pirms iziešanas?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Saglabāt un iziet"</string>
+    <string name="exit" msgid="242642957038770113">"Iziet"</string>
+    <string name="history" msgid="455767361472692409">"Vēsture"</string>
+    <string name="reset" msgid="9013181350779592937">"Atiestatīt"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Izmantotie efekti"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Salīdzināt"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Lietot"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Atiestatīt"</string>
+    <string name="aspect" msgid="4025244950820813059">"Malu attiecība"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Nav"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fiksēts"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Ekspozīcija"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Asums"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrasts"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Dzīvīgums"</string>
+    <string name="saturation" msgid="7026791551032438585">"Piesātinājums"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"M/B filtrs"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Aut. krāsu pal."</string>
+    <string name="hue" msgid="6231252147971086030">"Nokrāsa"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Ēnas"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Gaišās vietas"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Līknes"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinjete"</string>
+    <string name="redeye" msgid="4508883127049472069">"Sarkano acu ef."</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Zīmējumi"</string>
+    <string name="straighten" msgid="26025591664983528">"Iztaisnošana"</string>
+    <string name="crop" msgid="5781263790107850771">"Izgriešana"</string>
+    <string name="rotate" msgid="2796802553793795371">"Pagriezt"</string>
+    <string name="mirror" msgid="5482518108154883096">"Spogulis"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatīvs"</string>
+    <string name="none" msgid="6633966646410296520">"Nav"</string>
+    <string name="edge" msgid="7036064886242147551">"Malas"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Vorhols"</string>
+    <string name="downsample" msgid="3552938534146980104">"Sam. liel."</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Sarkans"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Zaļš"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Zils"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stils"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Izmēri"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Krāsa"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Līnijas"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marķieris"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Šļakatas"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Notīrīt"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Izvēlēties pielāgotu krāsu"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Krāsas atlase"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Izmēru atlase"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"Labi"</string>
+</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
new file mode 100644
index 0000000..04385ba
--- /dev/null
+++ b/res/values-lv/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerija"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Attēla ietvars"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Video atskaņotājs"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Notiek video ielāde..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Notiek attēla ielāde…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Notiek konta ielāde…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Atsākt video atskaņošanu"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Vai atsākt atskaņošanu no %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Atsākt atskaņošanu"</string>
+    <string name="loading" msgid="7038208555304563571">"Notiek ielāde…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nevarēja ielādēt"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nevarēja ielādēt attēlu."</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Nav sīktēla."</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Sākt vēlreiz"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Labi"</string>
+    <string name="ok" msgid="5296833083983263293">"Labi"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Pieskarieties sejai, lai sāktu."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Notiek attēla saglabāšana..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Notiek attēla saglabāšana albumā <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Nevarēja saglabāt izgriezto attēlu."</string>
+    <string name="crop_label" msgid="521114301871349328">"Apgriezt attēlu"</string>
+    <string name="trim_label" msgid="274203231381209979">"Saīsināt videoklipu"</string>
+    <string name="select_image" msgid="7841406150484742140">"Atlasiet fotoattēlu"</string>
+    <string name="select_video" msgid="4859510992798615076">"Atlasiet videoklipu"</string>
+    <string name="select_item" msgid="2816923896202086390">"Atlasiet vienumu"</string>
+    <string name="select_album" msgid="1557063764849434077">"Albuma atlase"</string>
+    <string name="select_group" msgid="6744208543323307114">"Atlasiet grupu"</string>
+    <string name="set_image" msgid="2331476809308010401">"Iestatīt attēlu kā:"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Iestatīt fona tapeti"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Notiek fona tapetes iestatīšana..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fona tapete"</string>
+    <string name="delete" msgid="2839695998251824487">"Dzēst"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vai dzēst atlasīto vienumu?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vai dzēst atlasītos vienumus?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Apstiprināt"</string>
+    <string name="cancel" msgid="3637516880917356226">"Atcelt"</string>
+    <string name="share" msgid="3619042788254195341">"Dalies"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Kopīgot panorāmu"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Kopīgot kā fotoattēlu"</string>
+    <string name="deleted" msgid="6795433049119073871">"Dzēsts"</string>
+    <string name="undo" msgid="2930873956446586313">"ATSAUKT"</string>
+    <string name="select_all" msgid="3403283025220282175">"Atlasīt visu"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Atcelt visu atlasi"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Slaidrāde"</string>
+    <string name="details" msgid="8415120088556445230">"Detalizēta informācija"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d no %2$d vienumiem:"</string>
+    <string name="close" msgid="5585646033158453043">"Aizvērt"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Pārslēgšanās uz liet. Kamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Atlasīta %1$d vienumu"</item>
+    <item quantity="one" msgid="2478365152745637768">"Atlasīts %1$d vienums"</item>
+    <item quantity="other" msgid="754722656147810487">"Atlasīti %1$d vienumi"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Atlasīta %1$d vienumu"</item>
+    <item quantity="one" msgid="6184377003099987825">"Atlasīts %1$d vienums"</item>
+    <item quantity="other" msgid="53105607141906130">"Atlasīti %1$d vienumi"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Atlasīta %1$d vienumu"</item>
+    <item quantity="one" msgid="5030162638216034260">"Atlasīts %1$d vienums"</item>
+    <item quantity="other" msgid="3512041363942842738">"Atlasīti %1$d vienumi"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Rādīt kartē"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Pagriezt pa kreisi"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Pagriezt pa labi"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Nevarēja atrast vienumu."</string>
+    <string name="edit" msgid="1502273844748580847">"Rediģēt"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Tiek apstrādāti pieprasījumi rakstīšanai kešatmiņā"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Glabā kešatm..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Apgriezt"</string>
+    <string name="trim_action" msgid="703098114452883524">"Apgriezt"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Izslēgt skaņu"</string>
+    <string name="set_as" msgid="3636764710790507868">"Iestatīt kā:"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Nevar izslēgt videoklipa skaņu"</string>
+    <string name="video_err" msgid="7003051631792271009">"Nevar atskaņot video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Pēc atrašanās vietas"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Pēc uzņemšanas laika"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Pēc atzīmēm"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Pēc personām"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Pēc albumiem"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Pēc izmēra"</string>
+    <string name="untagged" msgid="7281481064509590402">"Bez atzīmēm"</string>
+    <string name="no_location" msgid="4043624857489331676">"Nav vietas inform."</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Tīkla problēmu dēļ nevarēja noteikt dažas atrašanās vietas."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Nevarēja lejupielādēt šī albuma fotoattēlus. Vēlāk mēģiniet vēlreiz."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Tikai attēli"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Tikai videoklipi"</string>
+    <string name="show_all" msgid="6963292714584735149">"Attēli un videoklipi"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerija"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Nav fotoattēlu."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Apgrieztais attēls ir saglabāts mapē <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nav pieejams neviens albums."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Nav pieejams neviens attēls/videoklips."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Ziņas"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Padarīt pieejamu bezsaistē"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Atsvaidzināt"</string>
+    <string name="done" msgid="217672440064436595">"Gatavs"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d no %2$d vienumiem:"</string>
+    <string name="title" msgid="7622928349908052569">"Nosaukums"</string>
+    <string name="description" msgid="3016729318096557520">"Apraksts"</string>
+    <string name="time" msgid="1367953006052876956">"Laiks"</string>
+    <string name="location" msgid="3432705876921618314">"Atrašanās vieta"</string>
+    <string name="path" msgid="4725740395885105824">"Ceļš"</string>
+    <string name="width" msgid="9215847239714321097">"Platums"</string>
+    <string name="height" msgid="3648885449443787772">"Augstums"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientācija"</string>
+    <string name="duration" msgid="8160058911218541616">"Ilgums"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME veids"</string>
+    <string name="file_size" msgid="8486169301588318915">"Faila lielums"</string>
+    <string name="maker" msgid="7921835498034236197">"Izgatavotājs"</string>
+    <string name="model" msgid="8240207064064337366">"Modelis"</string>
+    <string name="flash" msgid="2816779031261147723">"Zibspuldze"</string>
+    <string name="aperture" msgid="5920657630303915195">"Diafragmas atvērums"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Fokusa attālums"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Baltā balanss"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Ekspon. laiks"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Rokasgrāmata"</string>
+    <string name="auto" msgid="4296941368722892821">"Automātiski"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Zibspuldze ir aktivizēta"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Bez zibspuldzes"</string>
+    <string name="unknown" msgid="3506693015896912952">"Nezināms"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Sākotnējais"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Senlaicīgs"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Ātrais foto"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Balināts"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Zils"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Melnbalts"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Dzirkstošs"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Balta kafija"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litogrāfija"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Albums tiek padarīts pieejams bezsaistē."</item>
+    <item quantity="other" msgid="4948604338155959389">"Albumi tiek padarīti pieejami bezsaistē."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Šis vienums tiek glabāts lokāli un ir pieejams bezsaistē."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Visi albumi"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Albumi ierīcē"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP ierīces"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa albumi"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Brīva vieta: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> vai mazāk"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> vai vairāk"</string>
+    <string name="size_between" msgid="8779660840898917208">"No <xliff:g id="MIN_SIZE">%1$s</xliff:g> līdz <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importēt"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importēš. pabeigta."</string>
+    <string name="import_fail" msgid="8497942380703298808">"Neveiksmīga importēšana"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Ir pievienota kamera."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera ir atvienota."</string>
+    <string name="click_import" msgid="6407959065464291972">"Pieskarieties šeit, lai importētu."</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Izvēlēties albumu"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Rādīt attēlus jauktā secībā"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Izvēlēties attēlu"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Attēlu izvēle"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Slaidrāde"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumi"</string>
+    <string name="times" msgid="2023033894889499219">"Reizes"</string>
+    <string name="locations" msgid="6649297994083130305">"Vietas"</string>
+    <string name="people" msgid="4114003823747292747">"Personas"</string>
+    <string name="tags" msgid="5539648765482935955">"Atzīmes"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupēt pēc:"</string>
+    <string name="settings" msgid="1534847740615665736">"Iestatījumi"</string>
+    <string name="add_account" msgid="4271217504968243974">"Konta pievienošana"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Lejupielādētie"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Rediģēti fotoattēli tiešsaistē"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importētie"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekrānuzņēmums"</string>
+    <string name="help" msgid="7368960711153618354">"Palīdzība"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nav atmiņas"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nav ārējās atmiņas"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Kinolentes skats"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Režģa skats"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Pilnekrāna skatījums"</string>
+    <string name="trimming" msgid="9122385768369143997">"Notiek apgriešana"</string>
+    <string name="muting" msgid="5094925919589915324">"Skaņas izslēgšana"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Lūdzu, uzgaidiet!"</string>
+    <string name="save_into" msgid="9155488424829609229">"Notiek videoklipa saglabāšana albumā <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Nevar apgriezt: mērķa videoklips ir pārāk īss."</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Notiek panorāmas atveidošana"</string>
+    <string name="save" msgid="613976532235060516">"Saglabāt"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Notiek satura skenēšana..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Skenēti %1$d vienumi"</item>
+    <item quantity="one" msgid="4340019444460561648">"Skenēts %1$d vienums"</item>
+    <item quantity="other" msgid="3138021473860555499">"Skenēti %1$d vienumi"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Notiek kārtošana..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skenēšana ir pabeigta."</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Notiek importēšana..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Nav pieejams šajā ierīcē importējams saturs."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nav pievienota neviena MTP ierīce."</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kameras kļūda"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Nevar izveidot savienojumu ar kameru."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kamera ir atspējota drošības politiku dēļ."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Lūdzu, uzgaidiet…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Pirms kameras lietošanas pievienojiet USB atmiņu."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Pirms kameras izmantošanas ievietojiet SD karti."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Notiek USB atm. sagatavošana"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Notiek SD kartes sagatavošana..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nevarēja piekļūt USB atmiņai."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nevarēja piekļūt SD kartei."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ATCELT"</string>
+    <string name="review_ok" msgid="1156261588693116433">"GATAVS"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Ierakstīšana laika intervāla režīmā"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Kameras izvēlēšanās"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Atpakaļ"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Priekšpuse"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Saglabāt atrašanās vietu"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Laika atskaites taimeris"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sekunde"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d sekundes"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Atskaņot signālu"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Izslēgts"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Ieslēgts"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Video kvalitāte"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Augsta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Zema"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Laika intervāls"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kameras iestatījumi"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Videokameras iestatījumi"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Attēla izmērs"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapikseļi"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapikseļi"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapikseļi"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapikseļi"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapikseļi"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapikselis"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fokusa režīms"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automātiski"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Bezgalība"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Zibspuldzes režīms"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automātiski"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Ieslēgts"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Izslēgt"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Baltās krāsas balanss"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automātiski"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Spožs"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Dienasgaisma"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescējošs"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Mākoņains"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Ainas režīms"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automātiski"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Kustība"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nakts"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Saulriets"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Viesības"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Nevar atlasīt ainavas režīmā."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Ekspozīcija"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"Labi"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USB atmiņa drīz būs pilna. Mainiet kvalitātes iestatījumu vai dzēsiet dažus attēlus vai citus failus."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"SD karte drīz būs pilna. Mainiet kvalitātes iestatījumu vai dzēsiet dažus attēlus vai citus failus."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Lieluma ierobežojums ir sasniegts."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Pārāk ātri"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Notiek panorāmas sagatavošana"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Nevarēja saglabāt panorāmu."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorāma"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Notiek panorāmas tveršana"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Tiek gaidīta iepriekšējā panorāma."</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Saglabā..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Notiek panorāmas atveide"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Pieskarieties, lai fokusētu."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efekti"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Nav"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Saspiest"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Lielas acis"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Liela mute"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Maza mute"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Liels deguns"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Mazas acis"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Kosmosā"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Saulriets"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Videoklips"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Novietojiet ierīci uz leju."\n"Uz brīdi izejiet no kameras skata."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Pieskarieties, lai uzņemtu fotoattēlu ieraksta laikā."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Video ierakstīšana ir sākta."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Video ierakstīšana ir pārtraukta."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Video momentuzņēmums ir atspējots, ja ir ieslēgti specefekti."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Noņemt efektus"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"SMIEKL. SEJAS IZTEIKSMES"</string>
+    <string name="effect_background" msgid="6579360207378171022">"FONS"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Slēdža poga"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Izvēlnes poga"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Pēdējais fotoattēls"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Priekšējais un aizmugurējais kameras slēdzis"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kameras, videokameras vai panorāmas atlasītājs"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Vairāk iestatījumu vadīklu"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Aizvērt iestatījumu vadīklas"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Tālummaiņas vadība"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Samazināt %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Palielināt %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s izvēles rūtiņa"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Pārslēgt uz fotoattēlu režīmu"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Pārslēgt uz video režīmu"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Pārslēgt uz panorāmas režīmu"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Pārslēgties uz jaunu panorāmu"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Atcelt pārskatīšanu"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Pabeigt pārskatīšanu"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Pārskatīt atkārtotu attēlu"</string>
+    <string name="capital_on" msgid="5491353494964003567">"IESLĒGTS"</string>
+    <string name="capital_off" msgid="7231052688467970897">"IZSLĒGTS"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Beidzies"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekundes"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minūte"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minūtes"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 stunda"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 stundas"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 stundas"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekunde(-es)"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minūte(-es)"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"stundas"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Gatavs"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Laika intervāla iestatīšana"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Palēninātās filmēšanas funkcija ir izslēgta. Ieslēdziet to, lai iestatītu laika intervālu."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Laika atskaites taimeris ir izslēgts. Ieslēdziet to, lai skaitītu laiku pirms attēla uzņemšanas."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Iestatīt ilgumu sekundēs"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Notiek laika skaitīšana līdz fotoattēla uzņemšanai"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Vai atcerēties fotoattēlu uzņemšanas vietas?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Atzīmē jūsu fotoattēlos un videoklipos atrašanās vietas, kurās tie tika uzņemti."\n\n"Citas lietotnes var piekļūt šai informācijai līdz ar saglabātajiem attēliem."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nē, paldies!"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Jā"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Meklēt"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotoattēli"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumi"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotoattēls"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotoattēli"</item>
+  </plurals>
+</resources>
diff --git a/res/values-ms/filtershow_strings.xml b/res/values-ms/filtershow_strings.xml
new file mode 100644
index 0000000..e41024f
--- /dev/null
+++ b/res/values-ms/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor Foto"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Tidak dapat memuatkan imej!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Menetapkan kertas dinding"</string>
+    <string name="original" msgid="3524493791230430897">"Asli"</string>
+    <string name="borders" msgid="2067345080568684614">"Sempadan"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Buat asal"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Buat semula"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Tunjukkan Sejarah"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Sembunyikan Sejarah"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Tunjukkan Keadaan Imej"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Sembunyikan Keadaan Imej"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Tetapan"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Terdapat perubahan kepada imej ini yang tidak disimpan."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Adakah anda ingin simpan sebelum keluar?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Simpan dan Keluar"</string>
+    <string name="exit" msgid="242642957038770113">"Keluar"</string>
+    <string name="history" msgid="455767361472692409">"Sejarah"</string>
+    <string name="reset" msgid="9013181350779592937">"Tetapkan semula"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Kesan Digunakan"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Bandingkan"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Gunakan"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Tetapkan semula"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspek"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Tiada"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Dibetulkan"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Planet Kecil"</string>
+    <string name="exposure" msgid="6526397045949374905">"Dedahan"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ketajaman"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontras"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Kecerahan"</string>
+    <string name="saturation" msgid="7026791551032438585">"Ketepuan"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Penapis H &amp; P"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autowarna"</string>
+    <string name="hue" msgid="6231252147971086030">"Rona"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Bayang-bayang"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Serlahan"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Lengkung"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignet"</string>
+    <string name="redeye" msgid="4508883127049472069">"Mata Merah"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Lukis"</string>
+    <string name="straighten" msgid="26025591664983528">"Luruskan"</string>
+    <string name="crop" msgid="5781263790107850771">"Pangkas"</string>
+    <string name="rotate" msgid="2796802553793795371">"Putar"</string>
+    <string name="mirror" msgid="5482518108154883096">"Cermin"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatif"</string>
+    <string name="none" msgid="6633966646410296520">"Tiada"</string>
+    <string name="edge" msgid="7036064886242147551">"Pinggir"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Mengecil"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Merah"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Hijau"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Biru"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Gaya"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Saiz"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Warna"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Garisan"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Penanda"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Percik"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Padam bersih"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Pilih warna peribadi"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Pilih Warna"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Pilih Saiz"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
new file mode 100644
index 0000000..b1ed9b3
--- /dev/null
+++ b/res/values-ms/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeri"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Bingkai gambar"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Pemain video"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Memuatkan video..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Memuatkan imej..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Memuatkan akaun…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Sambung semula video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Sambung semula proses main dari %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Sambung semula proses main"</string>
+    <string name="loading" msgid="7038208555304563571">"Memuatkan..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Tidak dapat memuatkan"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Tidak dapat memuatkan imej"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Tiada lakaran kenit"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Mainkan semula dari mula"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Sentuh muka untuk bermula."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Menyimpan gambar..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Menyimpan gambar ke <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Tidak dapat menyimpan imej yang dipangkas."</string>
+    <string name="crop_label" msgid="521114301871349328">"Pangkas gambar"</string>
+    <string name="trim_label" msgid="274203231381209979">"Potong video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Pilih foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Pilih video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Pilih item"</string>
+    <string name="select_album" msgid="1557063764849434077">"Pilih album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Pilih kumpulan"</string>
+    <string name="set_image" msgid="2331476809308010401">"Tetapkan gambar sebagai"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Tetapkan kertas dinding"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Menetapkan kertas dinding…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Kertas dinding"</string>
+    <string name="delete" msgid="2839695998251824487">"Padam"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Padam item yang dipilih?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Padam item yang dipilih?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Sahkan"</string>
+    <string name="cancel" msgid="3637516880917356226">"Batal"</string>
+    <string name="share" msgid="3619042788254195341">"Kongsi"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Kongsi panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Kongsikan sebagai foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Dipadamkan"</string>
+    <string name="undo" msgid="2930873956446586313">"BUAT ASAL"</string>
+    <string name="select_all" msgid="3403283025220282175">"Pilih semua"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Nyahpilih semua"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Tayangan slaid"</string>
+    <string name="details" msgid="8415120088556445230">"Butiran"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d dari %2$d item:"</string>
+    <string name="close" msgid="5585646033158453043">"Tutup"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Bertukar kepada kamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d dipilih"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d dipilih"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d dipilih"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d dipilih"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d dipilih"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d dipilih"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d dipilih"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d dipilih"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d dipilih"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Tunjukkan pada peta"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Putar ke kiri"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Putar ke kanan"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Tidak dapat mencari item."</string>
+    <string name="edit" msgid="1502273844748580847">"Edit"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Memproses permintaan cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Mengcache..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Pangkas"</string>
+    <string name="trim_action" msgid="703098114452883524">"Pangkas"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Redam"</string>
+    <string name="set_as" msgid="3636764710790507868">"Tetapkan sebagai"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Tidak dapat meredam video."</string>
+    <string name="video_err" msgid="7003051631792271009">"Tidak boleh memainkan video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Mengikut lokasi"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Mengikut masa"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Mengikut teg"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Mengikut orang"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Mengikut album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Mengikut saiz"</string>
+    <string name="untagged" msgid="7281481064509590402">"Tidak ditanda namakan"</string>
+    <string name="no_location" msgid="4043624857489331676">"Tiada lokasi"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Beberapa lokasi tidak dapat dikenal pasti kerana masalah rangkaian."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Tidak dapat memuat turun foto dalam album ini. Cuba lagi nanti."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Imej sahaja"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Video sahaja"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imej &amp; video"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galeri Foto"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Tiada foto."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Imej yang dipangkas disimpan ke <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Tiada album tersedia."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O imej/video yang tersedia."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Siaran"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Jadikan tersedia luar talian"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Segar semula"</string>
+    <string name="done" msgid="217672440064436595">"Selesai"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d dari %2$d item:"</string>
+    <string name="title" msgid="7622928349908052569">"Tajuk"</string>
+    <string name="description" msgid="3016729318096557520">"Perihalan"</string>
+    <string name="time" msgid="1367953006052876956">"Masa"</string>
+    <string name="location" msgid="3432705876921618314">"Lokasi"</string>
+    <string name="path" msgid="4725740395885105824">"Laluan"</string>
+    <string name="width" msgid="9215847239714321097">"Lebar"</string>
+    <string name="height" msgid="3648885449443787772">"Tinggi"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientasi"</string>
+    <string name="duration" msgid="8160058911218541616">"Tempoh"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Jenis MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Saiz fail"</string>
+    <string name="maker" msgid="7921835498034236197">"Pembuat"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Bukaan"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Jarak Fokus"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Imbangan putih"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Masa dedahan"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Denyar dilepas"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Tiada denyar"</string>
+    <string name="unknown" msgid="3506693015896912952">"Tak diketahui"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Asli"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintaj"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Semerta"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Peluntur"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Biru"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"H &amp; P"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Proses X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Menjadikan album tersedia di luar talian."</item>
+    <item quantity="other" msgid="4948604338155959389">"Menjadikan album tersedia di luar talian."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Item ini disimpan pada peranti dan tersedia di luar talian."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Semua album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Album setempat"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Peranti MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Album Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> percuma"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> atau kurang"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> atau kurang"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> hingga <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Import"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Import selesai"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Import tidak berjaya"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera disambungkan."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera diputuskan sambungan."</string>
+    <string name="click_import" msgid="6407959065464291972">"Sentuh di sini untuk mengimport"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Pilih album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Rombak semua imej"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Pilih imej"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Pilih imej"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Tayangan slaid"</string>
+    <string name="albums" msgid="7320787705180057947">"Album"</string>
+    <string name="times" msgid="2023033894889499219">"Masa"</string>
+    <string name="locations" msgid="6649297994083130305">"Lokasi"</string>
+    <string name="people" msgid="4114003823747292747">"Orang"</string>
+    <string name="tags" msgid="5539648765482935955">"Tanda nama"</string>
+    <string name="group_by" msgid="4308299657902209357">"Kumpulkan mengikut"</string>
+    <string name="settings" msgid="1534847740615665736">"Tetapan"</string>
+    <string name="add_account" msgid="4271217504968243974">"Tambah akaun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Muat Turun"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Foto Dalam Talian yang Diedit"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Diimport"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Tangkapan skrin"</string>
+    <string name="help" msgid="7368960711153618354">"Bantuan"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Tiada Storan"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Tiada storan luar tersedia"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Pandangan jalur filem"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Paparan grid"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Pandangan skrin penuh"</string>
+    <string name="trimming" msgid="9122385768369143997">"Mencantas"</string>
+    <string name="muting" msgid="5094925919589915324">"Meredam"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Sila tunggu"</string>
+    <string name="save_into" msgid="9155488424829609229">"Menyimpan video ke <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Tidak boleh mencantas: video sasaran terlalu pendek"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Menghasilkan panorama"</string>
+    <string name="save" msgid="613976532235060516">"Simpan"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Mengimbas kandungan..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d item diimbas"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d item diimbas"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d item diimbas"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Mengisih..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Pengimbasan selesai"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Mengimport..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Tiada kandungan tersedia untuk diimport pada peranti ini."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Tiada peranti MTP disambungkan"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Ralat kamera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Tidak boleh menyambung kepada kamera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kamera telah dilumpuhkan kerana dasar keselamatan."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Kamkorder"</string>
+    <string name="wait" msgid="8600187532323801552">"Sila tunggu..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Lekapkan storan USB sebelum menggunakan kamera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Masukkan kad SD sebelum menggunakan kamera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Menyediakan storan USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Menyediakan kad SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Tidak dapat mengakses storan USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Tidak dapat mengakses kad SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"BATAL"</string>
+    <string name="review_ok" msgid="1156261588693116433">"SELESAI"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Rakaman selang masa"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Pilih kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Belakang"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Depan"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Lokasi stor"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Pemasa hitung detik"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 saat"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d saat"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Bip utk htg dtk"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Matikan"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Hidupkan"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Kualiti video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Tinggi"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Rendah"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Selang masa"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Tetapan kamera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Tetapan kamkorder"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Saiz gambar"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"Piksel 8M"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M piksel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M piksel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M piksel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3M piksel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M piksel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Mod fokus"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infiniti"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Mod denyar"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Auto"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Hidup"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Mati"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Imbangan putih"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Auto"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Berpijar"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Siang hari"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Pendarfluor"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Mendung"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Mod pemandangan"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Auto"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Aksi"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Malam"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Matahari Terbenam"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Parti"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Tidak boleh dipilih dalam mod pemandangan."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Dedahan"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Storan USB anda kehabisan ruang. Tukar tetapan mutu atau padamkan beberapa imej atau fail lain."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Kad SD anda kehabisan ruang. Tukar tetapan mutu atau padamkan beberapa imej atau fail lain."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Had saiz dicapai."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Trlalu cepat"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Menyediakan panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Tidak dapat menyimpan panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Menangkap gambar panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Menunggu panorama sebelumnya"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Menyimpan…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Menghasilkan panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Sentuh untuk memfokus."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Kesan"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Tiada"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Picit"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Mata besar"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Mulut besar"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Mulut kecil"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Hidung besar"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Mata kecil"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Di angkasa"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Matahari Terbenam"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Video anda"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Tetapkan peranti anda ke bawah."\n"Keluar dari pandangan seketika."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Sentuh untuk mengambil gambar semasa merakam."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Rakaman video telah bermula."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Rakaman video telah berhenti."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Petikan video dilumpuhkan apabila kesan khas dihidupkan."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Padamkan kesan"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"MUKA BODOH"</string>
+    <string name="effect_background" msgid="6579360207378171022">"LATAR BELAKANG"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Butang pengatup"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Butang menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Foto paling terbaru"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Suis kamera depan dan belakang"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Pemilih kamera, video atau panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Lagi kawalan tetapan"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Tutup kawalan tetapan"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Kawalan zum"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Pengurangan %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Tingkatkan %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s kotak semak"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Tukar ke foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Tukar kepada video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Tukar kepada panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Beralih ke panorama baharu"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Semakan dibatalkan"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Semakan selesai"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Semak penggambaran semula"</string>
+    <string name="capital_on" msgid="5491353494964003567">"HIDUPKAN"</string>
+    <string name="capital_off" msgid="7231052688467970897">"MATIKAN"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Dimatikan"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minit"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 jam"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 jam"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"saat"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minit"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"jam"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Selesai"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Tetapkan Selang Masa"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Ciri selang masa dimatikan. Hidupkannya untuk menetapkan selang masa."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Pemasa hitung detik dimatikan. Hidupkannya untuk menghitung detik sebelum mengambil gambar."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Tetapkan tempoh dalam saat"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Menghitung detik untuk mengambil gambar"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Ingat lokasi foto?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Teg foto dan video anda dengan lokasi tempat diambil."\n\n"Apl lain boleh mengakses maklumat ini bersama dengan imej disimpan anda."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Tidak, terima kasih"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ya"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Cari"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Foto"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Album"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d foto"</item>
+  </plurals>
+</resources>
diff --git a/res/values-nb/filtershow_strings.xml b/res/values-nb/filtershow_strings.xml
new file mode 100644
index 0000000..82af248
--- /dev/null
+++ b/res/values-nb/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Bilderedigerer"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Kan ikke laste inn bildet."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Angir bakgrunn …"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Kantlinjer"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Angre"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Gjør om"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Vis loggen"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Skjul loggen"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Vis bildetilstanden"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Skjul bildetilstanden"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Innstillinger"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Det er ulagrede endringer i dette bildet."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Vil du lagre før du avslutter?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Lagre og avslutt"</string>
+    <string name="exit" msgid="242642957038770113">"Avslutt"</string>
+    <string name="history" msgid="455767361472692409">"Logg"</string>
+    <string name="reset" msgid="9013181350779592937">"Tilbakestill"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Brukte effekter"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Sammenlign"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Bruk"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Tilbakestill"</string>
+    <string name="aspect" msgid="4025244950820813059">"Størrelsesforhold"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ingen"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Låst"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Liten planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Eksponering"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Skarphet"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Livlig"</string>
+    <string name="saturation" msgid="7026791551032438585">"Fargemetning"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Svart/hvitt-filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autofarger"</string>
+    <string name="hue" msgid="6231252147971086030">"Nyanse"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Skygger"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Lyse punkter"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kurver"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignettering"</string>
+    <string name="redeye" msgid="4508883127049472069">"Røde øyne"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Tegn"</string>
+    <string name="straighten" msgid="26025591664983528">"Rett opp"</string>
+    <string name="crop" msgid="5781263790107850771">"Beskjær"</string>
+    <string name="rotate" msgid="2796802553793795371">"Rotér"</string>
+    <string name="mirror" msgid="5482518108154883096">"Speil"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Ingen"</string>
+    <string name="edge" msgid="7036064886242147551">"Rammer"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Forminske"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rød"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Grønn"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blå"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Størrelse"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Farge"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linjer"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Merkepenn"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Drypp"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Fjern"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Velg egendefinert farge"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Velg farge"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Velg størrelse"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
new file mode 100644
index 0000000..9543f8a
--- /dev/null
+++ b/res/values-nb/strings.xml
@@ -0,0 +1,398 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleri"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Bilderamme"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videospiller"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Laster video…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Laster inn bilde …"</string>
+    <string name="loading_account" msgid="928195413034552034">"Laster inn konto …"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Fortsett avspilling"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Fortsett avspilling fra %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsett avspilling"</string>
+    <string name="loading" msgid="7038208555304563571">"Laster inn ..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Kunne ikke laste"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kunne ikke laste inn bildet"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniatyrbilder"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Begynn på nytt"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Berør et ansikt for å begynne."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Lagrer bilde ..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Lagrer bildet i <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Kan ikke lagre det beskårede bildet."</string>
+    <string name="crop_label" msgid="521114301871349328">"Beskjær bilde"</string>
+    <string name="trim_label" msgid="274203231381209979">"Klipp videoen"</string>
+    <string name="select_image" msgid="7841406150484742140">"Velg bilde"</string>
+    <string name="select_video" msgid="4859510992798615076">"Velg video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Velg element"</string>
+    <string name="select_album" msgid="1557063764849434077">"Velg album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Valg av gruppe"</string>
+    <string name="set_image" msgid="2331476809308010401">"Angi bilde som"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Angi bakgrunnsbilde"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Angir bakgrunn …"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrunnsbilde"</string>
+    <string name="delete" msgid="2839695998251824487">"Slett"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vil du slette det valgte medieelementet?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vil du slette de valgte medieelementene?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bekreft"</string>
+    <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
+    <string name="share" msgid="3619042788254195341">"Del"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Del panoramabilde"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Del som et bilde"</string>
+    <string name="deleted" msgid="6795433049119073871">"Slettet"</string>
+    <string name="undo" msgid="2930873956446586313">"ANGRE"</string>
+    <string name="select_all" msgid="3403283025220282175">"Velg alle"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Opphev alle valg"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Lysbildevisning"</string>
+    <string name="details" msgid="8415120088556445230">"Detaljer"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d av %2$d elementer:"</string>
+    <string name="close" msgid="5585646033158453043">"Lukk"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Bytt til kamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d valgt"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d valgt"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d valgt"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d valgt"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d valgt"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d valgt"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d valgt"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d valgt"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d valgt"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Vis på kartet"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Roter mot venstre"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Roter mot høyre"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Finner ikke elementet."</string>
+    <string name="edit" msgid="1502273844748580847">"Rediger"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Behandler forespørsler om bufring"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Henter …"</string>
+    <string name="crop_action" msgid="3427470284074377001">"Beskjær"</string>
+    <string name="trim_action" msgid="703098114452883524">"Beskjær"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Kutt lyden"</string>
+    <string name="set_as" msgid="3636764710790507868">"Angi som"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Videoens lyd kan ikke kuttes."</string>
+    <string name="video_err" msgid="7003051631792271009">"Kan ikke spille av video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Etter posisjon"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Etter dato"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Etter etiketter"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Etter personer"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Etter album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Etter størrelse"</string>
+    <string name="untagged" msgid="7281481064509590402">"Uten etikett"</string>
+    <string name="no_location" msgid="4043624857489331676">"Ingen posisjon"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Noen steder kunne ikke identifiseres på grunn av nettverksproblemer."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Kunne ikke laste ned bildene i dette albumet. Prøv på nytt senere."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Kun bilder"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Kun videoer"</string>
+    <string name="show_all" msgid="6963292714584735149">"Bilder og videoer"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalleri"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Ingen bilder."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Det beskårede bildet er lagret i <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Ingen album tilgjengelig."</string>
+    <string name="empty_album" msgid="4542880442593595494">"0 bilder/videoer tilgjengelig."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Innlegg"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Gjør tilgjengelig i frakoblet modus"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Last inn på nytt"</string>
+    <string name="done" msgid="217672440064436595">"Ferdig"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d av %2$d elementer:"</string>
+    <string name="title" msgid="7622928349908052569">"Tittel"</string>
+    <string name="description" msgid="3016729318096557520">"Beskrivelse"</string>
+    <string name="time" msgid="1367953006052876956">"Tid"</string>
+    <string name="location" msgid="3432705876921618314">"Sted"</string>
+    <string name="path" msgid="4725740395885105824">"Bane"</string>
+    <string name="width" msgid="9215847239714321097">"Bredde"</string>
+    <string name="height" msgid="3648885449443787772">"Høyde"</string>
+    <string name="orientation" msgid="4958327983165245513">"Retning"</string>
+    <string name="duration" msgid="8160058911218541616">"Varighet"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-type"</string>
+    <string name="file_size" msgid="8486169301588318915">"Filstørrelse"</string>
+    <string name="maker" msgid="7921835498034236197">"Skaper"</string>
+    <string name="model" msgid="8240207064064337366">"Modell"</string>
+    <string name="flash" msgid="2816779031261147723">"Blits"</string>
+    <string name="aperture" msgid="5920657630303915195">"Blender"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Brennvidde"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Hvitbalanse"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Eksponeringstid"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manuelt"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Blits brukes"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Uten blits"</string>
+    <string name="unknown" msgid="3506693015896912952">"Ukjent"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Polaroid"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleket"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blå"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Svart/hvitt"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X-prosess"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografi"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Gjør album tilgjengelig i frakoblet modus."</item>
+    <item quantity="other" msgid="4948604338155959389">"Gjør album tilgjengelige i frakoblet modus."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dette elementet lagres lokalt og er tilgjengelig i frakoblet modus."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Alle album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Lokale album"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-enheter"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa-album"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> tilgjengelig"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> eller mindre"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> eller mer"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> til <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importér"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importen er fullført"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Import mislyktes"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera tilkoblet."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera frakoblet."</string>
+    <string name="click_import" msgid="6407959065464291972">"Trykk her for å importere"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Velg et album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Stokk om på bildene"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Velg et bilde"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Valg av bilder"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Lysbildefremvisning"</string>
+    <string name="albums" msgid="7320787705180057947">"Album"</string>
+    <string name="times" msgid="2023033894889499219">"Tidspunkt"</string>
+    <string name="locations" msgid="6649297994083130305">"Steder"</string>
+    <string name="people" msgid="4114003823747292747">"Folk"</string>
+    <string name="tags" msgid="5539648765482935955">"Etiketter"</string>
+    <string name="group_by" msgid="4308299657902209357">"Gruppér etter"</string>
+    <string name="settings" msgid="1534847740615665736">"Innstillinger"</string>
+    <string name="add_account" msgid="4271217504968243974">"Legg til konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Nedlasting"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Redigerte bilder på nettet"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importert"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skjermdump"</string>
+    <string name="help" msgid="7368960711153618354">"Brukerstøtte"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Ingen lagring"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Ekstern lagringsplass ikke tilgjengelig"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmstripevisning"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Rutenettvisning"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Fullskjermvisning"</string>
+    <string name="trimming" msgid="9122385768369143997">"Klipping"</string>
+    <string name="muting" msgid="5094925919589915324">"Lyden er kuttet"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Et øyeblikk"</string>
+    <string name="save_into" msgid="9155488424829609229">"Lagrer videoen i <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Videoen kan ikke klippes – målvideoen er for kort"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panoramaet settes sammen"</string>
+    <string name="save" msgid="613976532235060516">"Lagre"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Skanner innhold ..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elementer ble skannet"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d element ble skannet"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d elementer ble skannet"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sorterer ..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skanning fullført"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importerer ..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Det er ikke noe innhold for import på denne enheten."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Ingen MTP-enhet tilkoblet"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kamerafeil"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Kan ikke koble til kameraet."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kameraet har blitt deaktivert på grunn av retningslinjer for sikkerhet."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Video"</string>
+    <string name="wait" msgid="8600187532323801552">"Vent litt…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Sett inn en USB-lagring før du bruker kameraet."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Sett inn et SD-kort før du bruker kameraet."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Forbereder USB-lagring …"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Forbereder minnekort…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Får ikke tilgang til USB-lagring."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Får ikke tilgang til SD-kort."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"Avbryt"</string>
+    <string name="review_ok" msgid="1156261588693116433">"FERDIG"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Tidsforkortelsesopptak"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Velg kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Bakside"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Forside"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Lagre sted i bilder"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Nedtellingstidtaker"</string>
+    <!-- String.format failed for translation -->
+    <!-- no translation found for pref_camera_timer_entry:other (6455381617076792481) -->
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Lydsignaler under nedtellingen"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Av"</string>
+    <string name="setting_on" msgid="8602246224465348901">"På"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videokvalitet"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Høy"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Lav"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Intervallmodus (time lapse)"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kamerainnstillinger"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Videoinnstillinger"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Bildestørrelse"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapiksler"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapiksler"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapiksler"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapiksler"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapiksler"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapiksel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fokusmodus"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Autofokus"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Uendelig"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Blitzmodus"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatisk"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"På"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Av"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Hvitbalanse"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatisk"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Flamme"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Dagslys"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Lysstoffrør"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Overskyet"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Velg scenemodus"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatisk"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Action"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Natt"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Solnedgang"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Fest"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Kan ikke velges i scenemodus."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Eksponering"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USB-lagringsplass er snart oppbrukt. Endre kvalitetsinnstillingene, eller slett bilder eller andre filer."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Lagringsplassen på SD-kortet er snart oppbrukt. Endre kvalitetsinnstillingene, eller slett bilder eller andre filer."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Videoen ble for stor."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"For rask"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Forbereder panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Kunne ikke lagre panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Panoramaopptak"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Venter på forrige panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Lagrer …"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panoramaet settes sammen"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Trykk for å fokusere."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effekter"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ingen"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Klem sammen"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Store øyne"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Stor munn"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Liten munn"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Stor nese"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Små øyne"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Verdensrommet"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Solnedgang"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Din video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Sett enheten ned."\n"Gå ut av syne et øyeblikk."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Trykk for å ta et bilde mens du filmer."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Videoopptaket har startet."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Videoopptaket har stoppet."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Øyeblikksbilder er deaktivert når spesialeffekter er aktivert."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Fjern effekter"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"MORSOMME ANSIKTER"</string>
+    <string name="effect_background" msgid="6579360207378171022">"BAKGRUNN"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Lukkerknapp"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Meny-knapp"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Nyeste bilde"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Bryter for foran/bak"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Valg av kamera, video eller panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Flere innstillingskontroller"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Lukk innstillingskontrollene"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoomkontroll"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Reduser %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Øk %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s-avmerkingsboks"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Bytt til foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Bytt til video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Bytt til panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Bytt til ny panoramamodus"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Avbryt redigering"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Redigering fullført"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Ta nytt bilde/video"</string>
+    <string name="capital_on" msgid="5491353494964003567">"PÅ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"AV"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Av"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minutt"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutter"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 time"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 timer"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 timer"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekunder"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutter"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"timer"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Ferdig"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Fast tidsintervall"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Funksjonen for intervallmodus er avslått. Slå den på for å angi et tidsintervall."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Nedtellingstidtaker er slått av. Slå den på for å telle ned før du tar et bilde."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Still varighet i sekunder"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Teller ned før bildet tas"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Vil du huske bildestedene?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Merk bildene og videoene med hvor de ble tatt."\n\n"Andre apper kan bruke denne informasjonen med de lagrede bildene dine."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nei takk"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ja"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Søk"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Bilder"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumer"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d bilde"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d bilder"</item>
+  </plurals>
+</resources>
diff --git a/res/values-nl/filtershow_strings.xml b/res/values-nl/filtershow_strings.xml
new file mode 100644
index 0000000..4284f25
--- /dev/null
+++ b/res/values-nl/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Foto-editor"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Kan de afbeelding niet laden."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Achtergrond instellen"</string>
+    <string name="original" msgid="3524493791230430897">"Origineel"</string>
+    <string name="borders" msgid="2067345080568684614">"Randen"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Ongedaan maken"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Opnieuw"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Geschiedenis weerg."</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Geschiedenis verb."</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Afb.status weergeven"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Afb.status verbergen"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Instellingen"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Er zijn niet-opgeslagen wijzigingen aangebracht in deze afbeelding."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Wilt u opslaan voordat u afsluit?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Opslaan en sluiten"</string>
+    <string name="exit" msgid="242642957038770113">"Sluiten"</string>
+    <string name="history" msgid="455767361472692409">"Geschiedenis"</string>
+    <string name="reset" msgid="9013181350779592937">"Opnieuw instellen"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Toegepaste effecten"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Vergelijken"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Toepassen"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Opnieuw instellen"</string>
+    <string name="aspect" msgid="4025244950820813059">"Beeldverhouding"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Geen"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Vast"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Belichting"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Scherpte"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Levendigheid"</string>
+    <string name="saturation" msgid="7026791551032438585">"Verzadiging"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Z/W-filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Auto-kleur"</string>
+    <string name="hue" msgid="6231252147971086030">"Kleurschakering"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Schaduw"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Accenten"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curven"</string>
+    <string name="vignette" msgid="934721068851885390">"Vervloeien"</string>
+    <string name="redeye" msgid="4508883127049472069">"Rode ogen"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Tekenen"</string>
+    <string name="straighten" msgid="26025591664983528">"Recht maken"</string>
+    <string name="crop" msgid="5781263790107850771">"Bijsnijden"</string>
+    <string name="rotate" msgid="2796802553793795371">"Draaien"</string>
+    <string name="mirror" msgid="5482518108154883096">"Spiegelen"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatief"</string>
+    <string name="none" msgid="6633966646410296520">"Geen"</string>
+    <string name="edge" msgid="7036064886242147551">"Randen"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Downsample"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rood"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Groen"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blauw"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stijl"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Grootte"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Kleur"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Lijnen"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Markeerstift"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Spetters"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Wissen"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Aangepaste kleur kiezen"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Kleur selecteren"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Formaat selecteren"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
new file mode 100644
index 0000000..b9b0544
--- /dev/null
+++ b/res/values-nl/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerij"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Fotolijstje"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videospeler"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Video laden..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Afbeelding laden..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Account laden…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Video hervatten"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Afspelen hervatten vanaf %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Afspelen hervatten"</string>
+    <string name="loading" msgid="7038208555304563571">"Laden..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Kan niet laden"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kan de afbeelding niet laden"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Geen miniatuur"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Opnieuw starten"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Raak een gezicht aan om te beginnen."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Foto opslaan..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Foto opslaan in <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Kan bijgesneden afbeelding niet opslaan."</string>
+    <string name="crop_label" msgid="521114301871349328">"Foto bijsnijden"</string>
+    <string name="trim_label" msgid="274203231381209979">"Video bijsnijden"</string>
+    <string name="select_image" msgid="7841406150484742140">"Foto selecteren"</string>
+    <string name="select_video" msgid="4859510992798615076">"Video selecteren"</string>
+    <string name="select_item" msgid="2816923896202086390">"Item selecteren"</string>
+    <string name="select_album" msgid="1557063764849434077">"Album selecteren"</string>
+    <string name="select_group" msgid="6744208543323307114">"Groep selecteren"</string>
+    <string name="set_image" msgid="2331476809308010401">"Foto instellen als"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Achtergrond inst."</string>
+    <string name="wallpaper" msgid="140165383777262070">"Achtergrond instellen..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Achtergrond"</string>
+    <string name="delete" msgid="2839695998251824487">"Verwijderen"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Geselecteerd item verwijderen?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Geselect. items verwijderen?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bevestigen"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annuleren"</string>
+    <string name="share" msgid="3619042788254195341">"Delen"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Panorama delen"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Delen als foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Verwijderd"</string>
+    <string name="undo" msgid="2930873956446586313">"ONGEDAAN MAKEN"</string>
+    <string name="select_all" msgid="3403283025220282175">"Alles selecteren"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Alle selecties opheffen"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diavoorstelling"</string>
+    <string name="details" msgid="8415120088556445230">"Details"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d van %2$d items:"</string>
+    <string name="close" msgid="5585646033158453043">"Sluiten"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Overschakelen naar Camera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d geselecteerd"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d geselecteerd"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d geselecteerd"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d geselecteerd"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d geselecteerd"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d geselecteerd"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d geselecteerd"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d geselecteerd"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d geselecteerd"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Op kaart weergeven"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Linksom draaien"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rechtsom draaien"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Kan item niet vinden."</string>
+    <string name="edit" msgid="1502273844748580847">"Bewerken"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Cacheverzoeken verwerken"</string>
+    <string name="caching_label" msgid="4521059045896269095">"In cache opslaan..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Bijsnijden"</string>
+    <string name="trim_action" msgid="703098114452883524">"Bijsnijden"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Dempen"</string>
+    <string name="set_as" msgid="3636764710790507868">"Instellen als"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Kan video niet dempen."</string>
+    <string name="video_err" msgid="7003051631792271009">"Video kan niet worden afgespeeld."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Op locatie"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Op tijd"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Op labels"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Op personen"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Op album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Op grootte"</string>
+    <string name="untagged" msgid="7281481064509590402">"Geen tags"</string>
+    <string name="no_location" msgid="4043624857489331676">"Geen locatie"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Sommige locaties kunnen wegens problemen met het netwerk niet worden geïdentificeerd."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Kan de foto\'s in dit album niet downloaden. Probeer het later opnieuw."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Alleen afbeeldingen"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Alleen video\'s"</string>
+    <string name="show_all" msgid="6963292714584735149">"Afbeeldingen en video\'s"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerij"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Geen foto\'s."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Bijgesneden afbeelding is opgeslagen in <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Geen albums beschikbaar."</string>
+    <string name="empty_album" msgid="4542880442593595494">"0 afbeeldingen/video\'s beschikbaar."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Berichten"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Offline beschikbaar maken"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Vernieuwen"</string>
+    <string name="done" msgid="217672440064436595">"Gereed"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d van %2$d items:"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="description" msgid="3016729318096557520">"Beschrijving"</string>
+    <string name="time" msgid="1367953006052876956">"Tijd"</string>
+    <string name="location" msgid="3432705876921618314">"Locatie"</string>
+    <string name="path" msgid="4725740395885105824">"Pad"</string>
+    <string name="width" msgid="9215847239714321097">"Breedte"</string>
+    <string name="height" msgid="3648885449443787772">"Hoogte"</string>
+    <string name="orientation" msgid="4958327983165245513">"Stand"</string>
+    <string name="duration" msgid="8160058911218541616">"Duur"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-type"</string>
+    <string name="file_size" msgid="8486169301588318915">"Bestandsgrootte"</string>
+    <string name="maker" msgid="7921835498034236197">"Maker"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flits"</string>
+    <string name="aperture" msgid="5920657630303915195">"Diafragma"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Brandpuntsafst."</string>
+    <string name="white_balance" msgid="1582509289994216078">"Witbalans"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Belichtingstijd"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Handmatig"</string>
+    <string name="auto" msgid="4296941368722892821">"Autom."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Geflitst"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Geen flits"</string>
+    <string name="unknown" msgid="3506693015896912952">"Onbekend"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Origineel"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleek"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blauw"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Z/W"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X-proces"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Album offline beschikbaar maken."</item>
+    <item quantity="other" msgid="4948604338155959389">"Albums offline beschikbaar maken."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dit item is lokaal opgeslagen en offline beschikbaar."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Alle albums"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Lokale albums"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-apparaten"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa-albums"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> vrij"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> of kleiner"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> of groter"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> tot <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importeren"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importeren voltooid"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Importeren mislukt"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Camera aangesloten."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Camera losgekoppeld."</string>
+    <string name="click_import" msgid="6407959065464291972">"Raak dit aan om te importeren"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Een album selecteren"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Alle afbeeldingen verwisselen"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Een afbeelding kiezen"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Afbeeldingen kiezen"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Diavoorstelling"</string>
+    <string name="albums" msgid="7320787705180057947">"Albums"</string>
+    <string name="times" msgid="2023033894889499219">"Opnametijden"</string>
+    <string name="locations" msgid="6649297994083130305">"Locaties"</string>
+    <string name="people" msgid="4114003823747292747">"Mensen"</string>
+    <string name="tags" msgid="5539648765482935955">"Labels"</string>
+    <string name="group_by" msgid="4308299657902209357">"Groeperen op"</string>
+    <string name="settings" msgid="1534847740615665736">"Instellingen"</string>
+    <string name="add_account" msgid="4271217504968243974">"Account toevoegen"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Camera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Downloaden"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Bewerkte online foto\'s"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Geïmporteerd"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Schermafbeelding"</string>
+    <string name="help" msgid="7368960711153618354">"Help"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Geen opslag"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Geen externe opslag beschikbaar"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmstrookweergave"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Rasterweergave"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Op volledig scherm"</string>
+    <string name="trimming" msgid="9122385768369143997">"Bijsnijden"</string>
+    <string name="muting" msgid="5094925919589915324">"Dempen"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Een ogenblik geduld"</string>
+    <string name="save_into" msgid="9155488424829609229">"Video opslaan in <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Kan niet bijsnijden: doelvideo is te kort"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panorama wordt gegenereerd"</string>
+    <string name="save" msgid="613976532235060516">"Opslaan"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Inhoud scannen..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d items gescand"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d item gescand"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d items gescand"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sorteren..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Scannen gereed"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importeren..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Er is geen inhoud om te importeren naar dit apparaat."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Er is geen MTP-apparaat verbonden"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Camerafout"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Kan geen verbinding maken met de camera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"De camera is uitgeschakeld vanwege het beveiligingsbeleid."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Camera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Camcorder"</string>
+    <string name="wait" msgid="8600187532323801552">"Een ogenblik geduld..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Koppel de USB-opslag voordat u de camera gebruikt."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Plaats een SD-kaart voordat u de camera gebruikt."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB-opslag voorbereiden…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD-kaart voorbereiden…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Geen toegang tot USB-opslag."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Geen toegang tot SD-kaart."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ANNULEREN"</string>
+    <string name="review_ok" msgid="1156261588693116433">"GEREED"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Time-lapse-opname"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Camera selecteren"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Achterzijde"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Voorzijde"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Locatie opslaan"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Afteltimer"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 seconde"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d seconden"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Pieptoon bij aftellen"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Uit"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Aan"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videokwaliteit"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Hoog"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Laag"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Time-lapse"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Camera-instellingen"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Camcorder-instellingen"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Grootte van foto"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapixels"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Scherpstelmodus"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatisch"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Oneindig"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Flitsmodus"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatisch"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Aan"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Uit"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Witbalans"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatisch"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Gloeilamp"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Daglicht"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescerend"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Bewolkt"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scènemodus"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatisch"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Actie"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nacht"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Zonsondergang"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Feest"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Kan niet worden geselecteerd in scènemodus."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Belichting"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Uw USB-opslag is bijna vol. Wijzig de kwaliteitsinstelling of verwijder enkele afbeeldingen of andere bestanden."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Uw SD-kaart is bijna vol. Wijzig de kwaliteitsinstelling of verwijder enkele afbeeldingen of andere bestanden."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Maximale grootte bereikt"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Te snel"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Panorama voorbereiden"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Kan panorama niet opslaan."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Panorama vastleggen"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Wachten op het vorige panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Opslaan…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panorama genereren"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Raak aan voor scherpstellen."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effecten"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Geen"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Samendrukken"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Grote ogen"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Grote mond"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Kleine mond"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Grote neus"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Kleine ogen"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"In de ruimte"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Zonsondergang"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Uw video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Zet uw apparaat neer."\n"Stap even uit beeld."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Raak aan om een foto te maken tijdens een opname."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Video-opname is gestart."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Video-opname is gestopt."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Videosnapshot staat uit als speciale effecten zijn ingeschakeld."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Effecten wissen"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"GEKKE GEZICHTEN"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ACHTERGROND"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Sluiterknop"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menuknop"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Meest recente foto"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Schakelen tussen camera aan voorzijde en aan achterzijde"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Camera-, video- of panoramakiezer"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Meer instelopties"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Instelopties sluiten"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoomregeling"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"%1$s verlagen"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"%1$s verhogen"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s selectievakje"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Overschakelen naar foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Overschakelen naar video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Overschakelen naar panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Overschakelen naar nieuwe panoramamodus"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Beoordeling: annuleren"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Beoordeling: gereed"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Opnieuw maken na beoordeling"</string>
+    <string name="capital_on" msgid="5491353494964003567">"AAN"</string>
+    <string name="capital_off" msgid="7231052688467970897">"UIT"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Uit"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 seconde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 seconden"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuut"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minuten"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 uur"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 uur"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"seconden"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minuten"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"uren"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Gereed"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Tijdsinterval instellen"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Time-lapse-functie is uitgeschakeld. Schakel deze in om het tijdsinterval in te stellen."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Afteltimer is uitgeschakeld. Schakel de timer in om af te tellen vóór het nemen van een foto."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Duur in seconden instellen"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Aftellen om een foto te nemen"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Fotolocaties onthouden?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Label uw foto\'s en video\'s met de locaties waar ze zijn genomen."\n\n"Andere apps hebben toegang tot deze informatie en uw opgeslagen afbeeldingen."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nee, bedankt"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ja"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Camera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Zoeken"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Foto\'s"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albums"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d foto\'s"</item>
+  </plurals>
+</resources>
diff --git a/res/values-notouch-v14/styles.xml b/res/values-notouch-v14/styles.xml
new file mode 100644
index 0000000..1b1b1af
--- /dev/null
+++ b/res/values-notouch-v14/styles.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <style name="Theme.GalleryBase" parent="android:Theme.Holo.NoActionBar.Fullscreen">
+        <item name="listPreferredItemHeightSmall">?android:attr/listPreferredItemHeightSmall</item>
+        <item name="switchStyle">@android:style/Widget.CompoundButton</item>
+    </style>
+    <style name="ActionBarTwoLineItem">
+        <item name="android:background">?android:attr/activatedBackgroundIndicator</item>
+    </style>
+</resources>
diff --git a/res/values-pl/filtershow_strings.xml b/res/values-pl/filtershow_strings.xml
new file mode 100644
index 0000000..3166939
--- /dev/null
+++ b/res/values-pl/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Edytor zdjęć"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Nie można wczytać zdjęcia."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Ustawiam tapetę"</string>
+    <string name="original" msgid="3524493791230430897">"Oryginalny"</string>
+    <string name="borders" msgid="2067345080568684614">"Granice"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Cofnij"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Ponów"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Pokaż historię"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ukryj historię"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Pokaż stan obrazu"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ukryj stan obrazu"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Ustawienia"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Ten obraz zawiera niezapisane zmiany."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Czy chcesz zapisać przed zamknięciem?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Zapisz i zamknij"</string>
+    <string name="exit" msgid="242642957038770113">"Zamknij"</string>
+    <string name="history" msgid="455767361472692409">"Historia"</string>
+    <string name="reset" msgid="9013181350779592937">"Resetuj"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Zastosowane efekty"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Porównaj"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Zastosuj"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Resetuj"</string>
+    <string name="aspect" msgid="4025244950820813059">"Proporcje"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Brak"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Stałe"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Mała planetka"</string>
+    <string name="exposure" msgid="6526397045949374905">"Ekspozycja"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ostrość"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Wibrancja"</string>
+    <string name="saturation" msgid="7026791551032438585">"Nasycenie"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtr cz-b"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autokolor"</string>
+    <string name="hue" msgid="6231252147971086030">"Odcień"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Cienie"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Podświetlenie"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Krzywe"</string>
+    <string name="vignette" msgid="934721068851885390">"Winietowanie"</string>
+    <string name="redeye" msgid="4508883127049472069">"Czerwone oczy"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Rysuj"</string>
+    <string name="straighten" msgid="26025591664983528">"Wyprostuj"</string>
+    <string name="crop" msgid="5781263790107850771">"Przycinanie"</string>
+    <string name="rotate" msgid="2796802553793795371">"Obróć"</string>
+    <string name="mirror" msgid="5482518108154883096">"Odbicie"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatyw"</string>
+    <string name="none" msgid="6633966646410296520">"Brak"</string>
+    <string name="edge" msgid="7036064886242147551">"Krawędzie"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Zmniejsz"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Czerwony"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Zielony"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Niebieski"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Styl"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Rozmiar"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Kolor"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linie"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marker"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Rozprysk"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Wyczyść"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Wybierz własny kolor"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Wybierz kolor"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Wybierz rozmiar"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
new file mode 100644
index 0000000..e2418aa
--- /dev/null
+++ b/res/values-pl/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Ramka ze zdjęciem"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Odtwarzacz wideo"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Ładowanie filmu..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Wczytywanie obrazu…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Wczytywanie konta..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Wznów film"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Wznowić odtwarzanie od %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Wznów odtwarzanie"</string>
+    <string name="loading" msgid="7038208555304563571">"Wczytywanie…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nie można wczytać"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nie można wczytać zdjęcia"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Brak miniatury"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Rozpocznij"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Dotknij twarzy, aby rozpocząć."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Zapisywanie zdjęcia…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Zapisuję obraz w albumie <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Nie można zapisać przyciętego obrazu."</string>
+    <string name="crop_label" msgid="521114301871349328">"Przytnij zdjęcie"</string>
+    <string name="trim_label" msgid="274203231381209979">"Przytnij film"</string>
+    <string name="select_image" msgid="7841406150484742140">"Wybierz zdjęcie"</string>
+    <string name="select_video" msgid="4859510992798615076">"Wybierz film"</string>
+    <string name="select_item" msgid="2816923896202086390">"Wybierz element"</string>
+    <string name="select_album" msgid="1557063764849434077">"Wybierz album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Wybierz grupę"</string>
+    <string name="set_image" msgid="2331476809308010401">"Ustaw zdjęcie jako"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Ustaw tapetę"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Ustawianie tapety…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
+    <string name="delete" msgid="2839695998251824487">"Usuń"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Usunąć wybrany element?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Usunąć wybrane elementy?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potwierdź"</string>
+    <string name="cancel" msgid="3637516880917356226">"Anuluj"</string>
+    <string name="share" msgid="3619042788254195341">"Udostępnij"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Udostępnij panoramę"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Udostępnij zdjęcie"</string>
+    <string name="deleted" msgid="6795433049119073871">"Usunięty"</string>
+    <string name="undo" msgid="2930873956446586313">"COFNIJ"</string>
+    <string name="select_all" msgid="3403283025220282175">"Zaznacz wszystkie"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Usuń zaznaczenie wszystkich"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Pokaz slajdów"</string>
+    <string name="details" msgid="8415120088556445230">"Szczegóły"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d z %2$d elementów:"</string>
+    <string name="close" msgid="5585646033158453043">"Zamknij"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Przełącz na aparat"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Wybrane: %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Wybrane: %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Wybrane: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Wybrane: %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Wybrane: %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Wybrane: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Wybrane: %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Wybrane: %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Wybrane: %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Pokaż na mapie"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Obróć w lewo"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Obróć w prawo"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Nie można znaleźć elementu."</string>
+    <string name="edit" msgid="1502273844748580847">"Edytuj"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Przetwarzanie żądań dotyczących buforowania"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Buforowanie…"</string>
+    <string name="crop_action" msgid="3427470284074377001">"Przytnij"</string>
+    <string name="trim_action" msgid="703098114452883524">"Przytnij"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Wycisz"</string>
+    <string name="set_as" msgid="3636764710790507868">"Ustaw jako"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Nie można wyciszyć filmu."</string>
+    <string name="video_err" msgid="7003051631792271009">"Nie można odtworzyć filmu."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Według lokalizacji"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Według daty"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Według tagów"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Według osób"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Według albumu"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Wg rozmiaru"</string>
+    <string name="untagged" msgid="7281481064509590402">"Nieoznaczone tagami"</string>
+    <string name="no_location" msgid="4043624857489331676">"Brak lokalizacji"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Niektórych lokalizacji nie można zidentyfikować z powodu problemów z siecią."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Nie można pobrać zdjęć z tego albumu. Spróbuj ponownie później."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Tylko obrazy"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Tylko filmy"</string>
+    <string name="show_all" msgid="6963292714584735149">"Obrazy i filmy"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galeria zdjęć"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Brak zdjęć"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Przycięty obraz zapisano w <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Brak dostępnych albumów"</string>
+    <string name="empty_album" msgid="4542880442593595494">"O dostępnych obrazów/filmów"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Posty"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Udostępnij w trybie offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Odśwież"</string>
+    <string name="done" msgid="217672440064436595">"Gotowe"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d z %2$d elementów:"</string>
+    <string name="title" msgid="7622928349908052569">"Tytuł"</string>
+    <string name="description" msgid="3016729318096557520">"Opis"</string>
+    <string name="time" msgid="1367953006052876956">"Godzina"</string>
+    <string name="location" msgid="3432705876921618314">"Lokalizacja"</string>
+    <string name="path" msgid="4725740395885105824">"Ścieżka"</string>
+    <string name="width" msgid="9215847239714321097">"Szerokość"</string>
+    <string name="height" msgid="3648885449443787772">"Wysokość"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientacja"</string>
+    <string name="duration" msgid="8160058911218541616">"Czas trwania"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Typ MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Rozmiar pliku"</string>
+    <string name="maker" msgid="7921835498034236197">"Producent"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Przesłona"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Ogniskowa"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balans bieli"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Czas ekspozycji"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Ręczna"</string>
+    <string name="auto" msgid="4296941368722892821">"Automat."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Z lampą"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Bez lampy"</string>
+    <string name="unknown" msgid="3506693015896912952">"Nieznane"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Oryginalny"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Wybielacz"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Niebieski"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Czarno-biały"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Stempel"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Cross"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Udostępnianie albumu w trybie offline"</item>
+    <item quantity="other" msgid="4948604338155959389">"Udostępnianie albumów w trybie offline"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ten element jest przechowywany lokalnie i dostępny w trybie offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Wszystkie albumy"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Albumy lokalne"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Urządzenia MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Albumy Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> wolne"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> lub mniej"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> lub więcej"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> do <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importuj"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Import zakończony"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Nie udało się zaimportować."</string>
+    <string name="camera_connected" msgid="916021826223448591">"Aparat podłączony"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Aparat odłączony"</string>
+    <string name="click_import" msgid="6407959065464291972">"Dotknij tutaj, aby zaimportować"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Wybierz album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Wszystkie zdjęcia losowo"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Wybierz obraz"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Wybierz obrazy"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Pokaz slajdów"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumy"</string>
+    <string name="times" msgid="2023033894889499219">"Czas"</string>
+    <string name="locations" msgid="6649297994083130305">"Lokalizacje"</string>
+    <string name="people" msgid="4114003823747292747">"Osoby"</string>
+    <string name="tags" msgid="5539648765482935955">"Tagi"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupuj według"</string>
+    <string name="settings" msgid="1534847740615665736">"Ustawienia"</string>
+    <string name="add_account" msgid="4271217504968243974">"Dodaj konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Aparat"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Pobrane"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Edytowane zdjęcia online"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Zaimportowane"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Zrzuty ekranu"</string>
+    <string name="help" msgid="7368960711153618354">"Pomoc"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Brak pamięci"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Brak pamięci zewnętrznej"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Widok taśmy filmowej"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Widok siatki"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Widok pełnoekranowy"</string>
+    <string name="trimming" msgid="9122385768369143997">"Przycinam"</string>
+    <string name="muting" msgid="5094925919589915324">"Wyciszam"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Poczekaj"</string>
+    <string name="save_into" msgid="9155488424829609229">"Zapisuję film w albumie <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Nie można przyciąć: film docelowy jest za krótki"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Renderuję panoramę"</string>
+    <string name="save" msgid="613976532235060516">"Zapisz"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Skanuję zawartość..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d przeskanowanych elementów"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d element przeskanowany"</item>
+    <item quantity="other" msgid="3138021473860555499">"Przeskanowane elementy: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sortuję..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skanowanie ukończone"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importuję..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Brak treści do zaimportowania na tym urządzeniu."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Brak podłączonego urządzenia MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Błąd aparatu"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Nie można nawiązać połączenia z aparatem."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Aparat został wyłączony z powodu zasad bezpieczeństwa."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Aparat"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Kamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Proszę czekać…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Zanim użyjesz aparatu podłącz nośnik USB."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Przed przystąpieniem do korzystania z aparatu włóż kartę SD."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Przygotowywanie nośnika USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Przygotowywanie karty SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nie można uzyskać dostępu do pamięci USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nie można uzyskać dostępu do karty SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ANULUJ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"GOTOWE"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Nagrywanie poklatkowe"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Wybierz aparat"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Tył"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Przód"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Zapis lokalizacji"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Samowyzwalacz"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sekunda"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d s"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Sygnał odliczania"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Wyłączono"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Włączono"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Jakość wideo"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Wysoka"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Niska"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Upływ czasu"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Ustawienia aparatu"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Ustawienia kamery"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Rozmiar zdjęcia"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapikseli"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 Mpix"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 Mpix"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 Mpix"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 Mpix"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 Mpix"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Tryb ostrości"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Nieskończoność"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Lampa błyskowa"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatyczna"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Włączona"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Wyłączona"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balans bieli"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Auto"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Światło żarowe"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Światło dzienne"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescencja"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Zachmurzenie"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Tryb scenerii"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Auto"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Akcja"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Noc"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Zachód słońca"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Impreza"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Opcja niedostępna w trybie scenerii."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Ekspozycja"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Na nośniku USB kończy się miejsce. Zmień ustawienie jakości bądź usuń niektóre zdjęcia lub inne pliki."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Na karcie SD kończy się miejsce. Zmień ustawienie jakości bądź usuń niektóre zdjęcia lub inne pliki."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Osiągnięto limit rozmiaru."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Zbyt szybko"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Przygotowywanie panoramy"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Nie można zapisać panoramy."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Tworzenie panoramy"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Oczekiwanie na poprzednią panoramę"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Zapis..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Renderowanie panoramy"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Ustaw ostrość, klikając."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efekty"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Brak"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Ściśnięcie"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Wielkie oczy"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Wielkie usta"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Małe usta"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Wielki nos"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Małe oczy"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"W kosmosie"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Zachód słońca"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Twój film"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Skieruj urządzenie w dół."\n"Opuść na chwilę kadr."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Dotknij, by zrobić zdjęcie podczas nagrywania."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Rozpoczęto nagrywanie filmu."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Zatrzymano nagrywanie filmu."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Stopklatka nie działa, gdy są aktywne efekty specjalne."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Usuń efekty"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"ZABAWNE TWARZE"</string>
+    <string name="effect_background" msgid="6579360207378171022">"TŁO"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Przycisk migawki"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Przycisk menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Najnowsze zdjęcie"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Przełącznik przedniego i tylnego aparatu"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Wybór aparatu, filmu lub panoramy"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Więcej ustawień"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Zamknij ustawienia"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Sterowanie powiększeniem"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Zmniejsz: %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Zwiększ: %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Pole wyboru %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Przełącz na zdjęcia"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Przełącz na wideo"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Przełącz na panoramę"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Przełącz na nową panoramę"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Przegląd: anulowanie"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Przegląd: gotowe"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Przegląd – zrób zdjęcie/nagraj film ponownie"</string>
+    <string name="capital_on" msgid="5491353494964003567">"WŁĄCZONY"</string>
+    <string name="capital_off" msgid="7231052688467970897">"WYŁĄCZONY"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Wył."</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekunda"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minuty"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 godziny"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 godzina"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 godziny"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 godziny"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 godziny"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 godziny"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 godziny"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 godzin"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 godz."</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 godzin"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 godzin"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 godzin"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 godziny"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sek."</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"min"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"godz."</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Gotowe"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Ustaw interwał czasu"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Funkcja filmu poklatkowego jest wyłączona. Włącz ją, by ustawić interwał czasu."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Samowyzwalacz jest wyłączony. Włącz go, by odliczał czas pozostały do wykonania zdjęcia."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Ustaw czas w sekundach"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Odliczanie czasu pozostałego do zrobienia zdjęcia"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Zapamiętywać lokalizacje zdjęć?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Oznacz zdjęcia i filmy informacją, gdzie zostały zrobione."\n\n"Inne aplikacje mają dostęp do tych informacji wraz z zapisanymi zdjęciami."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nie, dziękuję"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Tak"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Aparat"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Szukaj"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Zdjęcia"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumy"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d zdjęcie"</item>
+    <item quantity="other" msgid="3813306834113858135">"Zdjęcia: %1$d"</item>
+  </plurals>
+</resources>
diff --git a/res/values-port/styles.xml b/res/values-port/styles.xml
new file mode 100644
index 0000000..46871c6
--- /dev/null
+++ b/res/values-port/styles.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android">
+    <style name="ReviewControlIcon">
+        <item name="android:layout_height">@dimen/switcher_size</item>
+        <item name="android:layout_width">@dimen/switcher_size</item>
+        <item name="android:gravity">center</item>
+        <item name="android:layout_centerVertical">true</item>
+        <item name="android:clickable">true</item>
+        <item name="android:focusable">true</item>
+        <item name="android:background">@drawable/bg_pressed</item>
+    </style>
+    <style name="SettingPopupWindow">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_centerHorizontal">true</item>
+        <item name="android:layout_marginBottom">@dimen/setting_popup_right_margin</item>
+        <item name="android:visibility">gone</item>
+    </style>
+    <style name="PopupTitleText">
+        <item name="android:textSize">@dimen/popup_title_text_size</item>
+        <item name="android:layout_gravity">left|center_vertical</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textColor">@color/popup_title_color</item>
+        <item name="android:layout_marginLeft">10dp</item>
+    </style>
+    <style name="ViewfinderLabelLayout">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:layout_marginTop">13dp</item>
+        <item name="android:layout_marginBottom">@dimen/indicator_bar_width</item>
+        <item name="android:layout_marginLeft">13dp</item>
+        <item name="android:layout_marginRight">13dp</item>
+    </style>
+    <style name="PanoViewHorizontalBar">
+        <item name="android:background">#000000</item>
+        <item name="android:alpha">1.0</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">0dp</item>
+        <item name="android:layout_weight">1</item>
+    </style>
+    <style name="SettingPopupWindow_xlarge">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_centerHorizontal">true</item>
+        <item name="android:layout_alignParentBottom">true</item>
+        <item name="android:layout_marginBottom">@dimen/setting_popup_right_margin</item>
+        <item name="android:visibility">gone</item>
+    </style>
+</resources>
diff --git a/res/values-pt-rPT/filtershow_strings.xml b/res/values-pt-rPT/filtershow_strings.xml
new file mode 100644
index 0000000..1f144c0
--- /dev/null
+++ b/res/values-pt-rPT/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor de Fotografias"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Não é possível carregar a imagem!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"A definir imagem de fundo"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Limites"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Anular"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Refazer"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Mostrar Histórico"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ocultar Histórico"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Ver Estado da Imagem"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ocul. Estado Imagem"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Definições"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Existem alterações a esta imagem não guardadas."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Pretende guardar antes de sair?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Guardar e Sair"</string>
+    <string name="exit" msgid="242642957038770113">"Sair"</string>
+    <string name="history" msgid="455767361472692409">"Histórico"</string>
+    <string name="reset" msgid="9013181350779592937">"Repor"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Efeitos Aplicados"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Comparar"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Aplicar"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Repor"</string>
+    <string name="aspect" msgid="4025244950820813059">"Formato"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Nenhum"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fixo"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Planeta Minúsculo"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposição"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Nitidez"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contraste"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vibração"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturação"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtro P&amp;B"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Cor automática"</string>
+    <string name="hue" msgid="6231252147971086030">"Tonalidade"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Sombras"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Destaques"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curvas"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinheta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Olhos Vermelhos"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Desenhar"</string>
+    <string name="straighten" msgid="26025591664983528">"Endireitar"</string>
+    <string name="crop" msgid="5781263790107850771">"Recortar"</string>
+    <string name="rotate" msgid="2796802553793795371">"Rodar"</string>
+    <string name="mirror" msgid="5482518108154883096">"Espelho"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativo"</string>
+    <string name="none" msgid="6633966646410296520">"Nenhum"</string>
+    <string name="edge" msgid="7036064886242147551">"Limites"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Subamostra"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Vermelho"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Verde"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Azul"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Estilo"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Tamanho"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Cor"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linhas"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marcador"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Salpicar"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Limpar"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Selecionar cor personalizada"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Selecionar a cor"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Selecionar Tamanho"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..6bea992
--- /dev/null
+++ b/res/values-pt-rPT/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Moldura da imagem"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Leitor de vídeo"</string>
+    <string name="loading_video" msgid="4013492720121891585">"A carregar vídeo..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"A carregar imagem..."</string>
+    <string name="loading_account" msgid="928195413034552034">"A carregar conta..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Retomar o vídeo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Retomar reprodução a partir de %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string>
+    <string name="loading" msgid="7038208555304563571">"A carregar..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Não foi possível carregar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Não foi possível carregar a imagem"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Sem miniatura"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Recomeçar"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Toque num rosto para começar."</string>
+    <string name="saving_image" msgid="7270334453636349407">"A guardar imagem..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"A guardar imagem em <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Impossível guardar imagem recortada."</string>
+    <string name="crop_label" msgid="521114301871349328">"Recortar imagem"</string>
+    <string name="trim_label" msgid="274203231381209979">"Cortar vídeo"</string>
+    <string name="select_image" msgid="7841406150484742140">"Selecionar fotog."</string>
+    <string name="select_video" msgid="4859510992798615076">"Seleccionar vídeo"</string>
+    <string name="select_item" msgid="2816923896202086390">"Seleccionar item"</string>
+    <string name="select_album" msgid="1557063764849434077">"Selecionar álbum"</string>
+    <string name="select_group" msgid="6744208543323307114">"Selecionar grupo"</string>
+    <string name="set_image" msgid="2331476809308010401">"Definir imagem como"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Definir imagem fundo"</string>
+    <string name="wallpaper" msgid="140165383777262070">"A definir imagem de fundo…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Imagem de fundo"</string>
+    <string name="delete" msgid="2839695998251824487">"Eliminar"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Eliminar item selecionado?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Eliminar itens selecionados?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Partilhar"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Partilhar panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Partilhar como fotografia"</string>
+    <string name="deleted" msgid="6795433049119073871">"Eliminada"</string>
+    <string name="undo" msgid="2930873956446586313">"ANULAR"</string>
+    <string name="select_all" msgid="3403283025220282175">"Selecionar tudo"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Desmarcar tudo"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Apresentação de diapositivos"</string>
+    <string name="details" msgid="8415120088556445230">"Detalhes"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d de %2$d itens:"</string>
+    <string name="close" msgid="5585646033158453043">"Fechar"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Mudar para Câmara"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d selecionado(s)"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d selecionado(s)"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d selecionado(s)"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d selecionado(s)"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d selecionado(s)"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d selecionado(s)"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d selecionado(s)"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d selecionado(s)"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d selecionado(s)"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar no mapa"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Rodar para a esquerda"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rodar para a direita"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Não foi possível encontrar item."</string>
+    <string name="edit" msgid="1502273844748580847">"Editar"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"A processar pedidos de colocação em cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"A col. cache..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Recortar"</string>
+    <string name="trim_action" msgid="703098114452883524">"Recortar"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Desativar som"</string>
+    <string name="set_as" msgid="3636764710790507868">"Definir como"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Impossível silenciar o vídeo."</string>
+    <string name="video_err" msgid="7003051631792271009">"Não é possível reproduzir vídeo."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Por localização"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Por hora"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Por etiquetas"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Por pessoas"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Por tamanho"</string>
+    <string name="untagged" msgid="7281481064509590402">"Sem etiqueta"</string>
+    <string name="no_location" msgid="4043624857489331676">"Sem localização"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Não foi possível identificar algumas localizações devido a problemas de rede."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Não foi possível transferir as fotografias deste álbum. Tente novamente mais tarde."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Apenas imagens"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Apenas vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imagens e vídeos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotografias"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Não existem fotografias."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Imagem recortada guardada em <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nenhum álbum disponível."</string>
+    <string name="empty_album" msgid="4542880442593595494">"0 imagens/vídeos disponíveis."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Mensagens"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Disponibilizar offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Atualizar"</string>
+    <string name="done" msgid="217672440064436595">"Concluído"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d itens:"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="description" msgid="3016729318096557520">"Descrição"</string>
+    <string name="time" msgid="1367953006052876956">"Hora"</string>
+    <string name="location" msgid="3432705876921618314">"Localização"</string>
+    <string name="path" msgid="4725740395885105824">"Caminho"</string>
+    <string name="width" msgid="9215847239714321097">"Largura"</string>
+    <string name="height" msgid="3648885449443787772">"Altura"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientação"</string>
+    <string name="duration" msgid="8160058911218541616">"Duração"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Tipo MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Tam. ficheiro"</string>
+    <string name="maker" msgid="7921835498034236197">"Fabricante"</string>
+    <string name="model" msgid="8240207064064337366">"Modelo"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Abertura"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Dist. focal"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Eq. brancos"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Tempo expos."</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Automático"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash dispar."</string>
+    <string name="flash_off" msgid="1445443413822680010">"Sem flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Desconhecida"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Azul"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"P/B"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Processo X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Disponibilizar álbum offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Disponibilizar álbuns offline."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Este artigo está armazenado localmente e está disponível off-line."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Todos os álbuns"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Álbuns locais"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Aparelhos MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Álbuns Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> livres"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ou abaixo"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ou acima"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> para <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importar"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importação concluída"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Importação falhou"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Câmara ligada."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Câmara desligada."</string>
+    <string name="click_import" msgid="6407959065464291972">"Toque aqui para importar"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Escolher um álbum"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Repr. aleat. todas as imagens"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Escolher uma imagem"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Escolher imagens"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Apres. de diap."</string>
+    <string name="albums" msgid="7320787705180057947">"Álbuns"</string>
+    <string name="times" msgid="2023033894889499219">"Vezes"</string>
+    <string name="locations" msgid="6649297994083130305">"Localizações"</string>
+    <string name="people" msgid="4114003823747292747">"Pessoas"</string>
+    <string name="tags" msgid="5539648765482935955">"Etiquetas"</string>
+    <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
+    <string name="settings" msgid="1534847740615665736">"Definições"</string>
+    <string name="add_account" msgid="4271217504968243974">"Adicionar conta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Câmara"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Transferidas"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Fotografias Online Editadas"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importada"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captura de ecrã"</string>
+    <string name="help" msgid="7368960711153618354">"Ajuda"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nenhum Armazenamento"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nenhum armazenamento externo disponível"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Vista de película"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Vista de grelha"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Vista ecrã inteiro"</string>
+    <string name="trimming" msgid="9122385768369143997">"Recorte"</string>
+    <string name="muting" msgid="5094925919589915324">"A desativar o som"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Aguarde"</string>
+    <string name="save_into" msgid="9155488424829609229">"A guardar o vídeo em <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Não é possível recortar: o vídeo de destino é demasiado pequeno"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"A compor panorama"</string>
+    <string name="save" msgid="613976532235060516">"Guardar"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"A digitalizar o conteúdo..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d itens digitalizados"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d item digitalizado"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d itens digitalizados"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"A ordenar..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Digitalização concluída"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"A importar..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Não existe conteúdo disponível para importar neste dispositivo."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Não estão ligados dispositivos MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Erro da câmara"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Não é possível efetuar ligação à câmara."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Devido a políticas de segurança, a câmara foi desativada."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Câmara"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Câmara de vídeo"</string>
+    <string name="wait" msgid="8600187532323801552">"Aguarde..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Monte a memória de armazenamento USB antes de utilizar a câmara."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Insira um cartão SD antes de utilizar a câmara."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Preparar armazenamento USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"A preparar o cartão SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Não foi possível aceder à memória de armazenamento USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Não foi possível aceder ao cartão SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"CANCELAR"</string>
+    <string name="review_ok" msgid="1156261588693116433">"CONCLUÍDO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Gravação com lapso de tempo"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Escolher câmara"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Traseira"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Frontal"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Armazenar localização"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Temporizador de contagem decr."</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 segundo"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d segundos"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Apitar durante a contagem descrescente"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Desativado"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Ativado"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Qualidade de vídeo"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Alta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Baixa"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Intervalo de tempo"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Definições da câmara"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Definições da câmara de vídeo"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Tamanho da imagem"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 MP"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapíxeis"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapíxeis"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapíxeis"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapíxeis"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapíxel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Modo de focagem"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automático"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinito"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Modo flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automático"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Activado"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Desativado"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Equilíbrio dos brancos"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automático"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescente"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Luz do dia"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescente"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nublado"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Modo cenário"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automático"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Acção"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Nocturno"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Ocaso"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Partido"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Não selecionável no modo de cena."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exposição"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Está a ficar sem espaço no armazenamento USB. Altere as definições de qualidade ou elimine algumas imagens ou outros ficheiros."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Está a ficar sem espaço no cartão SD. Altere as definições de qualidade ou elimine algumas imagens ou outros ficheiros."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Limite de tamanho atingido."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Muito rápido"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"A preparar panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Não foi possível guardar panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"A tirar foto de panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"A aguardar panorama anterior"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"A guardar..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"A compor panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Toque para focar."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efeitos"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Nenhum"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Comprimir"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Olhos grandes"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Boca grande"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Boca pequena"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Nariz grande"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Olhos pequenos"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"No espaço"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Pôr do Sol"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"O seu vídeo"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Vire o seu aparelho para baixo."\n"Saia da vista por alguns momentos."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Toque para tirar uma fotografia durante a gravação."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"A gravação de vídeo foi iniciada."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"A gravação de vídeo foi parada."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Instantâneo vídeo desat. quando efeitos especiais estão ativos."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Limpar efeitos"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"CARETAS"</string>
+    <string name="effect_background" msgid="6579360207378171022">"FUNDO"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Botão Obturador"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Botão de menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Fotografia mais recente"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Interruptor da câmara frontal e posterior"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Seletor de câmara, vídeo ou panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Mais controlos de definições"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Fechar controlos de definições"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Controlo de zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Diminuir %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Aumentar %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Caixa de verificação %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Mudar para fotografia"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Mudar para vídeo"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Mudar para panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Mudar para o novo panorama"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Comentário cancelado."</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Reveja o que está concluído"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Retomar comentário"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ATIVADA"</string>
+    <string name="capital_off" msgid="7231052688467970897">"DESATIVADA"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Desativado"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hora"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 horas"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"segundos"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutos"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"horas"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Concluído"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Definir Intervalo de Tempo"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"A funcionalidade de intervalo de tempo está desativada. Ative-a para definir o intervalo de tempo."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"O temporizador de contagem decrescente está desativado. Volte a ativá-lo para efetuar a contagem antes de tirar uma fotografia."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Definir a duração em segundos"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Contagem decrescente para tirar uma fotografia"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Memorizar localizações das fotografias?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Insira etiquetas nas suas fotografias e vídeos com as localizações onde foram capturados."\n\n"Outras aplicações podem aceder a estas informações juntamente com as suas imagens guardadas."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Não, obrigado"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Sim"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Câmara"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Pesquisa"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotografias"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Álbuns"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotografia"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotog."</item>
+  </plurals>
+</resources>
diff --git a/res/values-pt/filtershow_strings.xml b/res/values-pt/filtershow_strings.xml
new file mode 100644
index 0000000..bce7944
--- /dev/null
+++ b/res/values-pt/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor de fotos"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Não é possível carregar a imagem!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Definindo plano de fundo"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Bordas"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Desfazer"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Refazer"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Mostrar histórico"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ocultar histórico"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Mostrar estado"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ocultar estado"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Configurações"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Existem alterações não salvas nesta imagem."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Deseja salvar antes de sair?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Salvar e sair"</string>
+    <string name="exit" msgid="242642957038770113">"Sair"</string>
+    <string name="history" msgid="455767361472692409">"Histórico"</string>
+    <string name="reset" msgid="9013181350779592937">"Restaurar"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Efeitos aplicados"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Comparar"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Aplicar"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Restaurar"</string>
+    <string name="aspect" msgid="4025244950820813059">"Proporção"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Nenhuma"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fixo"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Planetinha"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposição"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Nitidez"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contraste"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vibração"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturação"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtro P&amp;B"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Cor automática"</string>
+    <string name="hue" msgid="6231252147971086030">"Matiz"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Sombras"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Destaques"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curvas"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinheta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Olhos vermelhos"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Desenhar"</string>
+    <string name="straighten" msgid="26025591664983528">"Endireitar"</string>
+    <string name="crop" msgid="5781263790107850771">"Cortar"</string>
+    <string name="rotate" msgid="2796802553793795371">"Girar"</string>
+    <string name="mirror" msgid="5482518108154883096">"Espelhar"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativo"</string>
+    <string name="none" msgid="6633966646410296520">"Nenhum"</string>
+    <string name="edge" msgid="7036064886242147551">"Bordas"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Diminuir"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Vermelho"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Verde"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Azul"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Estilo"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Tamanho"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Cor"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linhas"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marcador"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Respingo"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Limpar"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Escolha uma cor personalizada"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Selecionar cor"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Selecionar tamanho"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
new file mode 100644
index 0000000..27b418f
--- /dev/null
+++ b/res/values-pt/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Moldura de uma imagem"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Player de vídeo"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Carregando vídeo..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Carregando imagem…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Carregando conta..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Retomar vídeo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Retomar reprodução de %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string>
+    <string name="loading" msgid="7038208555304563571">"Carregando..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Não foi possível carregar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Não foi possível carregar a imagem"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Sem miniatura"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Reiniciar"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Toque em um rosto para começar."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Salvando imagem…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Salvando a imagem em <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Não é possível salvar a imagem cortada."</string>
+    <string name="crop_label" msgid="521114301871349328">"Cortar imagem"</string>
+    <string name="trim_label" msgid="274203231381209979">"Cortar vídeo"</string>
+    <string name="select_image" msgid="7841406150484742140">"Selecionar foto"</string>
+    <string name="select_video" msgid="4859510992798615076">"Selecionar vídeo"</string>
+    <string name="select_item" msgid="2816923896202086390">"Selecionar item"</string>
+    <string name="select_album" msgid="1557063764849434077">"Selecionar álbum"</string>
+    <string name="select_group" msgid="6744208543323307114">"Selecionar grupo"</string>
+    <string name="set_image" msgid="2331476809308010401">"Definir imagem como"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Definir plano de fundo"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Definindo plano de fundo..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Plano de fundo"</string>
+    <string name="delete" msgid="2839695998251824487">"Excluir"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Excluir o item selecionado?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Excluir os itens selecionados?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Compartilhar"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Compartilhar panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Compartilhar como foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Excluída"</string>
+    <string name="undo" msgid="2930873956446586313">"DESFAZER"</string>
+    <string name="select_all" msgid="3403283025220282175">"Selecionar tudo"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Desmarcar tudo"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Apresentação de slides"</string>
+    <string name="details" msgid="8415120088556445230">"Detalhes"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d de %2$d itens:"</string>
+    <string name="close" msgid="5585646033158453043">"Fechar"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Alternar para câmera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d selecionado(s)"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d selecionado(s)"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d selecionado(s)"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d selecionado(s)"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d selecionado(s)"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d selecionado(s)"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d selecionado(s)"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d selecionado(s)"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d selecionado(s)"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar no mapa"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Girar para a esquerda"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Girar para a direita"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Não foi possível encontrar o item."</string>
+    <string name="edit" msgid="1502273844748580847">"Editar"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Processando solicitações de armazenamento em cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Cache..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Cortar"</string>
+    <string name="trim_action" msgid="703098114452883524">"Cortar"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Desativar som"</string>
+    <string name="set_as" msgid="3636764710790507868">"Definir como"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Impossível silenciar o vídeo."</string>
+    <string name="video_err" msgid="7003051631792271009">"Não é possível reproduzir o vídeo."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Por local"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Por tempo"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Por tags"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Por pessoas"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Por tamanho"</string>
+    <string name="untagged" msgid="7281481064509590402">"Sem tags"</string>
+    <string name="no_location" msgid="4043624857489331676">"Nenhum local"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Alguns locais não foram identificados por problemas na rede."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Não foi possível fazer download das fotos neste álbum. Tente novamente mais tarde."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Apenas imagens"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Somente vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imagens e vídeos"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotos"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Nenhuma foto."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"A imagem cortada foi salva em <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nenhum álbum disponível."</string>
+    <string name="empty_album" msgid="4542880442593595494">"0 imagens/vídeos disponíveis."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Postagens"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Tornar disponível off-line"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Atualizar"</string>
+    <string name="done" msgid="217672440064436595">"Concluído"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d itens:"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="description" msgid="3016729318096557520">"Descrição"</string>
+    <string name="time" msgid="1367953006052876956">"Horário"</string>
+    <string name="location" msgid="3432705876921618314">"Local"</string>
+    <string name="path" msgid="4725740395885105824">"Caminho"</string>
+    <string name="width" msgid="9215847239714321097">"Largura"</string>
+    <string name="height" msgid="3648885449443787772">"Altura"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientação"</string>
+    <string name="duration" msgid="8160058911218541616">"Duração"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Tipo MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Tam. arquivo"</string>
+    <string name="maker" msgid="7921835498034236197">"Criador"</string>
+    <string name="model" msgid="8240207064064337366">"Modelo"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Abertura"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Comprimento focal"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balanç. branco"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Temp. exposiç."</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash ativo"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Sem flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Desconhecido"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant."</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Branqueamento"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Azul"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"P&amp;B"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Proc. cruzado"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Disponibilizando álbum off-line."</item>
+    <item quantity="other" msgid="4948604338155959389">"Disponibilizando álbuns off-line."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Este item está armazenado localmente e disponível off-line."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Todos os álbuns"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Álbuns locais"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Dispositivos MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Álbuns do Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> gratuito"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ou menos"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ou mais"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> para <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importar"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importação concluída"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Falha na importação"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Câmera conectada."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Câmera desconectada."</string>
+    <string name="click_import" msgid="6407959065464291972">"Toque aqui para importar"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Escolher um álbum"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Reprod. aleator. as imagens"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Selecionar uma imagem"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Escolher imagens"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Apresent. de slides"</string>
+    <string name="albums" msgid="7320787705180057947">"Álbuns"</string>
+    <string name="times" msgid="2023033894889499219">"Data e hora"</string>
+    <string name="locations" msgid="6649297994083130305">"Locais"</string>
+    <string name="people" msgid="4114003823747292747">"Pessoas"</string>
+    <string name="tags" msgid="5539648765482935955">"Etiquetas"</string>
+    <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
+    <string name="settings" msgid="1534847740615665736">"Configurações"</string>
+    <string name="add_account" msgid="4271217504968243974">"Adicionar conta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Câmera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Downloads"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Fotos on-line editadas"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importado"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Capturas de tela"</string>
+    <string name="help" msgid="7368960711153618354">"Ajuda"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nenhum armazenamento"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nenhum armazenamento externo disponível"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Visualização de película"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Visualização de grade"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Visual. tela inteira"</string>
+    <string name="trimming" msgid="9122385768369143997">"Cortando"</string>
+    <string name="muting" msgid="5094925919589915324">"Desativando som"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Aguarde"</string>
+    <string name="save_into" msgid="9155488424829609229">"Salvando vídeo em <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Impossível cortar: o vídeo de destino é curto demais"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Renderizando panorama"</string>
+    <string name="save" msgid="613976532235060516">"Salvar"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Verificando conteúdo..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d itens verificados"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d item verificado"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d itens verificados"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Classificando..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Verificação concluída"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importando..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Nenhum conteúdo disponível para importação no dispositivo."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nenhum dispositivo MTP conectado"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Erro de câmera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Não é possível conectar à câmera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"A câmera foi desativada devido às políticas de segurança."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Câmera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Filmadora"</string>
+    <string name="wait" msgid="8600187532323801552">"Aguarde..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Monte o armazenamento USB antes de usar a câmera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Insira um cartão SD antes de usar a câmera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Preparando armazenamento USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Preparando o cartão SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Não foi possível acessar o armazenamento USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Não foi possível acessar o cartão SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"CANCELAR"</string>
+    <string name="review_ok" msgid="1156261588693116433">"CONCLUÍDO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Gravação de lapso de tempo"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Escolher câmera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Traseira"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Visão frontal"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Local de armazenamento"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Contagem regressiva"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"Um segundo"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d segundos"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Aviso sonoro"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Desativado"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Ativado"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Qualidade do vídeo"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Alta"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Baixa"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Passagem de tempo"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Configurações da câmera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Configurações da filmadora"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Tamanho da imagem"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 MP"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapixels"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapixels"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapixels"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapixels"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Modo de foco"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automático"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinito"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Modo de flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automático"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Ativado"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Desativado"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balanço de branco"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automático"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescente"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Luz do dia"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescente"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nublado"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Modo de cena"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automático"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Ação"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Cena noturna"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Pôr-do-sol"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Festa"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Não pode ser selecionado no modo de cena."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exposição"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Seu armazenamento USB está sem espaço. Altere a configuração de qualidade ou exclua algumas imagens ou outros arquivos."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Seu cartão SD está sem espaço. Altere a configuração de qualidade ou exclua algumas imagens ou outros arquivos."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Limite de tamanho atingido."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Muito rápido"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Preparando panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Não foi possível salvar panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Capturando panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Aguardando panorama anterior"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Salvando..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Renderizando panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Toque para ajustar o foco."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efeitos"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Nenhum"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Comprimir"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Olhos grandes"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Boca grande"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Boca pequena"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Nariz grande"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Olhos pequenos"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"No espaço"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Pôr-do-sol"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Seu vídeo"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Abaixe seu dispositivo."\n"Saia de vista por um momento."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Toque para tirar uma foto durante a gravação."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"A gravação de vídeo foi iniciada."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"A gravação de vídeo foi interrompida."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"O instant. de vídeo é desat. quando os efeitos esp. estão ativad."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Clarear efeitos"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"CARETAS"</string>
+    <string name="effect_background" msgid="6579360207378171022">"SEGUNDO PLANO"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Botão \"Tirar foto\""</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Botão de menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Foto mais recente"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Botão da câmera frontal e traseira"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Seletor de câmera, vídeo ou panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Mais controles de ajuste"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Fechar controles de ajuste"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Controle de zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Diminuir %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Aumentar %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"caixa de seleção %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Alternar para foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Alternar para vídeo"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Alterar para panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Alterar para panorama novo"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Cancelar"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Revisão concluída"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Repetir anexo"</string>
+    <string name="capital_on" msgid="5491353494964003567">"LIGADO"</string>
+    <string name="capital_off" msgid="7231052688467970897">"DESLIGADO"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Desligado"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 segundos"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minutos"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 hora"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hora"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 hora"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 horas"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 horas"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"segundos"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minutos"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"horas"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Concluído"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Definir intervalo de tempo"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"O recurso de passagem de tempo está desativado. Ative-o para definir o intervalo de tempo."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"A contagem regressiva está desativada. Ative-a para fazer uma contagem regressiva antes de tirar uma foto."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Definir duração em segundos"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Contagem regressiva para tirar foto"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Memorizar locais de fotos?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Marque seus vídeos e fotos com os locais onde foram gerados."\n\n"Outros aplicativos podem acessar essas informações juntamente com suas imagens salvas."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Não, obrigado"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Sim"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Câmera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Pesquisar"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotos"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Álbuns"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotos"</item>
+  </plurals>
+</resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
new file mode 100644
index 0000000..c022955
--- /dev/null
+++ b/res/values-rm/strings.xml
@@ -0,0 +1,667 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Catalog"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Rom da maletgs"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <!-- no translation found for movie_view_label (3526526872644898229) -->
+    <skip />
+    <string name="loading_video" msgid="4013492720121891585">"Chargiar il video…"</string>
+    <!-- no translation found for loading_image (1200894415793838191) -->
+    <skip />
+    <!-- no translation found for loading_account (928195413034552034) -->
+    <skip />
+    <string name="resume_playing_title" msgid="8996677350649355013">"Cuntinuar cun il video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Cuntinuar la reproducziun davent da %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Cuntinuar la reproducziun"</string>
+    <!-- no translation found for loading (7038208555304563571) -->
+    <skip />
+    <!-- no translation found for fail_to_load (8394392853646664505) -->
+    <skip />
+    <!-- no translation found for fail_to_load_image (6155388718549782425) -->
+    <skip />
+    <!-- no translation found for no_thumbnail (284723185546429750) -->
+    <skip />
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Cumenzar"</string>
+    <!-- no translation found for crop_save_text (152200178986698300) -->
+    <skip />
+    <!-- no translation found for ok (5296833083983263293) -->
+    <skip />
+    <!-- no translation found for multiface_crop_help (2554690102655855657) -->
+    <skip />
+    <string name="saving_image" msgid="7270334453636349407">"Memorisar il maletg..."</string>
+    <!-- no translation found for filtershow_saving_image (6659463980581993016) -->
+    <skip />
+    <!-- no translation found for save_error (6857408774183654970) -->
+    <skip />
+    <string name="crop_label" msgid="521114301871349328">"Retagliar il maletg"</string>
+    <!-- no translation found for trim_label (274203231381209979) -->
+    <skip />
+    <!-- no translation found for select_image (7841406150484742140) -->
+    <skip />
+    <!-- no translation found for select_video (4859510992798615076) -->
+    <skip />
+    <!-- no translation found for select_item (2816923896202086390) -->
+    <skip />
+    <!-- no translation found for select_album (1557063764849434077) -->
+    <skip />
+    <!-- no translation found for select_group (6744208543323307114) -->
+    <skip />
+    <string name="set_image" msgid="2331476809308010401">"Definir il maletg sco"</string>
+    <!-- no translation found for set_wallpaper (8491121226190175017) -->
+    <skip />
+    <!-- no translation found for wallpaper (140165383777262070) -->
+    <skip />
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Culissa"</string>
+    <string name="delete" msgid="2839695998251824487">"Stizzar"</string>
+    <!-- no translation found for delete_selection:one (6453379735401083732) -->
+    <!-- no translation found for delete_selection:other (5874316486520635333) -->
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
+    <string name="cancel" msgid="3637516880917356226">"Interrumper"</string>
+    <string name="share" msgid="3619042788254195341">"Cundivider"</string>
+    <!-- no translation found for share_panorama (2569029972820978718) -->
+    <skip />
+    <!-- no translation found for share_as_photo (8959225188897026149) -->
+    <skip />
+    <!-- no translation found for deleted (6795433049119073871) -->
+    <skip />
+    <!-- no translation found for undo (2930873956446586313) -->
+    <skip />
+    <!-- no translation found for select_all (3403283025220282175) -->
+    <skip />
+    <!-- no translation found for deselect_all (5758897506061723684) -->
+    <skip />
+    <string name="slideshow" msgid="4355906903247112975">"Preschentaziun da dia"</string>
+    <string name="details" msgid="8415120088556445230">"Detagls"</string>
+    <!-- no translation found for details_title (2611396603977441273) -->
+    <skip />
+    <!-- no translation found for close (5585646033158453043) -->
+    <skip />
+    <!-- no translation found for switch_to_camera (7280111806675169992) -->
+    <skip />
+    <!-- no translation found for number_of_items_selected:zero (2142579311530586258) -->
+    <!-- no translation found for number_of_items_selected:one (2478365152745637768) -->
+    <!-- no translation found for number_of_items_selected:other (754722656147810487) -->
+    <!-- no translation found for number_of_albums_selected:zero (749292746814788132) -->
+    <!-- no translation found for number_of_albums_selected:one (6184377003099987825) -->
+    <!-- no translation found for number_of_albums_selected:other (53105607141906130) -->
+    <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) -->
+    <!-- no translation found for number_of_groups_selected:one (5030162638216034260) -->
+    <!-- no translation found for number_of_groups_selected:other (3512041363942842738) -->
+    <string name="show_on_map" msgid="6157544221201750980">"Mussar sin la charta"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Rotar a sanestra"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rotar a dretga"</string>
+    <!-- no translation found for no_such_item (5315144556325243400) -->
+    <skip />
+    <!-- no translation found for edit (1502273844748580847) -->
+    <skip />
+    <!-- no translation found for process_caching_requests (8722939570307386071) -->
+    <skip />
+    <!-- no translation found for caching_label (4521059045896269095) -->
+    <skip />
+    <!-- no translation found for crop_action (3427470284074377001) -->
+    <skip />
+    <!-- no translation found for trim_action (703098114452883524) -->
+    <skip />
+    <!-- no translation found for mute_action (5296241754753306251) -->
+    <skip />
+    <string name="set_as" msgid="3636764710790507868">"Definir sco"</string>
+    <!-- no translation found for video_mute_err (6392457611270600908) -->
+    <skip />
+    <!-- no translation found for video_err (7003051631792271009) -->
+    <skip />
+    <!-- no translation found for group_by_location (316641628989023253) -->
+    <skip />
+    <!-- no translation found for group_by_time (9046168567717963573) -->
+    <skip />
+    <!-- no translation found for group_by_tags (3568731317210676160) -->
+    <skip />
+    <!-- no translation found for group_by_faces (1566351636227274906) -->
+    <skip />
+    <!-- no translation found for group_by_album (1532818636053818958) -->
+    <skip />
+    <!-- no translation found for group_by_size (153766174950394155) -->
+    <skip />
+    <!-- no translation found for untagged (7281481064509590402) -->
+    <skip />
+    <!-- no translation found for no_location (4043624857489331676) -->
+    <skip />
+    <!-- no translation found for no_connectivity (7164037617297293668) -->
+    <skip />
+    <!-- no translation found for sync_album_error (1020688062900977530) -->
+    <skip />
+    <!-- no translation found for show_images_only (7263218480867672653) -->
+    <skip />
+    <!-- no translation found for show_videos_only (3850394623678871697) -->
+    <skip />
+    <!-- no translation found for show_all (6963292714584735149) -->
+    <skip />
+    <!-- no translation found for appwidget_title (6410561146863700411) -->
+    <skip />
+    <!-- no translation found for appwidget_empty_text (1228925628357366957) -->
+    <skip />
+    <!-- no translation found for crop_saved (1595985909779105158) -->
+    <skip />
+    <!-- no translation found for no_albums_alert (4111744447491690896) -->
+    <skip />
+    <!-- no translation found for empty_album (4542880442593595494) -->
+    <skip />
+    <!-- no translation found for picasa_posts (1497721615718760613) -->
+    <skip />
+    <!-- no translation found for make_available_offline (5157950985488297112) -->
+    <skip />
+    <!-- no translation found for sync_picasa_albums (8522572542111169872) -->
+    <skip />
+    <!-- no translation found for done (217672440064436595) -->
+    <skip />
+    <!-- no translation found for sequence_in_set (7235465319919457488) -->
+    <skip />
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <!-- no translation found for description (3016729318096557520) -->
+    <skip />
+    <!-- no translation found for time (1367953006052876956) -->
+    <skip />
+    <string name="location" msgid="3432705876921618314">"Lieu"</string>
+    <!-- no translation found for path (4725740395885105824) -->
+    <skip />
+    <!-- no translation found for width (9215847239714321097) -->
+    <skip />
+    <!-- no translation found for height (3648885449443787772) -->
+    <skip />
+    <!-- no translation found for orientation (4958327983165245513) -->
+    <skip />
+    <!-- no translation found for duration (8160058911218541616) -->
+    <skip />
+    <!-- no translation found for mimetype (8024168704337990470) -->
+    <skip />
+    <!-- no translation found for file_size (8486169301588318915) -->
+    <skip />
+    <!-- no translation found for maker (7921835498034236197) -->
+    <skip />
+    <!-- no translation found for model (8240207064064337366) -->
+    <skip />
+    <!-- no translation found for flash (2816779031261147723) -->
+    <skip />
+    <!-- no translation found for aperture (5920657630303915195) -->
+    <skip />
+    <!-- no translation found for focal_length (1291383769749877010) -->
+    <skip />
+    <!-- no translation found for white_balance (1582509289994216078) -->
+    <skip />
+    <!-- no translation found for exposure_time (3990163680281058826) -->
+    <skip />
+    <!-- no translation found for iso (5028296664327335940) -->
+    <skip />
+    <!-- no translation found for unit_mm (1125768433254329136) -->
+    <skip />
+    <!-- no translation found for manual (6608905477477607865) -->
+    <skip />
+    <!-- no translation found for auto (4296941368722892821) -->
+    <skip />
+    <!-- no translation found for flash_on (7891556231891837284) -->
+    <skip />
+    <!-- no translation found for flash_off (1445443413822680010) -->
+    <skip />
+    <!-- no translation found for unknown (3506693015896912952) -->
+    <skip />
+    <!-- no translation found for ffx_original (372686331501281474) -->
+    <skip />
+    <!-- no translation found for ffx_vintage (8348759951363844780) -->
+    <skip />
+    <!-- no translation found for ffx_instant (726968618715691987) -->
+    <skip />
+    <!-- no translation found for ffx_bleach (8946700451603478453) -->
+    <skip />
+    <!-- no translation found for ffx_blue_crush (6034283412305561226) -->
+    <skip />
+    <!-- no translation found for ffx_bw_contrast (517988490066217206) -->
+    <skip />
+    <!-- no translation found for ffx_punch (1343475517872562639) -->
+    <skip />
+    <!-- no translation found for ffx_x_process (4779398678661811765) -->
+    <skip />
+    <!-- no translation found for ffx_washout (4594160692176642735) -->
+    <skip />
+    <!-- no translation found for ffx_washout_color (8034075742195795219) -->
+    <skip />
+    <!-- no translation found for make_albums_available_offline:one (2171596356101611086) -->
+    <!-- no translation found for make_albums_available_offline:other (4948604338155959389) -->
+    <!-- no translation found for try_to_set_local_album_available_offline (2184754031896160755) -->
+    <skip />
+    <!-- no translation found for set_label_all_albums (4581863582996336783) -->
+    <skip />
+    <!-- no translation found for set_label_local_albums (6698133661656266702) -->
+    <skip />
+    <!-- no translation found for set_label_mtp_devices (1283513183744896368) -->
+    <skip />
+    <!-- no translation found for set_label_picasa_albums (5356258354953935895) -->
+    <skip />
+    <!-- no translation found for free_space_format (8766337315709161215) -->
+    <skip />
+    <!-- no translation found for size_below (2074956730721942260) -->
+    <skip />
+    <!-- no translation found for size_above (5324398253474104087) -->
+    <skip />
+    <!-- no translation found for size_between (8779660840898917208) -->
+    <skip />
+    <!-- no translation found for Import (3985447518557474672) -->
+    <skip />
+    <!-- no translation found for import_complete (3875040287486199999) -->
+    <skip />
+    <!-- no translation found for import_fail (8497942380703298808) -->
+    <skip />
+    <!-- no translation found for camera_connected (916021826223448591) -->
+    <skip />
+    <!-- no translation found for camera_disconnected (2100559901676329496) -->
+    <skip />
+    <!-- no translation found for click_import (6407959065464291972) -->
+    <skip />
+    <!-- no translation found for widget_type_album (6013045393140135468) -->
+    <skip />
+    <!-- no translation found for widget_type_shuffle (8594622705019763768) -->
+    <skip />
+    <!-- no translation found for widget_type_photo (6267065337367795355) -->
+    <skip />
+    <!-- no translation found for widget_type (1364653978966343448) -->
+    <skip />
+    <!-- no translation found for slideshow_dream_name (6915963319933437083) -->
+    <skip />
+    <!-- no translation found for albums (7320787705180057947) -->
+    <skip />
+    <!-- no translation found for times (2023033894889499219) -->
+    <skip />
+    <!-- no translation found for locations (6649297994083130305) -->
+    <skip />
+    <!-- no translation found for people (4114003823747292747) -->
+    <skip />
+    <!-- no translation found for tags (5539648765482935955) -->
+    <skip />
+    <!-- no translation found for group_by (4308299657902209357) -->
+    <skip />
+    <string name="settings" msgid="1534847740615665736">"Parameters"</string>
+    <!-- no translation found for add_account (4271217504968243974) -->
+    <skip />
+    <!-- no translation found for folder_camera (4714658994809533480) -->
+    <skip />
+    <!-- no translation found for folder_download (7186215137642323932) -->
+    <skip />
+    <!-- no translation found for folder_edited_online_photos (6278215510236800181) -->
+    <skip />
+    <!-- no translation found for folder_imported (2773581395524747099) -->
+    <skip />
+    <!-- no translation found for folder_screenshot (7200396565864213450) -->
+    <skip />
+    <!-- no translation found for help (7368960711153618354) -->
+    <skip />
+    <!-- no translation found for no_external_storage_title (2408933644249734569) -->
+    <skip />
+    <!-- no translation found for no_external_storage (95726173164068417) -->
+    <skip />
+    <!-- no translation found for switch_photo_filmstrip (8227883354281661548) -->
+    <skip />
+    <!-- no translation found for switch_photo_grid (3681299459107925725) -->
+    <skip />
+    <!-- no translation found for switch_photo_fullscreen (8360489096099127071) -->
+    <skip />
+    <!-- no translation found for trimming (9122385768369143997) -->
+    <skip />
+    <!-- no translation found for muting (5094925919589915324) -->
+    <skip />
+    <!-- no translation found for please_wait (7296066089146487366) -->
+    <skip />
+    <!-- no translation found for save_into (9155488424829609229) -->
+    <skip />
+    <!-- no translation found for trim_too_short (751593965620665326) -->
+    <skip />
+    <!-- no translation found for pano_progress_text (1586851614586678464) -->
+    <skip />
+    <string name="save" msgid="613976532235060516">"Memorisar"</string>
+    <!-- no translation found for ingest_scanning (1062957108473988971) -->
+    <!-- no translation found for ingest_scanning (2048262851775139720) -->
+    <skip />
+    <!-- no translation found for ingest_number_of_items_scanned:zero (2623289390474007396) -->
+    <!-- no translation found for ingest_number_of_items_scanned:one (4340019444460561648) -->
+    <!-- no translation found for ingest_number_of_items_scanned:other (3138021473860555499) -->
+    <!-- no translation found for ingest_sorting (1028652103472581918) -->
+    <!-- no translation found for ingest_sorting (624687230903648118) -->
+    <skip />
+    <!-- no translation found for ingest_scanning_done (8911916277034483430) -->
+    <skip />
+    <!-- no translation found for ingest_importing (7456633398378527611) -->
+    <skip />
+    <!-- no translation found for ingest_empty_device (2010470482779872622) -->
+    <skip />
+    <!-- no translation found for ingest_no_device (3054128223131382122) -->
+    <skip />
+    <string name="camera_error_title" msgid="6484667504938477337">"Errur da la camera"</string>
+    <!-- no translation found for cannot_connect_camera (955440687597185163) -->
+    <skip />
+    <!-- no translation found for camera_disabled (8923911090533439312) -->
+    <skip />
+    <string name="camera_label" msgid="6346560772074764302">"Camera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Camera da video"</string>
+    <string name="wait" msgid="8600187532323801552">"Spetgar..."</string>
+    <!-- no translation found for no_storage (7335975356349008814) -->
+    <skip />
+    <!-- no translation found for no_storage (5137703033746873624) -->
+    <skip />
+    <!-- no translation found for preparing_sd (6104019983528341353) -->
+    <skip />
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Preparar la carta SD..."</string>
+    <!-- no translation found for access_sd_fail (8147993984037859354) -->
+    <skip />
+    <!-- no translation found for access_sd_fail (1584968646870054352) -->
+    <skip />
+    <string name="review_cancel" msgid="8188009385853399254">"INTERRUMPER"</string>
+    <!-- no translation found for review_ok (1156261588693116433) -->
+    <skip />
+    <!-- no translation found for time_lapse_title (4360632427760662691) -->
+    <skip />
+    <!-- no translation found for pref_camera_id_title (4040791582294635851) -->
+    <skip />
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Enavos"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Frunt"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Memorisar la posiziun"</string>
+    <!-- no translation found for pref_camera_timer_title (3105232208281893389) -->
+    <skip />
+    <!-- no translation found for pref_camera_timer_entry:one (1654523400981245448) -->
+    <!-- no translation found for pref_camera_timer_entry:other (6455381617076792481) -->
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <!-- no translation found for pref_camera_timer_sound_title (2469008631966169105) -->
+    <skip />
+    <!-- no translation found for setting_off (4480039384202951946) -->
+    <skip />
+    <!-- no translation found for setting_on (8602246224465348901) -->
+    <skip />
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Qualitad dal video"</string>
+    <!-- no translation found for pref_video_quality_entry_high (8664038216234805914) -->
+    <skip />
+    <!-- no translation found for pref_video_quality_entry_low (7258507152393173784) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_title (6245716906744079302) -->
+    <skip />
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Parameters da la camera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Parameters da la camera da video"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Grondezza dal maletg"</string>
+    <!-- no translation found for pref_camera_picturesize_entry_8mp (259953780932849079) -->
+    <skip />
+    <!-- no translation found for pref_camera_picturesize_entry_5mp (2882928212030661159) -->
+    <skip />
+    <!-- no translation found for pref_camera_picturesize_entry_3mp (741415860337400696) -->
+    <skip />
+    <!-- no translation found for pref_camera_picturesize_entry_2mp (1753709802245460393) -->
+    <skip />
+    <!-- no translation found for pref_camera_picturesize_entry_1_3mp (829109608140747258) -->
+    <skip />
+    <!-- no translation found for pref_camera_picturesize_entry_1mp (1669725616780375066) -->
+    <skip />
+    <!-- no translation found for pref_camera_picturesize_entry_vga (806934254162981919) -->
+    <skip />
+    <!-- no translation found for pref_camera_picturesize_entry_qvga (8576186463069770133) -->
+    <skip />
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Modus da focussar"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatic"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinit"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Modus da chametg (flash)"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatic"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Activà"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Deactivà"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Equiliber da l\'alv"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatic"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Pair electric"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Glisch dal di"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Glisch da neon"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nivlus"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Modus da scena"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatic"</string>
+    <!-- no translation found for pref_camera_scenemode_entry_hdr (2923388802899511784) -->
+    <skip />
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Acziun"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Notg"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Rendida dal sulegl"</string>
+    <!-- no translation found for pref_camera_scenemode_entry_party (907053529286788253) -->
+    <skip />
+    <!-- no translation found for not_selectable_in_scene_mode (2970291701448555126) -->
+    <skip />
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exposiziun"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <!-- no translation found for dialog_ok (6263301364153382152) -->
+    <skip />
+    <!-- no translation found for spaceIsLow_content (4401325203349203177) -->
+    <skip />
+    <!-- no translation found for spaceIsLow_content (1732882643101247179) -->
+    <skip />
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Cuntanschì la grondezza maximala."</string>
+    <!-- no translation found for pano_too_fast_prompt (2823839093291374709) -->
+    <skip />
+    <!-- no translation found for pano_dialog_prepare_preview (4788441554128083543) -->
+    <skip />
+    <!-- no translation found for pano_dialog_panorama_failed (2155692796549642116) -->
+    <skip />
+    <!-- no translation found for pano_dialog_title (5755531234434437697) -->
+    <skip />
+    <!-- no translation found for pano_capture_indication (8248825828264374507) -->
+    <skip />
+    <!-- no translation found for pano_dialog_waiting_previous (7800325815031423516) -->
+    <skip />
+    <!-- no translation found for pano_review_saving_indication_str (2054886016665130188) -->
+    <skip />
+    <!-- no translation found for pano_review_rendering (2887552964129301902) -->
+    <skip />
+    <!-- no translation found for tap_to_focus (8863427645591903760) -->
+    <skip />
+    <!-- no translation found for pref_video_effect_title (8243182968457289488) -->
+    <skip />
+    <!-- no translation found for effect_none (3601545724573307541) -->
+    <skip />
+    <!-- no translation found for effect_goofy_face_squeeze (1207235692524289171) -->
+    <skip />
+    <!-- no translation found for effect_goofy_face_big_eyes (3945182409691408412) -->
+    <skip />
+    <!-- no translation found for effect_goofy_face_big_mouth (7528748779754643144) -->
+    <skip />
+    <!-- no translation found for effect_goofy_face_small_mouth (3848209817806932565) -->
+    <skip />
+    <!-- no translation found for effect_goofy_face_big_nose (5180533098740577137) -->
+    <skip />
+    <!-- no translation found for effect_goofy_face_small_eyes (1070355596290331271) -->
+    <skip />
+    <!-- no translation found for effect_backdropper_space (7935661090723068402) -->
+    <skip />
+    <!-- no translation found for effect_backdropper_sunset (45198943771777870) -->
+    <skip />
+    <!-- no translation found for effect_backdropper_gallery (959158844620991906) -->
+    <skip />
+    <!-- no translation found for bg_replacement_message (9184270738916564608) -->
+    <skip />
+    <!-- no translation found for video_snapshot_hint (18833576851372483) -->
+    <skip />
+    <!-- no translation found for video_recording_started (4132915454417193503) -->
+    <skip />
+    <!-- no translation found for video_recording_stopped (5086919511555808580) -->
+    <skip />
+    <!-- no translation found for disable_video_snapshot_hint (4957723267826476079) -->
+    <skip />
+    <!-- no translation found for clear_effects (5485339175014139481) -->
+    <skip />
+    <!-- no translation found for effect_silly_faces (8107732405347155777) -->
+    <skip />
+    <!-- no translation found for effect_background (6579360207378171022) -->
+    <skip />
+    <!-- no translation found for accessibility_shutter_button (2664037763232556307) -->
+    <skip />
+    <!-- no translation found for accessibility_menu_button (7140794046259897328) -->
+    <skip />
+    <!-- no translation found for accessibility_review_thumbnail (8961275263537513017) -->
+    <skip />
+    <!-- no translation found for accessibility_camera_picker (8807945470215734566) -->
+    <skip />
+    <!-- no translation found for accessibility_mode_picker (3278002189966833100) -->
+    <skip />
+    <!-- no translation found for accessibility_second_level_indicators (3855951632917627620) -->
+    <skip />
+    <!-- no translation found for accessibility_back_to_first_level (5234411571109877131) -->
+    <skip />
+    <!-- no translation found for accessibility_zoom_control (1339909363226825709) -->
+    <skip />
+    <!-- no translation found for accessibility_decrement (1411194318538035666) -->
+    <skip />
+    <!-- no translation found for accessibility_increment (8447850530444401135) -->
+    <skip />
+    <!-- no translation found for accessibility_check_box (7317447218256584181) -->
+    <skip />
+    <!-- no translation found for accessibility_switch_to_camera (5951340774212969461) -->
+    <skip />
+    <!-- no translation found for accessibility_switch_to_video (4991396355234561505) -->
+    <skip />
+    <!-- no translation found for accessibility_switch_to_panorama (604756878371875836) -->
+    <skip />
+    <!-- no translation found for accessibility_switch_to_new_panorama (8116783308051524188) -->
+    <skip />
+    <!-- no translation found for accessibility_review_cancel (9070531914908644686) -->
+    <skip />
+    <!-- no translation found for accessibility_review_ok (7793302834271343168) -->
+    <skip />
+    <!-- no translation found for accessibility_review_retake (659300290054705484) -->
+    <skip />
+    <!-- no translation found for capital_on (5491353494964003567) -->
+    <skip />
+    <!-- no translation found for capital_off (7231052688467970897) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_off (3490489191038309496) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_500 (2949719376111679816) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_1000 (1672458758823855874) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_1500 (3415071702490624802) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_2000 (827813989647794389) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_2500 (5750464143606788153) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_3000 (2664846627499751396) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_4000 (7303255804306382651) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_5000 (6800566761690741841) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_6000 (8545447466540319539) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_10000 (3105568489694909852) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_12000 (6055574367392821047) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_15000 (2656164845371833761) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_24000 (2192628967233421512) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_30000 (5923393773260634461) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_60000 (4678581247918524850) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_90000 (1187029705069674152) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_120000 (145301938098991278) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_150000 (793707078196731912) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_180000 (1785467676466542095) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_240000 (3734507766184666356) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_300000 (7442765761995328639) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_360000 (6724596937972563920) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_600000 (6563665954471001352) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_720000 (8969801372893266408) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_900000 (5803172407245902896) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_1440000 (6286246349698492186) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_1800000 (5042628461448570758) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_3600000 (6366071632666482636) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_5400000 (536117788694519019) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_7200000 (6846617415182608533) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_9000000 (4242839574025261419) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_10800000 (2766886102170605302) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_14400000 (7497934659667867582) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_18000000 (8783643014853837140) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_21600000 (5005078879234015432) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_36000000 (69942198321578519) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_43200000 (285992046818504906) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_54000000 (5740227373848829515) -->
+    <skip />
+    <!-- no translation found for pref_video_time_lapse_frame_interval_86400000 (9040201678470052298) -->
+    <skip />
+    <!-- no translation found for time_lapse_seconds (2105521458391118041) -->
+    <skip />
+    <!-- no translation found for time_lapse_minutes (7738520349259013762) -->
+    <skip />
+    <!-- no translation found for time_lapse_hours (1776453661704997476) -->
+    <skip />
+    <!-- no translation found for time_lapse_interval_set (2486386210951700943) -->
+    <skip />
+    <!-- no translation found for set_time_interval (2970567717633813771) -->
+    <skip />
+    <!-- no translation found for set_time_interval_help (6665849510484821483) -->
+    <skip />
+    <!-- no translation found for set_timer_help (5007708849404589472) -->
+    <skip />
+    <!-- no translation found for set_duration (5578035312407161304) -->
+    <skip />
+    <!-- no translation found for count_down_title_text (4976386810910453266) -->
+    <skip />
+    <!-- no translation found for remember_location_title (9060472929006917810) -->
+    <skip />
+    <!-- no translation found for remember_location_prompt (724592331305808098) -->
+    <skip />
+    <!-- no translation found for remember_location_no (7541394381714894896) -->
+    <skip />
+    <!-- no translation found for remember_location_yes (862884269285964180) -->
+    <skip />
+    <!-- no translation found for menu_camera (3476709832879398998) -->
+    <skip />
+    <!-- no translation found for menu_search (7580008232297437190) -->
+    <skip />
+    <!-- no translation found for tab_photos (9110813680630313419) -->
+    <skip />
+    <!-- no translation found for tab_albums (8079449907770685691) -->
+    <skip />
+    <!-- no translation found for number_of_photos:one (6949174783125614798) -->
+    <!-- no translation found for number_of_photos:other (3813306834113858135) -->
+</resources>
diff --git a/res/values-ro/filtershow_strings.xml b/res/values-ro/filtershow_strings.xml
new file mode 100644
index 0000000..1452123
--- /dev/null
+++ b/res/values-ro/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor foto"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Nu se poate încărca imaginea!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Se setează imaginea de fundal"</string>
+    <string name="original" msgid="3524493791230430897">"Originală"</string>
+    <string name="borders" msgid="2067345080568684614">"Chenar"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Anulaţi"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Repetaţi"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Afişaţi istoricul"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ascundeţi istoricul"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Afişaţi stare foto."</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ascundeţi stare foto"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Setări"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Ați adus modificări imaginii pe care nu le-ați salvat."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Doriți să salvați înainte de a ieși?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Salvați și ieșiți"</string>
+    <string name="exit" msgid="242642957038770113">"Ieșiți"</string>
+    <string name="history" msgid="455767361472692409">"Istoric"</string>
+    <string name="reset" msgid="9013181350779592937">"Resetaţi"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Efecte aplicate"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Comparaţi"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Aplicaţi"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Resetaţi"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspect"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Niciunul"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fix"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Planetă mică"</string>
+    <string name="exposure" msgid="6526397045949374905">"Expunere"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Claritate"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vibranţă"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturaţie"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filtru A/N"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Culoare auto."</string>
+    <string name="hue" msgid="6231252147971086030">"Tonalitate"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Umbre"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Puncte luminoz."</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Curbe"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignetare"</string>
+    <string name="redeye" msgid="4508883127049472069">"Ochi roşii"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Desenați"</string>
+    <string name="straighten" msgid="26025591664983528">"Îndreptare"</string>
+    <string name="crop" msgid="5781263790107850771">"Decupare"</string>
+    <string name="rotate" msgid="2796802553793795371">"Rotire"</string>
+    <string name="mirror" msgid="5482518108154883096">"Oglindă"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Niciunul"</string>
+    <string name="edge" msgid="7036064886242147551">"Margini"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Mostră"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Roşu"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Verde"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Albastru"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Dimensiune"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Culoare"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linii"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marker"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Stropire"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Ștergeți"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Alegeți culoarea personalizată"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Selectați culoarea"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Selectați dimensiunea"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
new file mode 100644
index 0000000..50c6af8
--- /dev/null
+++ b/res/values-ro/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerie"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Ramă foto"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Player video"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Se încarcă videoclipul..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Se încarcă imaginea..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Contul se încarcă..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Reluaţi videoclipul"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Reluaţi redarea de la %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Reluaţi redarea"</string>
+    <string name="loading" msgid="7038208555304563571">"Se încarcă..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nu s-au putut încărca"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nu s-a putut încărca imaginea"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Nu există o miniatură"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Începeţi din nou"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Atingeţi o faţă pentru a începe."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Se salvează fotografia..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Se salvează imaginea în <xliff:g id="ALBUM_NAME">%1$s</xliff:g> ..."</string>
+    <string name="save_error" msgid="6857408774183654970">"Imaginea decupată nu s-a putut salva."</string>
+    <string name="crop_label" msgid="521114301871349328">"Decupaţi fotografia"</string>
+    <string name="trim_label" msgid="274203231381209979">"Decupaţi videoclipul"</string>
+    <string name="select_image" msgid="7841406150484742140">"Selectaţi fotografie"</string>
+    <string name="select_video" msgid="4859510992798615076">"Selectaţi videoclip"</string>
+    <string name="select_item" msgid="2816923896202086390">"Selectaţi un element"</string>
+    <string name="select_album" msgid="1557063764849434077">"Selectaţi un album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Selectaţi un grup"</string>
+    <string name="set_image" msgid="2331476809308010401">"Setaţi fotografia ca"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Setaţi imag. fundal"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Se setează imaginea de fundal..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Imagine de fundal"</string>
+    <string name="delete" msgid="2839695998251824487">"Ștergeţi"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ștergeţi articolul selectat?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Ștergeţi articolele selectate?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmaţi"</string>
+    <string name="cancel" msgid="3637516880917356226">"Anulaţi"</string>
+    <string name="share" msgid="3619042788254195341">"Distribuiţi"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Trimiteți panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Trimiteți fotografia"</string>
+    <string name="deleted" msgid="6795433049119073871">"Ștearsă"</string>
+    <string name="undo" msgid="2930873956446586313">"ANULAŢI"</string>
+    <string name="select_all" msgid="3403283025220282175">"Selectaţi-le pe toate"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Deselectaţi-le pe toate"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Prezentare"</string>
+    <string name="details" msgid="8415120088556445230">"Detalii"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d din %2$d (de) elemente:"</string>
+    <string name="close" msgid="5585646033158453043">"Închideţi"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Comutaţi la Camera foto"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d selectate"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d selectat"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d selectate"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d selectate"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d selectat"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d selectate"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d selectate"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d selectat"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d selectate"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Afişaţi pe hartă"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Rotiţi spre stânga"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rotiţi spre dreapta"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Elementul nu a putut fi găsit."</string>
+    <string name="edit" msgid="1502273844748580847">"Editaţi"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Se proces. solicit. de stocare în memoria cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Mem. cache..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Decupaţi"</string>
+    <string name="trim_action" msgid="703098114452883524">"Decupaţi"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Dezact. sunetul"</string>
+    <string name="set_as" msgid="3636764710790507868">"Setaţi ca"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Nu se poate dezactiva sunetul."</string>
+    <string name="video_err" msgid="7003051631792271009">"Nu se poate reda videoclipul."</string>
+    <string name="group_by_location" msgid="316641628989023253">"După locaţie"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"După dată"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"După etichete"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"În funcţie de persoane"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"După album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"În funcţie de dimensiune"</string>
+    <string name="untagged" msgid="7281481064509590402">"Neetichetate"</string>
+    <string name="no_location" msgid="4043624857489331676">"Fără locaţie"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Unele locaţii nu au putut fi identificate din cauza unor probleme de reţea."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Descărcarea fotografiilor din acest album a eşuat. Încercaţi din nou mai târziu."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Numai imagini"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Numai videoclipuri"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imagini şi videoclipuri"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Galerie foto"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Nicio fotografie."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Imaginea decupată a fost salvată în <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nu există albume disponibile."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Nu există imagini/videoclipuri disponibile."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Postări"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Faceţi-le disponibile offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Actualizaţi"</string>
+    <string name="done" msgid="217672440064436595">"Terminat"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d din %2$d (de) elemente:"</string>
+    <string name="title" msgid="7622928349908052569">"Titlu"</string>
+    <string name="description" msgid="3016729318096557520">"Descriere"</string>
+    <string name="time" msgid="1367953006052876956">"Oră"</string>
+    <string name="location" msgid="3432705876921618314">"Locaţie"</string>
+    <string name="path" msgid="4725740395885105824">"Cale"</string>
+    <string name="width" msgid="9215847239714321097">"Lăţime"</string>
+    <string name="height" msgid="3648885449443787772">"Înălţime"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientare"</string>
+    <string name="duration" msgid="8160058911218541616">"Durată"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Tip MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Dimens. fişier"</string>
+    <string name="maker" msgid="7921835498034236197">"Producător"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Bliţ"</string>
+    <string name="aperture" msgid="5920657630303915195">"Diafragmă"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Dist. focală"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Balanţă de alb"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Timp expunere"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Bliţ activat"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Fără bliţ"</string>
+    <string name="unknown" msgid="3506693015896912952">"Necunoscută"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Albire"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Albastru"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Alb-negru"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Proces. încr."</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografie"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Se face disponibil offline albumul."</item>
+    <item quantity="other" msgid="4948604338155959389">"Se fac disponibile offline albumele"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Acest element este stocat local şi disponibil offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Toate albumele"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Albume locale"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Dispozitive MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Albume Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Spaţiu liber: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> sau mai puţin"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> sau mai mult"</string>
+    <string name="size_between" msgid="8779660840898917208">"Între <xliff:g id="MIN_SIZE">%1$s</xliff:g> şi <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importaţi"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Import finalizat"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Import eşuat"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Camera foto conectată."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Camera foto deconectată."</string>
+    <string name="click_import" msgid="6407959065464291972">"Atingeţi aici pentru import"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Alegeţi un album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Redaţi aleatoriu toate imag."</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Alegeţi o imagine"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Alegeţi imagini"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Slideshow"</string>
+    <string name="albums" msgid="7320787705180057947">"Albume"</string>
+    <string name="times" msgid="2023033894889499219">"Ore"</string>
+    <string name="locations" msgid="6649297994083130305">"Locaţii"</string>
+    <string name="people" msgid="4114003823747292747">"Persoane"</string>
+    <string name="tags" msgid="5539648765482935955">"Etichete"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupaţi după"</string>
+    <string name="settings" msgid="1534847740615665736">"Setări"</string>
+    <string name="add_account" msgid="4271217504968243974">"Adăugaţi un cont"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Cameră foto"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Descărcate"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Fotografii online editate"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importate"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captură de ecran"</string>
+    <string name="help" msgid="7368960711153618354">"Ajutor"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Nicio stocare"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Nu este disponibilă nicio stocare externă"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Afişare tip bandă de film"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Afişare tip grilă"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Ecran complet"</string>
+    <string name="trimming" msgid="9122385768369143997">"Se ajustează"</string>
+    <string name="muting" msgid="5094925919589915324">"Se dezact. sunetul"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Aşteptaţi"</string>
+    <string name="save_into" msgid="9155488424829609229">"Se salvează videoclipul în <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Nu se poate ajusta: videoclipul ţintă este prea scurt"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Se redă panorama"</string>
+    <string name="save" msgid="613976532235060516">"Salvaţi"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Se scanează conținutul..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d elemente scanate"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d element scanat"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d (de) elemente scanate"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Se sortează..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Scanare finalizată"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Se importă..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Nu există conținut disponibil de importat pe acest gadget."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nu există niciun gadget MTP conectat"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Eroare cameră foto"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Nu se poate conecta la camera foto."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Camera foto a fost dezactivată din cauza politicilor de securitate."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Cameră foto"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Cameră video"</string>
+    <string name="wait" msgid="8600187532323801552">"Aşteptaţi…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Înainte de a utiliza camera foto, montaţi dispozitivul de stocare USB."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Introduceţi un card SD înainte de a utiliza camera foto."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Se pregăteşte stocarea USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Se pregăteşte cardul SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nu s-a putut accesa stocarea USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nu s-a putut accesa cardul SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"Anulaţi"</string>
+    <string name="review_ok" msgid="1156261588693116433">"TERMINAT"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Înregistrare cu filmare lentă"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Alegeţi camera foto"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Înapoi"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Frontal"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Stocaţi locaţia"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Cronometru numărătoare inversă"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"O secundă"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d (de) secunde"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Beep numărătoare"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Dezactivată"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Activată"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Calitate video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Ridicată"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Redusă"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Filmare lentă"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Setările camerei foto"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Setările camerei video"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Dimensiune fotografie"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapixeli"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 MP"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 MP"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 MP"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 MP"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 MP"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Mod focus"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automat"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Infinit"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Mod flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automat"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Activat"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Dezactivată"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Balanţă de alb"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automat"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Incandescent"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Lumină de zi"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescent"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Înnorat"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Mod Scenă"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automat"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Acţiune"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Noapte"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Apus de soare"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Petrecere"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Nu este selectabil în modul scenă."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Expunere"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Spaţiul de stocare USB este aproape ocupat. Editaţi setarea de calitate sau ştergeţi câteva imagini ori alte fişiere."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Cardul SD rămâne fără spaţiu de stocare. Editaţi setarea de calitate sau ştergeţi câteva imagini sau alte fişiere."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Dimensiune limită depăşită."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Prea repede"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Se pregăteşte panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panorama nu a putut fi salvată."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panoramă"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Se capturează panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Se aşteaptă panorama anterioară"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Se salvează.."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Se redă panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Atingeţi pentru a focaliza."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efecte"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Niciunul"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Comprimare"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Ochi mari"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Gură mare"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Gură mică"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Nas mare"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Ochi mici"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"În spaţiu"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Apus de soare"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Videoclip"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Aşezaţi dispozitivul jos."\n"Ieşiţi din cadru un moment."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Atingeţi pentru a fotografia în timpul înregistrării."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"A început înregistrarea videoclipului."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Înregistrarea videoclipului s-a oprit."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Instant. video este dezact. când efectele speciale sunt pornite."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Ștergeţi efectul"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"FEŢE PROSTUŢE"</string>
+    <string name="effect_background" msgid="6579360207378171022">"FUNDAL"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Butonul Declanşaţi"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Butonul Meniu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Cea mai recentă fotografie"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Comutator pentru camera foto din faţă şi din spate"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Selector pentru modurile cameră foto, cameră video sau panoramă"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Mai multe comenzi pentru setări"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Închideţi comenzile pentru setări"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Comandă mărire/micşorare"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Reduceţi %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Măriţi %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Caseta de selectare %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Comutaţi la camera foto"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Comutaţi la camera video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Comutaţi la panoramă"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Comutaţi la o panoramă nouă"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Anulaţi examinarea"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Terminaţi examinarea"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Refaceţi"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ACTIVAT"</string>
+    <string name="capital_off" msgid="7231052688467970897">"DEZACTIVAT"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Dezactivat"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 secundă"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 de secunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 de minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 oră"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ore"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 de ore"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"secunde"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minute"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ore"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Terminat"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Setaţi intervalul de timp"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Funcţia Filmare lentă este dezactivată. Activaţi-o pentru a seta intervalul de timp."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Cronometrul numărătorii inverse a fost dezactivat. Activați-l înainte de a fotografia."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Setați durata în secunde"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Numărătoare inversă până la fotografiere"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Doriţi să vă amintiţi locaţiile fotografiilor?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Etichetaţi-vă fotografiile şi videoclipurile cu locaţiile în care acestea au fost create."\n\n"Alte aplicaţii pot accesa aceste informaţii, împreună cu imaginile salvate."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nu, mulţumesc"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Da"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Cameră foto"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Căutați"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotografii"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albume"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotografie"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotografii"</item>
+  </plurals>
+</resources>
diff --git a/res/values-ru/filtershow_strings.xml b/res/values-ru/filtershow_strings.xml
new file mode 100644
index 0000000..f20447f
--- /dev/null
+++ b/res/values-ru/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Графический редактор"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Не удалось загрузить изображение."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Установка обоев…"</string>
+    <string name="original" msgid="3524493791230430897">"Оригинал"</string>
+    <string name="borders" msgid="2067345080568684614">"Границы"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Отмена"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Повторить"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Показать историю"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Скрыть историю"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Показать состояние"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Скрыть состояние"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Настройки"</string>
+    <string name="unsaved" msgid="8704442449002374375">"У вас есть несохраненные изменения."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Сохранить изменения?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Сохранить и закрыть"</string>
+    <string name="exit" msgid="242642957038770113">"Закрыть"</string>
+    <string name="history" msgid="455767361472692409">"История"</string>
+    <string name="reset" msgid="9013181350779592937">"Сброс"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Эффекты"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Сравнить"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Применить:"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Сброс"</string>
+    <string name="aspect" msgid="4025244950820813059">"Формат"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Вручную"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Постоянное"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Закругление"</string>
+    <string name="exposure" msgid="6526397045949374905">"Экспозиция"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Резкость"</string>
+    <string name="contrast" msgid="2310908487756769019">"Контраст"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vibrance"</string>
+    <string name="saturation" msgid="7026791551032438585">"Насыщ-сть"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Ч/Б"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Авторежим"</string>
+    <string name="hue" msgid="6231252147971086030">"Оттенок"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Тени"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Блики"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Кривые"</string>
+    <string name="vignette" msgid="934721068851885390">"Виньет-ние"</string>
+    <string name="redeye" msgid="4508883127049472069">"Красные глаза"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Подмалевок"</string>
+    <string name="straighten" msgid="26025591664983528">"Выровнять"</string>
+    <string name="crop" msgid="5781263790107850771">"Кадрирование"</string>
+    <string name="rotate" msgid="2796802553793795371">"Повернуть"</string>
+    <string name="mirror" msgid="5482518108154883096">"Отразить"</string>
+    <string name="negative" msgid="6998313764388022201">"Негатив"</string>
+    <string name="none" msgid="6633966646410296520">"Оригинал"</string>
+    <string name="edge" msgid="7036064886242147551">"Края"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Уорхол"</string>
+    <string name="downsample" msgid="3552938534146980104">"Уменьшить"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Красный"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Зеленый"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Синий"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Стиль"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Размер"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Цвет"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Линии"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Маркер"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Распыление"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Очистить"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Выбрать свой цвет"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Выберите цвет"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Выберите размер"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"ОК"</string>
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
new file mode 100644
index 0000000..7bb107f
--- /dev/null
+++ b/res/values-ru/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Галерея"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Рамка фотографии"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Видеопроигрыватель"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Загрузка видео…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Загрузка изображения..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Загрузка аккаунта..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Продолжение просмотра видео"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Продолжить воспроизведение с %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Продолжить воспроизведение"</string>
+    <string name="loading" msgid="7038208555304563571">"Загрузка…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Не загружено"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Не удалось загрузить изображение"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Нет уменьшенного изображения"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Начать с начала"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ОК"</string>
+    <string name="ok" msgid="5296833083983263293">"ОК"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Нажмите на лицо, чтобы начать."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Сохранение картинки..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Сохранение в альбоме \"<xliff:g id="ALBUM_NAME">%1$s</xliff:g>\"…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Изображение не сохранено."</string>
+    <string name="crop_label" msgid="521114301871349328">"Обрезать фотографию"</string>
+    <string name="trim_label" msgid="274203231381209979">"Вырезать фрагмент"</string>
+    <string name="select_image" msgid="7841406150484742140">"Выбрать фото"</string>
+    <string name="select_video" msgid="4859510992798615076">"Выбрать видео"</string>
+    <string name="select_item" msgid="2816923896202086390">"Выбрать файлы"</string>
+    <string name="select_album" msgid="1557063764849434077">"Выбрать альбомы"</string>
+    <string name="select_group" msgid="6744208543323307114">"Выбрать группы"</string>
+    <string name="set_image" msgid="2331476809308010401">"Установить картинку как"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Установить обои"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Установка обоев..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Обои"</string>
+    <string name="delete" msgid="2839695998251824487">"Удалить"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Удалить выбранный файл?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Удалить выбранные файлы?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Подтвердить"</string>
+    <string name="cancel" msgid="3637516880917356226">"Отмена"</string>
+    <string name="share" msgid="3619042788254195341">"Отправить"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Отправить панораму"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Отправить фото"</string>
+    <string name="deleted" msgid="6795433049119073871">"Удалено"</string>
+    <string name="undo" msgid="2930873956446586313">"ОТМЕНИТЬ"</string>
+    <string name="select_all" msgid="3403283025220282175">"Выбрать все"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Отменить все"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Слайд-шоу"</string>
+    <string name="details" msgid="8415120088556445230">"Сведения"</string>
+    <string name="details_title" msgid="2611396603977441273">"Элементов %1$d из %2$d:"</string>
+    <string name="close" msgid="5585646033158453043">"Закрыть"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Режим \"Фото\""</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Показать на карте"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Повернуть влево"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Повернуть вправо"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Элемент не найден."</string>
+    <string name="edit" msgid="1502273844748580847">"Изменить"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Обработка запросов на кэширование"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Кэширование..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Кадрировать"</string>
+    <string name="trim_action" msgid="703098114452883524">"Обрезка"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Отключить звук"</string>
+    <string name="set_as" msgid="3636764710790507868">"Установить как"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Произошла ошибка"</string>
+    <string name="video_err" msgid="7003051631792271009">"Не удалось воспроизвести видео."</string>
+    <string name="group_by_location" msgid="316641628989023253">"По месту съемки"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"По времени создания"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"По тегам"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"По именам"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"По альбомам"</string>
+    <string name="group_by_size" msgid="153766174950394155">"По размеру"</string>
+    <string name="untagged" msgid="7281481064509590402">"Без тегов"</string>
+    <string name="no_location" msgid="4043624857489331676">"Место не указано"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Не удалось найти все места из-за проблем с подключением."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Не удалось загрузить фото. Повторите попытку позже."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Только изображения"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Только видео"</string>
+    <string name="show_all" msgid="6963292714584735149">"Изображения и видео"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерея"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Нет фотографий"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Обрезанное изображение сохранено в папке <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Нет доступных альбомов."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Нет фото/видео"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Записи"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Офлайн-доступ"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Обновить"</string>
+    <string name="done" msgid="217672440064436595">"Готово"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d из %2$d:"</string>
+    <string name="title" msgid="7622928349908052569">"Название"</string>
+    <string name="description" msgid="3016729318096557520">"Описание"</string>
+    <string name="time" msgid="1367953006052876956">"Время"</string>
+    <string name="location" msgid="3432705876921618314">"Место съемки"</string>
+    <string name="path" msgid="4725740395885105824">"Путь"</string>
+    <string name="width" msgid="9215847239714321097">"Ширина"</string>
+    <string name="height" msgid="3648885449443787772">"Высота"</string>
+    <string name="orientation" msgid="4958327983165245513">"Ориентация"</string>
+    <string name="duration" msgid="8160058911218541616">"Длительность"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Тип MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Размер файла"</string>
+    <string name="maker" msgid="7921835498034236197">"Автор"</string>
+    <string name="model" msgid="8240207064064337366">"Модель"</string>
+    <string name="flash" msgid="2816779031261147723">"Вспышка"</string>
+    <string name="aperture" msgid="5920657630303915195">"Диафрагма"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Фокус. расст."</string>
+    <string name="white_balance" msgid="1582509289994216078">"Баланс белого"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Выдержка"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"мм"</string>
+    <string name="manual" msgid="6608905477477607865">"Вручную"</string>
+    <string name="auto" msgid="4296941368722892821">"Авто"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Со вспышкой"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Без вспышки"</string>
+    <string name="unknown" msgid="3506693015896912952">"Неизвестно"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Оригинал"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Винтаж"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Фотоавтомат"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Отбеливание"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Синева"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Ч/Б"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Сжатие"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X-процесс"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Латте"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Литография"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Загрузка альбома для офлайн-доступа"</item>
+    <item quantity="other" msgid="4948604338155959389">"Загрузка альбомов для офлайн-доступа."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Это содержание хранится на устройстве и доступно в автономном режиме."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Все альбомы"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Офлайн-альбомы"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-устройства"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Альбомы Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Свободно: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> или менее"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> или более"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> – <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Импорт"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Импорт завершен"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Сбой импорта"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Камера подключена."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Камера отключена."</string>
+    <string name="click_import" msgid="6407959065464291972">"Нажмите здесь, чтобы начать импорт"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Выбрать альбом"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Перемешать все изображения"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Выберите изображение"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Выбор изображений"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Слайд-шоу"</string>
+    <string name="albums" msgid="7320787705180057947">"Альбомы"</string>
+    <string name="times" msgid="2023033894889499219">"Даты"</string>
+    <string name="locations" msgid="6649297994083130305">"Места"</string>
+    <string name="people" msgid="4114003823747292747">"Люди"</string>
+    <string name="tags" msgid="5539648765482935955">"Теги"</string>
+    <string name="group_by" msgid="4308299657902209357">"Сгруппировать"</string>
+    <string name="settings" msgid="1534847740615665736">"Настройки"</string>
+    <string name="add_account" msgid="4271217504968243974">"Добавить аккаунт"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Загруженные"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Отредактированные онлайн"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Импортированные"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Скриншоты"</string>
+    <string name="help" msgid="7368960711153618354">"Справка"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Накопитель не найден"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Нет внешних накопителей"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Лента"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Сетка"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Во весь экран"</string>
+    <string name="trimming" msgid="9122385768369143997">"Обрезка"</string>
+    <string name="muting" msgid="5094925919589915324">"Подождите…"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Подождите…"</string>
+    <string name="save_into" msgid="9155488424829609229">"Сохранение в альбом \"<xliff:g id="ALBUM_NAME">%1$s</xliff:g>\"…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Нельзя обрезать видео: оно слишком короткое"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Создание панорамы…"</string>
+    <string name="save" msgid="613976532235060516">"Сохранить"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Сканирование…"</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Отсканировано: %1$d"</item>
+    <item quantity="one" msgid="4340019444460561648">"Отсканировано: %1$d"</item>
+    <item quantity="other" msgid="3138021473860555499">"Отсканировано: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Сортировка…"</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Сканирование завершено"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Импорт…"</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Ничего не найдено"</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"MTP-устройство не подключено"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Ошибка камеры"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Не удалось подключиться к камере."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Камера отключена в соответствии с политикой безопасности."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Камера"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Видеокамера"</string>
+    <string name="wait" msgid="8600187532323801552">"Подождите..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Подключите USB-накопитель перед использованием камеры."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Вставьте SD-карту перед использованием камеры."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Подготовка USB-накопителя…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Подготовка карты SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Нет доступа к USB-накопителю."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Нет доступа к SD-карте."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ОТМЕНА"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ГОТОВО"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Режим замедленной съемки"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Выбор камеры"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Задняя"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Передняя"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Геотеги"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Таймер обратного отсчета"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 сек."</item>
+    <item quantity="other" msgid="6455381617076792481">"%d сек."</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Сигнал"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Выкл."</string>
+    <string name="setting_on" msgid="8602246224465348901">"Вкл."</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Качество видео"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Высокое качество"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Низкое качество"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Замедленная съемка"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Настройки камеры"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Настройки видеокамеры"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Размер фото"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 Мпикс."</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 Мпикс."</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 Мпикс."</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 Мпикс."</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 Мпикс."</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 Мпикс."</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"640 х 480 пикс."</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"320 x 240 пикс."</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Режим фокусировки"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Авто"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Бесконечность"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Макро"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Режим вспышки"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Авто"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Вкл."</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Выкл."</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Баланс белого"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Авто"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Лампа накаливания"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Солнечный свет"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Лампа дн. света"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Пасмурный день"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Режим съемки"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Авто"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"Эффект HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Спорт"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Ночь"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Закат"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Вечеринка"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Недоступно в режиме съемки"</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Экспозиция"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"ОК"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Место на USB-накопителе заканчивается. Измените настройки качества или удалите ненужные изображения и другие файлы."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Место на вашей SD-карте заканчивается. Измените настройки качества или удалите ненужные изображения и другие файлы."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Достигнут предельный размер видео."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Очень быстро"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Обработка..."</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Не удалось сохранить файл."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Панорама"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Создание панорамы..."</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Обработка предыдущего файла…"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Сохранение…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Создание панорамы…"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Нажмите для фокусировки."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Эффекты"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Оригинал"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Узкое лицо"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Большие глаза"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Большой рот"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Маленький рот"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Большой нос"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Глаза в точку"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Космос"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Закат"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Мои видео"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Установите устройство на твердой поверхности."\n"Затем отойдите, чтобы вас было видно в камеру."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Нажмите, чтобы сделать фотографию во время записи."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Запись начата"</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Запись остановлена"</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Невозможно сделать снимок при включенных спецэффектах."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Убрать эффекты"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"СМЕШНЫЕ РОЖИЦЫ"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ФОН"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Кнопка \"Затвор\""</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Кнопка \"Меню\""</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Недавние фото"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Переключение на переднюю или заднюю камеру"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Переключатель между режимами \"Фото\", \"Видео\" и \"Панорама\""</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Дополнительные настройки"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Закрыть"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Зум"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Уменьшить %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Увеличить %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Флажок \"%1$s\""</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Режим \"Фото\""</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Режим \"Видео\""</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Режим \"Панорама\""</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Создать панораму"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Отмена"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Готово"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Ещё раз"</string>
+    <string name="capital_on" msgid="5491353494964003567">"I"</string>
+    <string name="capital_off" msgid="7231052688467970897">"O"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Выкл."</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 сек."</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 мин."</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ч."</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ч."</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"сек."</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"мин."</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ч."</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Готово"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Задайте интервал"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Сначала включите режим замедленной съемки."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Таймер обратного отсчета отключен. Включите его, чтобы сделать снимок с задержкой."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Время"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Обратный отсчет перед съемкой"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Сохранять место съемки?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Информация о месте съемки будет автоматически добавляться в описание ваших фотографий и видеозаписей."\n\n"Доступ к этим данным, а также к самим фотографиям и видео смогут получить и другие приложения."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Нет, спасибо"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Да"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Камера"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Поиск"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Фото"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Альбомы"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d фото"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d фото"</item>
+  </plurals>
+</resources>
diff --git a/res/values-sk/filtershow_strings.xml b/res/values-sk/filtershow_strings.xml
new file mode 100644
index 0000000..1e9c813
--- /dev/null
+++ b/res/values-sk/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor fotografií"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Obrázok sa nepodarilo načítať!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Prebieha nastavovanie tapety"</string>
+    <string name="original" msgid="3524493791230430897">"Pôvodné"</string>
+    <string name="borders" msgid="2067345080568684614">"Okraje"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Späť"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Znova"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Zobraziť históriu"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Skryť históriu"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Zobraz. stav obrázka"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Skryť stav obrázka"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Nastavenia"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Niektoré zmeny obrázka nie sú uložené."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Chcete zmeny pred ukončením uložiť?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Uložiť a ukončiť"</string>
+    <string name="exit" msgid="242642957038770113">"Ukončiť"</string>
+    <string name="history" msgid="455767361472692409">"História"</string>
+    <string name="reset" msgid="9013181350779592937">"Obnoviť"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Použité efekty"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Porovnať"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Použiť"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Obnoviť"</string>
+    <string name="aspect" msgid="4025244950820813059">"Pomer strán"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Žiadny"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Pevné"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Malá planéta"</string>
+    <string name="exposure" msgid="6526397045949374905">"Expozícia"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ostrosť"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Živosť"</string>
+    <string name="saturation" msgid="7026791551032438585">"Sýtosť"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Filter ČB"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autom. farba"</string>
+    <string name="hue" msgid="6231252147971086030">"Odtieň"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Tiene"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Najsvetlejšie tóny"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Krivky"</string>
+    <string name="vignette" msgid="934721068851885390">"Vineta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Červené oči"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Kresliť"</string>
+    <string name="straighten" msgid="26025591664983528">"Vyrovnať"</string>
+    <string name="crop" msgid="5781263790107850771">"Orezanie"</string>
+    <string name="rotate" msgid="2796802553793795371">"Otočiť"</string>
+    <string name="mirror" msgid="5482518108154883096">"Zrkadliť"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatív"</string>
+    <string name="none" msgid="6633966646410296520">"Žiadne"</string>
+    <string name="edge" msgid="7036064886242147551">"Hrany"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Zmen.vzor."</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Červená"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Zelená"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Modrá"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Štýl"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Veľkosť"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Farba"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Čiary"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Zvýrazňovač"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Škvrna"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Vymazať"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Vybrať vlastnú farbu"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Vyberte farbu"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Vyberte veľkosť"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
new file mode 100644
index 0000000..938c7ba
--- /dev/null
+++ b/res/values-sk/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galéria"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Rámec fotografie"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Prehrávač videa"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Prebieha načítavanie videa…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Prebieha načítavanie obrázka..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Načítavanie účtu…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Obnoviť prehrávanie videa"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Pokračovať v prehrávaní od %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Obnoviť prehrávanie"</string>
+    <string name="loading" msgid="7038208555304563571">"Prebieha načítavanie…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Nepodarilo sa načítať"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Obrázok sa nepodarilo načítať"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Žiadne miniatúry"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Začať odznova"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Začnite dotknutím sa tváre."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Prebieha ukladanie fotografie..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Obrázok sa ukladá do albumu <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Orezaný obrázok sa nepodarilo uložiť."</string>
+    <string name="crop_label" msgid="521114301871349328">"Orezať fotografiu"</string>
+    <string name="trim_label" msgid="274203231381209979">"Skrátiť video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Vyberte fotografiu"</string>
+    <string name="select_video" msgid="4859510992798615076">"Vyberte video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Vyberte položku"</string>
+    <string name="select_album" msgid="1557063764849434077">"Vybrať album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Vybrať skupinu"</string>
+    <string name="set_image" msgid="2331476809308010401">"Fotografia bude použitá ako"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nastaviť tapetu"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Prebieha nastavenie tapety..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
+    <string name="delete" msgid="2839695998251824487">"Odstrániť"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Odstrániť vybratú položku?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Odstrániť vybraté položky?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potvrdiť"</string>
+    <string name="cancel" msgid="3637516880917356226">"Zrušiť"</string>
+    <string name="share" msgid="3619042788254195341">"Zdieľať"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Zdieľať panorámu"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Zdieľať ako fotografiu"</string>
+    <string name="deleted" msgid="6795433049119073871">"Odstránený"</string>
+    <string name="undo" msgid="2930873956446586313">"SPÄŤ"</string>
+    <string name="select_all" msgid="3403283025220282175">"Vybrať všetko"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Zrušiť výber všetkých položiek"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Prezentácia"</string>
+    <string name="details" msgid="8415120088556445230">"Podrobnosti"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d z %2$d položiek:"</string>
+    <string name="close" msgid="5585646033158453043">"Zavrieť"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Prepnúť do režimu fotoaparát"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Počet vybratých položiek: %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Počet vybratých položiek: %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Počet vybratých položiek: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Počet vybratých albumov: %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Počet vybratých albumov: %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Počet vybratých albumov: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Počet vybratých skupín: %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Počet vybratých skupín: %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Počet vybratých skupín: %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Zobraziť na mape"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Otočiť doľava"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Otočiť doprava"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Položku sa nepodarilo nájsť."</string>
+    <string name="edit" msgid="1502273844748580847">"Upraviť"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Spracovávanie žiadostí o ulož. do vyrovnáv. pamäte"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Do pamäte..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Orezať"</string>
+    <string name="trim_action" msgid="703098114452883524">"Orezať"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Vypnúť zvuk"</string>
+    <string name="set_as" msgid="3636764710790507868">"Použiť ako"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Zvuk videa sa nedá vypnúť."</string>
+    <string name="video_err" msgid="7003051631792271009">"Video sa nepodarilo prehrať."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Podľa miesta"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Podľa času"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Podľa značiek"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Podľa ľudí"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Podľa albumu"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Podľa veľkosti"</string>
+    <string name="untagged" msgid="7281481064509590402">"Neoznačené"</string>
+    <string name="no_location" msgid="4043624857489331676">"Bez údajov o polohe"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Niektoré umiestnenia nebolo možné identifikovať kvôli problémom so sieťou."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Fotografie z tohto albumu sa nepodarilo prevziať. Skúste to znova neskôr."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Iba obrázky"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Iba videá"</string>
+    <string name="show_all" msgid="6963292714584735149">"Obrázky a videá"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogaléria"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Žiadne fotografie."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Orezaný obrázok bol uložený do priečinka <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Nie sú k dispozícii žiadne albumy."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Počet obrázkov / videí k dispozícii: 0"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Príspevky"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Sprístupniť offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Obnoviť"</string>
+    <string name="done" msgid="217672440064436595">"Hotovo"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d z %2$d položiek:"</string>
+    <string name="title" msgid="7622928349908052569">"Titul"</string>
+    <string name="description" msgid="3016729318096557520">"Popis"</string>
+    <string name="time" msgid="1367953006052876956">"Čas"</string>
+    <string name="location" msgid="3432705876921618314">"Poloha"</string>
+    <string name="path" msgid="4725740395885105824">"Cesta"</string>
+    <string name="width" msgid="9215847239714321097">"Šírka"</string>
+    <string name="height" msgid="3648885449443787772">"Výška"</string>
+    <string name="orientation" msgid="4958327983165245513">"Orientácia"</string>
+    <string name="duration" msgid="8160058911218541616">"Trvanie"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Typ MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Veľkosť súboru"</string>
+    <string name="maker" msgid="7921835498034236197">"Autor"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Blesk"</string>
+    <string name="aperture" msgid="5920657630303915195">"Clona"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Ohnisk. vzd."</string>
+    <string name="white_balance" msgid="1582509289994216078">"Vyváženie bielej"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Doba expozície"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Ručne"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"S bleskom"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Bez blesku"</string>
+    <string name="unknown" msgid="3506693015896912952">"Neznáme"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Pôvodné"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Staré"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instantný"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bielidlo"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Modrá"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"ČB"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Dierovanie"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Cross process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografia"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Sprístupňovanie albumu v režime offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Sprístupňovanie albumov v režime offline."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Táto položka je uložená miestne a je k dispozícii v režime offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Všetky albumy"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Miestne albumy"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Zariadenia MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Albumy Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Voľná pamäť: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> alebo menej"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> alebo viac"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> až <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Import"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Import sa dokončil"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Import bol neúspešný"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Fotoaparát bol pripojený."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Fotoaparát bol odpojený."</string>
+    <string name="click_import" msgid="6407959065464291972">"Ak chcete spustiť import, dotknite sa tu"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Vybrať album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Náhodné poradie obrázkov"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Zvoľte obrázok"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Vybrať obrázky"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Prezentácia"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumy"</string>
+    <string name="times" msgid="2023033894889499219">"Časy"</string>
+    <string name="locations" msgid="6649297994083130305">"Miesta"</string>
+    <string name="people" msgid="4114003823747292747">"Ľudia"</string>
+    <string name="tags" msgid="5539648765482935955">"Značky"</string>
+    <string name="group_by" msgid="4308299657902209357">"Zoskupiť podľa"</string>
+    <string name="settings" msgid="1534847740615665736">"Nastavenia"</string>
+    <string name="add_account" msgid="4271217504968243974">"Pridať účet"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparát"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Preberanie"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Upravené fotografie online"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importované"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Snímka obrazovky"</string>
+    <string name="help" msgid="7368960711153618354">"Pomocník"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Žiadny ukladací priestor"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"K dispozícii nie je žiadny externý ukladací priestor"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Zobrazenie filmového pásu"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Zobrazenie v mriežke"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Na celú obrazovku"</string>
+    <string name="trimming" msgid="9122385768369143997">"Orezanie"</string>
+    <string name="muting" msgid="5094925919589915324">"Vypína sa zvuk..."</string>
+    <string name="please_wait" msgid="7296066089146487366">"Počkajte"</string>
+    <string name="save_into" msgid="9155488424829609229">"Video sa ukladá do albumu <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Nie je možné orezať: výsledné video je príliš krátke"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Vykresľovanie panorámy"</string>
+    <string name="save" msgid="613976532235060516">"Uložiť"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Prebieha vyhľadávanie obsahu..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"počet vyhľadaných položiek: %1$d"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d vyhľadaná položka"</item>
+    <item quantity="other" msgid="3138021473860555499">"počet vyhľadaných položiek: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Prebieha zoradenie..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Vyhľadávanie bolo dokončené"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Prebieha import..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Nie je k dispozícii žiadny obsah na import do tohto zariadenia."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Nie je pripojené žiadne zariadenie MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Chyba fotoaparátu"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Nedá sa pripojiť k fotoaparátu."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Fotoaparát je zakázaný z dôvodu bezpečnostných pravidiel."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Fotoaparát"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Čakajte, prosím..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Pred použitím fotoaparátu pripojte zdieľané úložisko USB."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Pred použitím fotoaparátu vložte kartu SD."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Príprava uklad. priestoru USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Príprava karty SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Nepodarilo sa získať prístup k ukladaciemu priestoru USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Nepodarilo sa získať prístup ku karte SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ZRUŠIŤ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"HOTOVO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Časozberný záznam"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Vybrať fotoaparát"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Zozadu"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Spredu"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Úložné miesto"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Časovač odpočítavania"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 s"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d s"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Zvuk pri odpočítavaní"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Vypnuté"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Zapnuté"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Kvalita videa"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Vysoká"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Nízka"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Časozberné video"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Nastavenia fotoaparátu"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Nastavenie videokamery"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Veľkosť fotografie"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapixelov"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapixlov"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapixle"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapixle"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapixlov"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Režim zaostrenia"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Nekonečno"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Režim blesku"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Auto"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Zapnuté"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Vypnuté"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Vyváženie bielej"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Auto"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Žiariace"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Denné svetlo"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Svetielkujúce"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Zamračené"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scénický režim"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Auto"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Akcia"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Noc"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Západ slnka"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Strana"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Nie je možné vybrať v scénickom režime."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Expozícia"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"V ukladacom priestore USB je málo miesta. Zmeňte nastavenie kvality alebo odstráňte niektoré obrázky či iné súbory."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Na karte SD je málo miesta. Zmeňte nastavenie kvality alebo odstráňte niektoré obrázky či iné súbory."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Bolo dosiahnuté obmedzenie veľkosti."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Veľmi rýchlo"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Príprava panorámy"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panorámu sa nepodarilo uložiť."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panoráma"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Snímanie panorámy"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Čaká sa na predchádzajúcu panorámu"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Ukladá sa..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Vykresľovanie panorámy"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Dotykom zaostríte."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efekty"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Žiadne"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Stlačiť"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Veľké oči"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Veľké ústa"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Malé ústa"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Veľký nos"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Malé oči"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Vo vesmíre"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Západ slnka"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Vaše video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Položte svoje zariadenie."\n"Vyjdite na chvíľu zo zorného poľa."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Dotykom môžete počas záznamu fotiť."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Záznam videa bol spustený."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Záznam videa bol zastavený."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Pri zapnutých špeciálnych efektoch je vytváranie snímok zakázané."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Vymazať efekty"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"BLÁZNIVÉ TVÁRE"</string>
+    <string name="effect_background" msgid="6579360207378171022">"POZADIE"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Tlačidlo uzávierky"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Tlačidlo Menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Posledná fotografia"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Prepínač medzi prednou a zadnou kamerou"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Prepínač medzi panoramatickým režimom a režimami fotoaparátu a videokamery"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Ďalšie ovládacie prvky nastavenia"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Zavrieť ovládacie prvky nastavenia"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Ovládanie priblíženia/oddialenia"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Znížiť %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Zvýšiť %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Začiarkavacie políčko %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Prepnúť na fotoaparát"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Prepnúť do režimu videa"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Prepnúť na panorámu"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Prepnúť na novú panorámu"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Zrušiť"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Hotovo"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Nasnímať znova"</string>
+    <string name="capital_on" msgid="5491353494964003567">"ZAPNUTÉ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"VYPNUTÉ"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Vypnuté"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekunda"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekundy"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekúnd"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekúnd"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekúnd"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekúnd"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekúnd"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekúnd"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minúty"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minúta"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minúty"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minúty"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minúty"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minúty"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minúty"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minút"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minút"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minút"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minút"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minút"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minút"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 hodina"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 hodiny"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 hodín"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 hodín"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 hodín"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 hodín"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 hodín"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 hodín"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekundy"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minúty"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"hodiny"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Hotovo"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Nastaviť časový interval"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Funkcia časozberného videa je vypnutá. Zapnite ju a a nastavte časový interval."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Časovač odpočítavania je vypnutý. Ak chcete odpočítavať pred aktivovaním spúšte fotoaparátu, zapnite ho."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Nastavte dobu trvania v sekundách"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Odpočítavanie spúšte fotoaparátu"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Zapamätať si, kde boli fotografie vytvorené?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Označte pre fotografie a videá polohy, kde boli zaznamenané."\n\n"Ostatné aplikácie môžu pristupovať k týmto informáciám aj k vašim uloženým snímkam."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nie, ďakujem"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Áno"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Fotoaparát"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Vyhľadávanie"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotografie"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumy"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotografia"</item>
+    <item quantity="other" msgid="3813306834113858135">"Fotografie: %1$d"</item>
+  </plurals>
+</resources>
diff --git a/res/values-sl/filtershow_strings.xml b/res/values-sl/filtershow_strings.xml
new file mode 100644
index 0000000..7b288e6
--- /dev/null
+++ b/res/values-sl/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Urejevalnik fotografij"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Slike ni mogoče naložiti."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Nastavljanje ozadja"</string>
+    <string name="original" msgid="3524493791230430897">"Izvirnik"</string>
+    <string name="borders" msgid="2067345080568684614">"Obrobe"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Razveljavi"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Uveljavi"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Pokaži zgodovino"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Skrij zgodovino"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Pokaži stanje slike"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Skrij stanje slike"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Nastavitve"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Spremembe te slike niso shranjene."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Ali želite pred zapiranjem shraniti?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Shrani in zapri"</string>
+    <string name="exit" msgid="242642957038770113">"Izhod"</string>
+    <string name="history" msgid="455767361472692409">"Zgodovina"</string>
+    <string name="reset" msgid="9013181350779592937">"Ponastavi"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Uporabljeni učinki"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Primerjaj"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Uporabi"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Ponastavi"</string>
+    <string name="aspect" msgid="4025244950820813059">"Razmerje"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1 : 1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4 : 3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3 : 4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4 : 6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5 : 7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7 : 5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16 : 9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Brez"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Nespremenljivo"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Planetek"</string>
+    <string name="exposure" msgid="6526397045949374905">"Osvetlitev"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ostrina"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Živahnost"</string>
+    <string name="saturation" msgid="7026791551032438585">"Nasičenost"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"ČB-filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Samodej. barva"</string>
+    <string name="hue" msgid="6231252147971086030">"Odtenek"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Sence"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Svetli deli"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Krivulje"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinjeta"</string>
+    <string name="redeye" msgid="4508883127049472069">"Rdeče oči"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Risanje"</string>
+    <string name="straighten" msgid="26025591664983528">"Poravnanje"</string>
+    <string name="crop" msgid="5781263790107850771">"Obrezovanje"</string>
+    <string name="rotate" msgid="2796802553793795371">"Zavrti"</string>
+    <string name="mirror" msgid="5482518108154883096">"Zrcaljenje"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Brez"</string>
+    <string name="edge" msgid="7036064886242147551">"Robovi"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Zmanjšanje velikosti"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Rdeče"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Zeleno"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Modro"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Slog"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Velikost"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Barva"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Črte"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Flomaster"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Packe"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Izbriši"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Izberite barvo po meri"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Izberite barvo"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Izberite velikost"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"V redu"</string>
+</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
new file mode 100644
index 0000000..0d8a634
--- /dev/null
+++ b/res/values-sl/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerija"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Okvir slike"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videopredvajalnik"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Nalaganje videoposnetka ..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Nalaganje slike ..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Nalaganje računa…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Nadaljuj predvajanje videoposnetka"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Nadaljevanje predvajanja od %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Nadaljuj predvajanje"</string>
+    <string name="loading" msgid="7038208555304563571">"Prenos …"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Ni bilo mogoče naložiti"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Slike ni bilo mogoče naložiti"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Ni sličice"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Začni znova"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"V redu"</string>
+    <string name="ok" msgid="5296833083983263293">"V redu"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Dotaknite se obraza, če želite začeti."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Shranjevanje slike ..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Shranjevanje slike v album <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Obrezane slike ni bilo mogoče shraniti."</string>
+    <string name="crop_label" msgid="521114301871349328">"Obreži sliko"</string>
+    <string name="trim_label" msgid="274203231381209979">"Obreži videoposnetek"</string>
+    <string name="select_image" msgid="7841406150484742140">"Izberite fotogr."</string>
+    <string name="select_video" msgid="4859510992798615076">"Izberite videoposn."</string>
+    <string name="select_item" msgid="2816923896202086390">"Izberite element"</string>
+    <string name="select_album" msgid="1557063764849434077">"Izberite album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Izberite skupino"</string>
+    <string name="set_image" msgid="2331476809308010401">"Nastavi sliko kot"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nastavi ozadje"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Nastavljanje slike za ozadje ..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Slika za ozadje"</string>
+    <string name="delete" msgid="2839695998251824487">"Izbriši"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Želite izbrisati ta element?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Želite izbrisati te elemente?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potrdi"</string>
+    <string name="cancel" msgid="3637516880917356226">"Prekliči"</string>
+    <string name="share" msgid="3619042788254195341">"Skupna raba"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Deli panoramski posnetek z drugimi"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Deli z drugimi kot fotografijo"</string>
+    <string name="deleted" msgid="6795433049119073871">"Izbrisano"</string>
+    <string name="undo" msgid="2930873956446586313">"RAZVELJAVI"</string>
+    <string name="select_all" msgid="3403283025220282175">"Izberi vse"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Prekliči celoten izbor"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diaprojekcija"</string>
+    <string name="details" msgid="8415120088556445230">"Podrobnosti"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d od %2$d elementov:"</string>
+    <string name="close" msgid="5585646033158453043">"Zapri"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Preklopi na Fotoaparat"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Št. izbranih: %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Št. izbranih: %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Št. izbranih: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Št. izbranih: %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Št. izbranih: %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Št. izbranih: %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Št. izbranih: %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Št. izbranih: %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Št. izbranih: %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Pokaži na zemljevidu"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Zasukaj levo"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Zasukaj desno"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Ni mogoče najti elementa."</string>
+    <string name="edit" msgid="1502273844748580847">"Urejanje"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Obdelava zahtev za predpomnjenje"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Predpomnjenje ..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Obrezovanje"</string>
+    <string name="trim_action" msgid="703098114452883524">"Obreži"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Izklop zvoka"</string>
+    <string name="set_as" msgid="3636764710790507868">"Nastavi kot"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Zvoka ni mogoče izklopiti."</string>
+    <string name="video_err" msgid="7003051631792271009">"Videoposnetka ni mogoče predvajati."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Po lokaciji"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Po uri"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Po oznakah"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Po ljudeh"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Po albumu"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Glede na velikost"</string>
+    <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string>
+    <string name="no_location" msgid="4043624857489331676">"Ni lokacije"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Nekaterih ​​lokacij ni bilo mogoče določiti zaradi težav v omrežju."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Slik v tem albumu ni mogoče prenesti. Poskusite znova pozneje."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Samo slike"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Samo videoposnetki"</string>
+    <string name="show_all" msgid="6963292714584735149">"Slike in videoposnetki"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerija"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Ni fotografij."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Obrezana slika je shranjena v mapi <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Ni albumov."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Na voljo 0 slik oz. videoposnetkov."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Objave"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Omogoči dostop brez povezave"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Osveži"</string>
+    <string name="done" msgid="217672440064436595">"Končano"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d od %2$d elementov:"</string>
+    <string name="title" msgid="7622928349908052569">"Naslov"</string>
+    <string name="description" msgid="3016729318096557520">"Opis"</string>
+    <string name="time" msgid="1367953006052876956">"Ura"</string>
+    <string name="location" msgid="3432705876921618314">"Lokacija"</string>
+    <string name="path" msgid="4725740395885105824">"Pot"</string>
+    <string name="width" msgid="9215847239714321097">"Širina"</string>
+    <string name="height" msgid="3648885449443787772">"Višina"</string>
+    <string name="orientation" msgid="4958327983165245513">"Usmerjenost"</string>
+    <string name="duration" msgid="8160058911218541616">"Trajanje"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Vrsta razširitve MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Velikost datoteke"</string>
+    <string name="maker" msgid="7921835498034236197">"Izdelovalec"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Bliskavica"</string>
+    <string name="aperture" msgid="5920657630303915195">"Zaslonka"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Gorišč. razd."</string>
+    <string name="white_balance" msgid="1582509289994216078">"Izravnava beline"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Čas osvetlitve"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Ročno"</string>
+    <string name="auto" msgid="4296941368722892821">"Samod."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Blis. sprožena"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Brez bliskav."</string>
+    <string name="unknown" msgid="3506693015896912952">"Neznana"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Izvirnik"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Starinsko"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Takoj"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Pobeljeno"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Modra"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"ČB"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Ostrina"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Proces X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litografija"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Priprava albuma, da bo na voljo brez povezave."</item>
+    <item quantity="other" msgid="4948604338155959389">"Priprava albumov, da bodo na voljo brez povezave."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Element je shranjen lokalno in na voljo brez povezave."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Vsi albumi"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Lokalni albumi"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Naprave MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Albumi Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Prosto: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ali manj"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ali več"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> do <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Uvozi"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Uvoz je končan"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Uvoz ni uspel"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera je priključena."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera je izklopljena"</string>
+    <string name="click_import" msgid="6407959065464291972">"Tapnite tukaj, če želite uvoziti"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Izberite album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Naključno razporedi vse slike"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Izbira slike"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Izberite slike"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Diaprojekcija"</string>
+    <string name="albums" msgid="7320787705180057947">"Albumi"</string>
+    <string name="times" msgid="2023033894889499219">"Časi"</string>
+    <string name="locations" msgid="6649297994083130305">"Lokacije"</string>
+    <string name="people" msgid="4114003823747292747">"Osebe"</string>
+    <string name="tags" msgid="5539648765482935955">"Oznake"</string>
+    <string name="group_by" msgid="4308299657902209357">"Razvrsti po"</string>
+    <string name="settings" msgid="1534847740615665736">"Nastavitve"</string>
+    <string name="add_account" msgid="4271217504968243974">"Dodaj račun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparat"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Prenosi"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Urejene spletne fotografije"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Uvoženo"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Posnetek zaslona"</string>
+    <string name="help" msgid="7368960711153618354">"Pomoč"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Ni prostora za shranjevanje"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Na voljo ni nobena zunanja naprava za shranjevanje"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Pogled filmskega traku"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Mrežni pogled"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Celozaslonski pogled"</string>
+    <string name="trimming" msgid="9122385768369143997">"Obrezovanje"</string>
+    <string name="muting" msgid="5094925919589915324">"Izklop zvoka"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Počakajte"</string>
+    <string name="save_into" msgid="9155488424829609229">"Shranjevanje videa v album <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Obrezovanje ni mogoče: izvirni videoposnetek je prekratek"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Upodabljanje panorame"</string>
+    <string name="save" msgid="613976532235060516">"Shrani"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Pregledovanje vsebine ..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Št. pregledanih elementov: %1$d"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d pregledan element"</item>
+    <item quantity="other" msgid="3138021473860555499">"Št. pregledanih elementov: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Razvrščanje ..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Pregled končan"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Uvažanje ..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Ni na voljo vsebine za uvoz v to napravo."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Ni priključene naprave MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Napaka kamere"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Povezava s fotoaparatom ni mogoča."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Fotoaparat je onemogočen zaradi varnostnih pravilnikov."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Fotoaparat"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Kamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Počakajte ..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Pred uporabo fotoaparata vpnite pomnilnik USB."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Pred uporabo fotoaparata vstavite kartico SD."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Priprava pomnilnika USB ..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Priprava kartice SD ..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Do pomnilnika USB ni bilo mogoče dostopiti."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Do kartice SD ni bilo mogoče dostopiti."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"PREKLIČI"</string>
+    <string name="review_ok" msgid="1156261588693116433">"KONČANO"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Snemanje s časovnim zamikom"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Izberite fotoaparat"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Nazaj"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Pred"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Shrani lokacijo"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Odštevalnik"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 s"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d s"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Zvok med odštevanjem"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Izklopljeno"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Vklopljeno"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Kakovost videoposnetka"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Visoka"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Nizka"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Pospešena reprodukcija"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Nastavitve fotoaparata"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Nastavitve kamere"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Velikost slike"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 milijonov slikovnih pik"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 M sl. pik"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 M sl. pik"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 M sl. pik"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 M sl. pik"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 M sl. pik"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Način ostrenja"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Samodejno"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Neskončno"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Način bliskavice"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Samodejno"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Vključeno"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Izključeno"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Izravnava beline"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Samodejno"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Žareče"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Dnevna svetloba"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescenčno"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Oblačno"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scenski način"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Samodejno"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Dejanje"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Noč"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Sončni zahod"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Stranka"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Ni mogoče izbrati v načinu prizora."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Osvetlitev"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"V redu"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Na vašem pomnilniku USB zmanjkuje prostora. Spremenite nastavitev kakovosti ali izbrišite nekaj slik ali drugih datotek."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Na kartici SD zmanjkuje prostora. Spremenite nastavitev kakovosti ali izbrišite nekaj slik ali drugih datotek."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Dosežena je omejitev velikosti."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Prehitro"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Priprava panorame"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panorame ni bilo mogoče shraniti."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Snemanje panorame"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Čakanje na prejšnjo panoramo"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Shranjev. ..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Upodabljanje panorame"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Dotaknite se za ostrenje."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Učinki"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Brez"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Stiskanje"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Velike oči"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Velika usta"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Majhna usta"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Velik nos"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Majhne oči"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"V vesolju"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Sončni zahod"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Vaš video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Odložite napravo."\n"Za trenutek stopite iz vidnega polja."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Dotaknite se, če želite fotografirati med snemanjem."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Snemanje se je začelo."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Snemanje se je ustavilo."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Videoposnetek je onemogočen, ko so vklopljeni posebni učinki."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Počisti učinke"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"NORČAVI OBRAZI"</string>
+    <string name="effect_background" msgid="6579360207378171022">"OZADJE"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Gumb za fotografiranje"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Gumb za meni"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Najnovejša fotografija"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Preklop med fotoaparatom na sprednji in na hrbtni strani"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Izbirnik fotoaparata, videokamere ali panorame"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Več kontrolnikov nastavitev"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Zapri kontrolnike nastavitev"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Nadzor povečave/pomanjšave"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Pomanjšaj %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Povečaj %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Potrditveno polje %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Preklopi na fotoaparat"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Preklop na videoposnetek"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Preklop na panoramo"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Preklopi v novo panoramo"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Preklic pregleda"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Pregled opravljen"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Vnovični pregled posnetka"</string>
+    <string name="capital_on" msgid="5491353494964003567">"VKLOPLJENO"</string>
+    <string name="capital_off" msgid="7231052688467970897">"IZKLOPLJENO"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Izklopljeno"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekunda"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekundi"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekunde"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuta"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuti"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minute"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 ura"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 uri"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ure"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ur"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ur"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ur"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ur"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ur"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ur"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekunde"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minute"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ure"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Končano"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Nastavite časovni Interval"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Funkcija pospešene reprodukcije je izklopljena. Vklopite jo, da nastavite časovni interval."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Odštevalnik je izklopljen. Vklopite ga, če želite odštevanje pred fotografiranjem."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Nastavite trajanje v sekundah"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Odštevanje pred fotografiranjem"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Beleženje lokacije fotografije?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Fotografije in videoposnetke označite z lokacijami, na katerih so posneti."\n\n"Druge aplikacije lahko dostopajo do teh podatkov skupaj s shranjenimi slikami."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Ne, hvala"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Da"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Fotoaparat"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Išči"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotografije"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albumi"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotografija"</item>
+    <item quantity="other" msgid="3813306834113858135">"Št. fotografij: %1$d"</item>
+  </plurals>
+</resources>
diff --git a/res/values-sr/filtershow_strings.xml b/res/values-sr/filtershow_strings.xml
new file mode 100644
index 0000000..5b4d8f5
--- /dev/null
+++ b/res/values-sr/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Уређивач слика"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Није могуће учитати слику!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Подешавање позадине"</string>
+    <string name="original" msgid="3524493791230430897">"Оригинална"</string>
+    <string name="borders" msgid="2067345080568684614">"Ивице"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Опозови"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Понови"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Прикажи историју"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Сакриј историју"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Прикажи статус слике"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Сакриј статус слике"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Подешавања"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Постоје несачуване измене ове слике."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Желите ли да сачувате пре него што изађете?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Сачувај и изађи"</string>
+    <string name="exit" msgid="242642957038770113">"Изађи"</string>
+    <string name="history" msgid="455767361472692409">"Историја"</string>
+    <string name="reset" msgid="9013181350779592937">"Поново постави"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Примењени ефекти"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Упореди"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Примени"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Поново постави"</string>
+    <string name="aspect" msgid="4025244950820813059">"Размера"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ниједно"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Фиксно"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Мала планета"</string>
+    <string name="exposure" msgid="6526397045949374905">"Експозиција"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Оштрина"</string>
+    <string name="contrast" msgid="2310908487756769019">"Контраст"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Живост боја"</string>
+    <string name="saturation" msgid="7026791551032438585">"Засићење"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Црно-бели филтер"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Аутоматска боја"</string>
+    <string name="hue" msgid="6231252147971086030">"Нијанса"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Сенке"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Истицања"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Криве"</string>
+    <string name="vignette" msgid="934721068851885390">"Вињета"</string>
+    <string name="redeye" msgid="4508883127049472069">"Црвене очи"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Цртање"</string>
+    <string name="straighten" msgid="26025591664983528">"Исправљање"</string>
+    <string name="crop" msgid="5781263790107850771">"Опсецање"</string>
+    <string name="rotate" msgid="2796802553793795371">"Ротирај"</string>
+    <string name="mirror" msgid="5482518108154883096">"Огледало"</string>
+    <string name="negative" msgid="6998313764388022201">"Негатив"</string>
+    <string name="none" msgid="6633966646410296520">"Ниједно"</string>
+    <string name="edge" msgid="7036064886242147551">"Ивице"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Ворхол"</string>
+    <string name="downsample" msgid="3552938534146980104">"Смањи"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Црвена"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Зелена"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Плава"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Стил"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Величина"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Боја"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Линије"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Означивач"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Прскање"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Обриши"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Изабери прилагођену боју"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Избор боје"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Избор величине"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"Потврди"</string>
+</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
new file mode 100644
index 0000000..4b20924
--- /dev/null
+++ b/res/values-sr/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Галерија"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Оквир слике"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Видео плејер"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Учитавање видео снимка…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Учитавање слике…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Учитавање налога…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Наставак видео снимка"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Желите ли да наставите репродукцију од %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Настави репродукцију"</string>
+    <string name="loading" msgid="7038208555304563571">"Учитавање…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Учитавање није могуће"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Није могуће учитати слику"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Нема сличице"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Започни поново"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Потврди"</string>
+    <string name="ok" msgid="5296833083983263293">"Потврди"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Додирните неко лице за почетак."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Чување слике…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Чување слике у албум <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Није могуће сачувати опсечену слику."</string>
+    <string name="crop_label" msgid="521114301871349328">"Опсеци слику"</string>
+    <string name="trim_label" msgid="274203231381209979">"Скрати видео"</string>
+    <string name="select_image" msgid="7841406150484742140">"Избор фотографије"</string>
+    <string name="select_video" msgid="4859510992798615076">"Избор видео снимка"</string>
+    <string name="select_item" msgid="2816923896202086390">"Избор ставке"</string>
+    <string name="select_album" msgid="1557063764849434077">"Избор албума"</string>
+    <string name="select_group" msgid="6744208543323307114">"Избор групе"</string>
+    <string name="set_image" msgid="2331476809308010401">"Постављање слике као"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Подешавање позадине"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Подешавање позадине..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Позадина"</string>
+    <string name="delete" msgid="2839695998251824487">"Избриши"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Бришете изабрану ставку?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Бришете изабране ставке?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Потврди"</string>
+    <string name="cancel" msgid="3637516880917356226">"Откажи"</string>
+    <string name="share" msgid="3619042788254195341">"Дели"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Дели панораму"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Дели као слику"</string>
+    <string name="deleted" msgid="6795433049119073871">"Избрисана"</string>
+    <string name="undo" msgid="2930873956446586313">"ОПОЗОВИ"</string>
+    <string name="select_all" msgid="3403283025220282175">"Изабери све"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Опозови све изборе"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Пројекција слајдова"</string>
+    <string name="details" msgid="8415120088556445230">"Детаљи"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d од %2$d ставке(и):"</string>
+    <string name="close" msgid="5585646033158453043">"Затвори"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Пребацивање на Камеру"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Изабранo je %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Изабранo je %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Изабранo je %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Изабранo je %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Изабранo je %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Изабранo je %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Изабранo je %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Изабранo je %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Изабранo je %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Прикажи на мапи"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Ротирај улево"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Ротирај удесно"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Није могуће пронаћи ставку."</string>
+    <string name="edit" msgid="1502273844748580847">"Измени"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Обрада захтева за кеширање"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Кеширање..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Опсеци"</string>
+    <string name="trim_action" msgid="703098114452883524">"Скрати"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Искључи звук"</string>
+    <string name="set_as" msgid="3636764710790507868">"Постави као"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Звук не може да се искључи."</string>
+    <string name="video_err" msgid="7003051631792271009">"Није могуће пустити видео."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Према локацији"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Према времену"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Према ознакама"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Према особама"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Према албуму"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Према величини"</string>
+    <string name="untagged" msgid="7281481064509590402">"Није означено"</string>
+    <string name="no_location" msgid="4043624857489331676">"Без локације"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Није могуће идентификовати неке локације због проблема са мрежом."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Није могуће преузети фотографије у овом албуму. Покушајте поново касније."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Само слике"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Само видео снимци"</string>
+    <string name="show_all" msgid="6963292714584735149">"Слике и видео снимци"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Фото-галерија"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Нема фотографија."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Опсечена слика је сачувана у директоријуму <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Ниједан албум није доступан."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Доступно је 0 слика/видео снимака."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Постови"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Учини доступним ван мреже"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Освежи"</string>
+    <string name="done" msgid="217672440064436595">"Done"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d од %2$d ставке(и):"</string>
+    <string name="title" msgid="7622928349908052569">"Наслов"</string>
+    <string name="description" msgid="3016729318096557520">"Опис"</string>
+    <string name="time" msgid="1367953006052876956">"Време"</string>
+    <string name="location" msgid="3432705876921618314">"Локација"</string>
+    <string name="path" msgid="4725740395885105824">"Путања"</string>
+    <string name="width" msgid="9215847239714321097">"Ширина"</string>
+    <string name="height" msgid="3648885449443787772">"Висина"</string>
+    <string name="orientation" msgid="4958327983165245513">"Положај"</string>
+    <string name="duration" msgid="8160058911218541616">"Трајање"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME тип"</string>
+    <string name="file_size" msgid="8486169301588318915">"Величина датотеке"</string>
+    <string name="maker" msgid="7921835498034236197">"Аутор"</string>
+    <string name="model" msgid="8240207064064337366">"Модел"</string>
+    <string name="flash" msgid="2816779031261147723">"Блиц"</string>
+    <string name="aperture" msgid="5920657630303915195">"Отвор бленде"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Фокална дужина"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Баланс беле"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Време експозиције"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"мм"</string>
+    <string name="manual" msgid="6608905477477607865">"Ручно"</string>
+    <string name="auto" msgid="4296941368722892821">"Аутом."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Блиц је актив."</string>
+    <string name="flash_off" msgid="1445443413822680010">"Без блица"</string>
+    <string name="unknown" msgid="3506693015896912952">"Непознато"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Оригинална"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Плаво"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Црно-бело"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Испрано"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Литографски"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Омогућавање доступности албума ван мреже"</item>
+    <item quantity="other" msgid="4948604338155959389">"Омогућавање доступности албума ван мреже."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ова ставка је локално сачувана и доступна ван мреже."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Сви албуми"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Локални албуми"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP уређаји"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa албуми"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> слободно"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> или мање"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> или више"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> до <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Увези"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Увоз је довршен"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Увоз није успео"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Камера је прикључена."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Камера је искључена."</string>
+    <string name="click_import" msgid="6407959065464291972">"Додирните овде за увоз"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Изабери албум"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Пусти све слике насумично"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Изабери слику"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Избор слика"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Пројекција слајдова"</string>
+    <string name="albums" msgid="7320787705180057947">"Албуми"</string>
+    <string name="times" msgid="2023033894889499219">"Пута"</string>
+    <string name="locations" msgid="6649297994083130305">"Локације"</string>
+    <string name="people" msgid="4114003823747292747">"Особе"</string>
+    <string name="tags" msgid="5539648765482935955">"Ознаке"</string>
+    <string name="group_by" msgid="4308299657902209357">"Групиши према"</string>
+    <string name="settings" msgid="1534847740615665736">"Подешавања"</string>
+    <string name="add_account" msgid="4271217504968243974">"Додавање налога"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Преузето"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Измењене слике на мрежи"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Увезено"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Снимак екрана"</string>
+    <string name="help" msgid="7368960711153618354">"Помоћ"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Нема меморије"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Спољна меморија није доступна"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Приказ филмске траке"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Приказ мреже"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Приказ целог екрана"</string>
+    <string name="trimming" msgid="9122385768369143997">"Скраћивање"</string>
+    <string name="muting" msgid="5094925919589915324">"Искључивање звука"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Сачекајте"</string>
+    <string name="save_into" msgid="9155488424829609229">"Чување видео снимка у <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Скраћивање није могуће: циљни видео је прекратак"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Приказивање панораме"</string>
+    <string name="save" msgid="613976532235060516">"Сачувај"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Скенирање садржаја..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Скенирано је %1$d ставки"</item>
+    <item quantity="one" msgid="4340019444460561648">"Скенирана је %1$d ставка"</item>
+    <item quantity="other" msgid="3138021473860555499">"Скенирано је %1$d ставки"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Сортирање..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Скенирање је завршено"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Увоз..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Нема доступног садржаја за увоз на овом уређају."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"MTP уређај није повезан"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Грешка камере"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Није могуће повезати се са камером."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Камера је онемогућена због смерница за безбедност."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Камера"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Камкордер"</string>
+    <string name="wait" msgid="8600187532323801552">"Сачекајте…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Прикључите USB меморију пре коришћења камере."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Уметните SD картицу пре коришћења камере."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Припрема USB меморије…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Припремање SD картице…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Није било могуће приступити USB меморији."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Није било могуће приступити SD картици."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ОТКАЖИ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ГОТОВО"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Снимањe у дужем интервалу"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Избор камере"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Назад"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Напред"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Чување локац."</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Тајмер за одбројавање"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 секунда"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d сек"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Звучно одбројавање"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Искључено"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Укључено"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Квалитет видео снимка"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Висок"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Низак"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Снимање у дужем интервалу"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Подешавања камере"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Подешавања камкордера"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Величина слике"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 мегапиксела"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 мегапиксел"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Режим фокуса"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Аутоматски"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Бесконачност"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Макро"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Режим блица"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Аутоматски"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Укључено"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Искључено"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Баланс беле боје"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Аутоматски"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Усијано"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Дневна светлост"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Флуоресцентно"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Облачно"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Режим сцене"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Аутоматски"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Покрет"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Ноћ"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Залазак сунца"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Журка"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Ово не може да се изабере у режиму сцене."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Видљивост"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"Потврди"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"На вашој USB меморији понестаје места. Промените подешавања квалитета или избришите неке слике или друге датотеке."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"На вашој SD картици понестаје места. Промените поставке квалитета или избришите неке слике или друге датотеке."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Достигнуто је ограничење величине."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Пребрзо"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Припремање панораме"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Није могуће сачувати панораму."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Панорама"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Снимање панораме"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Чека се претходна панорама"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Чување..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Приказивање панораме"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Додирните за фокусирање."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Ефекти"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ништа"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Стисни"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Велике очи"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Велика уста"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Мала уста"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Велики нос"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Мале очи"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"У свемиру"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Залазак сунца"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Ваш видео"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Спустите уређај."\n"Изађите из кадра на тренутак."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Додирните да бисте направили фотографију током снимања."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Снимање видео садржаја је започето."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Снимање видео садржаја је заустављено."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Видео снимак је онемогућен када су специјални ефекти укључени."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Обриши ефекте"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"СМЕШНА ЛИЦА"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ПОЗАДИНА"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Дугме затварача"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Дугме менија"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Најновија фотографија"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Промена предње и задње камере"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Бирач камере, видео снимка или панораме"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Још контрола подешавања"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Затвори контроле подешавања"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Контрола зума"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Смањи %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Повећај %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Поље за потврду %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Пребаци на фотографију"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Пребаци на видео"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Пребаци на панораму"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Пребаци на нову панораму"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Откажи у режиму прегледа"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Довршено у режиму прегледа"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Поново сними за преглед"</string>
+    <string name="capital_on" msgid="5491353494964003567">"УКЉУЧEНO"</string>
+    <string name="capital_off" msgid="7231052688467970897">"ИСКЉУЧEНO"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Искључено"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 секунда"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 секунде"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 секунде"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 секунде"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 секунде"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 минут"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 минута"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 сат"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 сата"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 сата"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 сата"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 сати"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 сата"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"секунди"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"минута"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"сати"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Готово"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Подеси временски интервал"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Функција снимања у дужем интервалу је искључена. Укључите је да бисте подесили временски интервал."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Тајмер за одбројавање је искључен. Укључите га за одбројавање пре снимања слике."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Подеси трајање у секундама"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Одбројавање за снимање слике"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Желите ли да запамтите локације слика?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Додајте сликама и видео снимцима ознаке са местима где су снимљени."\n\n"Друге апликације могу да приступе овим информацијама заједно са сачуваним сликама."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Не, хвала"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Да"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Камера"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Претражи"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Слике"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Албуми"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d слика"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d слика"</item>
+  </plurals>
+</resources>
diff --git a/res/values-sv/filtershow_strings.xml b/res/values-sv/filtershow_strings.xml
new file mode 100644
index 0000000..36e235b
--- /dev/null
+++ b/res/values-sv/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fotoredigerare"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Det går inte att läsa in bilden."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Bakgrund anges"</string>
+    <string name="original" msgid="3524493791230430897">"Original"</string>
+    <string name="borders" msgid="2067345080568684614">"Ramar"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Ångra"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Gör om"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Visa historik"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Dölj historik"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Visa bildläge"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Dölj bildläge"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Inställningar"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Det finns ändringar i bilden som inte har sparats."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Vill du spara innan du avslutar?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Spara och avsluta"</string>
+    <string name="exit" msgid="242642957038770113">"Avsluta"</string>
+    <string name="history" msgid="455767361472692409">"Historik"</string>
+    <string name="reset" msgid="9013181350779592937">"Återställ"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Effekter som används"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Jämför"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Använd"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Återställ"</string>
+    <string name="aspect" msgid="4025244950820813059">"Format"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Ingen"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Fast"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exponering"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Skärpa"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Färgmättnad"</string>
+    <string name="saturation" msgid="7026791551032438585">"Mättnad"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Svartvitt filt."</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autofärg"</string>
+    <string name="hue" msgid="6231252147971086030">"Nyans"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Skuggor"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Högdagrar"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Kurvor"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignette"</string>
+    <string name="redeye" msgid="4508883127049472069">"Röda ögon"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Rita"</string>
+    <string name="straighten" msgid="26025591664983528">"Räta ut"</string>
+    <string name="crop" msgid="5781263790107850771">"Beskär"</string>
+    <string name="rotate" msgid="2796802553793795371">"Rotera"</string>
+    <string name="mirror" msgid="5482518108154883096">"Spegel"</string>
+    <string name="negative" msgid="6998313764388022201">"Negativ"</string>
+    <string name="none" msgid="6633966646410296520">"Inga"</string>
+    <string name="edge" msgid="7036064886242147551">"Kanter"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Nedsampla"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Röd"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Grön"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Blå"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Storlek"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Färg"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Linjer"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Märkpenna"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Stänk"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Rensa"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Välj anpassad färg"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Välj färg"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Välj storlek"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
new file mode 100644
index 0000000..6b73bed
--- /dev/null
+++ b/res/values-sv/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleri"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Bildram"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Videospelare"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Läser in video…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Läser in bild..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Läses kontot in…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Fortsätt spela videon"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Fortsätt spela upp från %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsätt spela upp"</string>
+    <string name="loading" msgid="7038208555304563571">"Läser in..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Kunde inte läsas in"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Det gick inte att läsa in bilden"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniatyrbild"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Börja om"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Tryck på ett ansikte när du vill börja."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Sparar bild…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Bilden sparas i <xliff:g id="ALBUM_NAME">%1$s</xliff:g> ..."</string>
+    <string name="save_error" msgid="6857408774183654970">"Det gick inte att spara den beskurna bilden."</string>
+    <string name="crop_label" msgid="521114301871349328">"Beskär bild"</string>
+    <string name="trim_label" msgid="274203231381209979">"Beskär videon"</string>
+    <string name="select_image" msgid="7841406150484742140">"Välj en bild"</string>
+    <string name="select_video" msgid="4859510992798615076">"Välj ett videoklipp"</string>
+    <string name="select_item" msgid="2816923896202086390">"Välj ett objekt"</string>
+    <string name="select_album" msgid="1557063764849434077">"Välj ett album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Välj en grupp"</string>
+    <string name="set_image" msgid="2331476809308010401">"Använd bild som"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Välj bakgrund"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Bakgrund ställs in ..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrund"</string>
+    <string name="delete" msgid="2839695998251824487">"Ta bort"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ta bort markerat objekt?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Radera markerade objekt?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bekräfta"</string>
+    <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
+    <string name="share" msgid="3619042788254195341">"Dela"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Dela panoramabild"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Dela som foto"</string>
+    <string name="deleted" msgid="6795433049119073871">"Borttagen"</string>
+    <string name="undo" msgid="2930873956446586313">"ÅNGRA"</string>
+    <string name="select_all" msgid="3403283025220282175">"Markera alla"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Avmarkera alla"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Bildspel"</string>
+    <string name="details" msgid="8415120088556445230">"Information"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d av %2$d objekt:"</string>
+    <string name="close" msgid="5585646033158453043">"Stäng"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Byt till kamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d har markerats"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d har markerats"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d har markerats"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d har markerats"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d har markerats"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d har markerats"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d har markerats"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d har markerats"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d har markerats"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Visa på karta"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Rotera åt vänster"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Rotera åt höger"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Det gick inte att hitta objektet."</string>
+    <string name="edit" msgid="1502273844748580847">"Redigera"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Begäran om cachelagring bearbetas"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Cachelagrar ..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Beskär"</string>
+    <string name="trim_action" msgid="703098114452883524">"Beskär"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Ljud av"</string>
+    <string name="set_as" msgid="3636764710790507868">"Använd som"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Ljud av misslyckades."</string>
+    <string name="video_err" msgid="7003051631792271009">"Det går inte att spela upp videon."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Efter plats"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Efter tid"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Efter taggar"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Efter personer"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Efter album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Efter storlek"</string>
+    <string name="untagged" msgid="7281481064509590402">"Saknar etikett"</string>
+    <string name="no_location" msgid="4043624857489331676">"Ingen plats"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Det gick inte att identifiera vissa platser på grund av nätverksproblem."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Det gick inte att hämta bilderna i albumet. Försök igen senare."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Endast bilder"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Endast video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Bilder och videor"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotogalleri"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Inga foton."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Den beskurna bilden har sparats i <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Det finns inga album."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Det finns inga bilder/videor."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Inlägg"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Gör tillgängliga offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Uppdatera"</string>
+    <string name="done" msgid="217672440064436595">"Klar"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d av %2$d objekt:"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="description" msgid="3016729318096557520">"Beskrivning"</string>
+    <string name="time" msgid="1367953006052876956">"Tid"</string>
+    <string name="location" msgid="3432705876921618314">"Plats"</string>
+    <string name="path" msgid="4725740395885105824">"Sökväg"</string>
+    <string name="width" msgid="9215847239714321097">"Bredd"</string>
+    <string name="height" msgid="3648885449443787772">"Höjd"</string>
+    <string name="orientation" msgid="4958327983165245513">"Riktning"</string>
+    <string name="duration" msgid="8160058911218541616">"Varaktighet"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME-typ"</string>
+    <string name="file_size" msgid="8486169301588318915">"Filstorlek"</string>
+    <string name="maker" msgid="7921835498034236197">"Upphovsman"</string>
+    <string name="model" msgid="8240207064064337366">"Modell"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Bländare"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Fokuslängd"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Vitbalans"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Exponeringstid"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manuell"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Blixt utlöst"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Ingen blixt"</string>
+    <string name="unknown" msgid="3506693015896912952">"Okänt"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Original"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Blue"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"B/W"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Gör album tillgängligt offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Gör album tillgängliga offline."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Objektet lagras lokalt och är tillgängligt offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Alla album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Lokala album"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP-enheter"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa-album"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ledigt"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> eller mindre"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> eller mer"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> till <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Importera"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Importen slutförd"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Importen misslyckades"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kameran är ansluten."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kameran är inte ansluten."</string>
+    <string name="click_import" msgid="6407959065464291972">"Tryck här om du vill importera"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Välj ett album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Blanda alla bilder"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Välj en bild"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Välj bilder"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Bildspel"</string>
+    <string name="albums" msgid="7320787705180057947">"Album"</string>
+    <string name="times" msgid="2023033894889499219">"Tider"</string>
+    <string name="locations" msgid="6649297994083130305">"Platser"</string>
+    <string name="people" msgid="4114003823747292747">"Personer"</string>
+    <string name="tags" msgid="5539648765482935955">"Taggar"</string>
+    <string name="group_by" msgid="4308299657902209357">"Ordna efter"</string>
+    <string name="settings" msgid="1534847740615665736">"Inställningar"</string>
+    <string name="add_account" msgid="4271217504968243974">"Lägg till konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Hämtat"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Redigerade onlinefoton"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importerat"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skärmbild"</string>
+    <string name="help" msgid="7368960711153618354">"Hjälp"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Ingen lagringsenhet"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Inget externt lagringsutrymme är tillgängligt"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmremsevy"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Rutnätsvy"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Helskärmsvy"</string>
+    <string name="trimming" msgid="9122385768369143997">"Beskärning"</string>
+    <string name="muting" msgid="5094925919589915324">"Stänger av ljud"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Vänta"</string>
+    <string name="save_into" msgid="9155488424829609229">"Sparar videon i <xliff:g id="ALBUM_NAME">%1$s</xliff:g> ..."</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Det gick inte att beskära videon. Målvideon är för kort"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panoramabild hämtas"</string>
+    <string name="save" msgid="613976532235060516">"Spara"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Skannar innehåll ..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d objekt har skannats"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d objekt har skannats"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d objekt har skannats"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sorterar ..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Skannat"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Importerar ..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Det finns inget tillgängligt innehåll att importera på den här enheten."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Det finns ingen ansluten MTP-enhet"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kamerafel"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Det går inte att ansluta till kameran."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kameran har inaktiverats på grund av gällande säkerhetspolicyer."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Videokamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Vänta…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Sätt i USB-lagringsenheten innan du använder kameran."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Sätt i ett SD-kort innan du använder kameran."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB-lagring förbereds…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Förbereder SD-kort…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Det gick inte att komma åt USB-enheten."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Det gick inte att öppna SD-kortet."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"AVBRYT"</string>
+    <string name="review_ok" msgid="1156261588693116433">"KLAR"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Intervallinspelning"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Välj kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Bakre"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Främre"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Spara plats"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Nedräkningstimer"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 sekund"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d sekunder"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Ljudsignal"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Inaktiverad"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Aktiverad"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Videokvalitet"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Hög"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Låg"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Intervallinspelning"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kamerainställningar"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Videokamerainställningar"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Bildstorlek"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 megapixlar"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 megapixlar"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 megapixlar"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 megapixlar"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 megapixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Fokusläge"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Automatiskt"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Oändligt"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Blixtläge"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Automatiskt"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"På"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Av"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Vitbalans"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Automatiskt"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Självlysande"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Dagsljus"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescerande"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Molnigt"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scenläge"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Automatiskt"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Action"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Natt"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Solnedgång"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Fest"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Inte valbart i scenläget."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Exponering"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Din USB-lagringsenhet börjar bli full. Ändra kvalitetsinställningen eller ta bort några bilder eller andra filer."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Den delade lagringsenheten börjar bli full. Ändra inställningen för kvalitet eller ta bort några bilder eller andra filer."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Storleksgränsen nådd."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"För snabbt"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Förbereder panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Det gick inte att spara panoramabild."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Ta panoramafoto"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Väntar på föregående panoramabild"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Sparar ..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panoramabild hämtas"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Fokusera genom att trycka."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Effekter"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Ingen"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Hopklämning"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Stora ögon"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Stor mun"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Liten mun"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Stor näsa"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Små ögon"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"I rymden"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Solnedgång"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Din video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Lägg ned enheten"\n"Ställ dig utom synhåll för ett ögonblick."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Tryck om du vill ta ett foto medan du spelar in."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Videoinspelningen har börjat."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Videoinspelningen har stoppats."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Ögonblicksbilden av en video inaktiveras när specialeffekter är aktiverade."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Ta bort effekter"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"ROLIGA GRIMASER"</string>
+    <string name="effect_background" msgid="6579360207378171022">"BAKGRUND"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Slutarknappen"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menyknapp"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Senaste fotot"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Kameraläge framåt/bakåt"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Väljare för kamera, video och panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Fler inställningskontroller"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Stäng inställningskontrollerna"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zoomkontroll"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Minska %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Öka %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Markeringsrutan %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Byt till kameraläge"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Byt till video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Byt till panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Byt till det nya panoramaläget"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Avbryt"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Klar"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Omtagning"</string>
+    <string name="capital_on" msgid="5491353494964003567">"PÅ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"AV"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Av"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 sekund"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 sekunder"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minut"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 minuter"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 timme"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 timmar"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 timmar"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekunder"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"minuter"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"timmar"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Klar"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Ange tidssintervall"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Intervallinspelningsfunktionen är avstängd. Aktivera den om du vill ange tidsintervall."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Nedräkningstimern är inaktiverad. Aktivera den om du vill att kameran ska räkna ned innan en bild tas."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Ange längden i sekunder"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Nedräkning innan fotot tas"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Vill du spara platser för foton?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Tagga dina foton och videor med de platser där de tas eller spelas in."\n\n"Andra appar kan få åtkomst till den här informationen tillsammans med dina sparade bilder."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Nej tack"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ja"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Sök"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Foton"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Album"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d foto"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d foton"</item>
+  </plurals>
+</resources>
diff --git a/res/values-sw/filtershow_strings.xml b/res/values-sw/filtershow_strings.xml
new file mode 100644
index 0000000..11320cb
--- /dev/null
+++ b/res/values-sw/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Uhariri Picha"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Haiwezi kupakia picha!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Inaweka mandhari"</string>
+    <string name="original" msgid="3524493791230430897">"Asili"</string>
+    <string name="borders" msgid="2067345080568684614">"Kingo"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Tendua"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Rudia"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Onyesha Historia"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ficha Historia"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Onyesha Hali ya Picha"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ficha Hali ya Picha"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Mipangilio"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Kuna mabadiliko ambayo hayajahifadhiwa ya picha hii."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Je, unataka kuhifadhi kabla hujaondoka?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Hifadhi na Uondoke"</string>
+    <string name="exit" msgid="242642957038770113">"Ondoka"</string>
+    <string name="history" msgid="455767361472692409">"Historia"</string>
+    <string name="reset" msgid="9013181350779592937">"Weka upya"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Madoido Yanayotumiwa"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Linganisha"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Tekeleza"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Weka upya"</string>
+    <string name="aspect" msgid="4025244950820813059">"Uwiano"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Bila"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Imerekebishwa"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Sayari Ndogo zaidi"</string>
+    <string name="exposure" msgid="6526397045949374905">"Kiasi cha mwangaza"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ung\'aavu"</string>
+    <string name="contrast" msgid="2310908487756769019">"Ulinganuzi"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Ya kusisimua"</string>
+    <string name="saturation" msgid="7026791551032438585">"Uloweshaji"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Kichujio cha Nyeusi na Nyeupe"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Rangi otomatiki"</string>
+    <string name="hue" msgid="6231252147971086030">"Rangi"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Vivuli"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Sehemu zenye ung\'avu zaidi"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Pindo"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignete"</string>
+    <string name="redeye" msgid="4508883127049472069">"Jicho Jekundu"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Chora"</string>
+    <string name="straighten" msgid="26025591664983528">"Nyoosha"</string>
+    <string name="crop" msgid="5781263790107850771">"Puna"</string>
+    <string name="rotate" msgid="2796802553793795371">"Zungusha"</string>
+    <string name="mirror" msgid="5482518108154883096">"Kioo"</string>
+    <string name="negative" msgid="6998313764388022201">"Hasi"</string>
+    <string name="none" msgid="6633966646410296520">"Umekamilisha"</string>
+    <string name="edge" msgid="7036064886242147551">"Pambizoni"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Sampuli ndogo"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Nyekundu"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Kijani"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Samawati"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Mtindo"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Ukubwa"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Rangi"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Mistari"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Kialamisho"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Tapanya"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Futa"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Chagua rangi maalum"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Chagua Rangi"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Chagua Ukubwa"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"SAWA"</string>
+</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
new file mode 100644
index 0000000..5be0128
--- /dev/null
+++ b/res/values-sw/strings.xml
@@ -0,0 +1,405 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Matunzio"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Fremu ya picha"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Kicheza video"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Inapakia video..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Inapakia picha…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Inapakia akaunti..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Endelea na video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Endelea kucheza kutoka %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Endelea kucheza"</string>
+    <string name="loading" msgid="7038208555304563571">"Inapakia…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Imeshindwa kupakia."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Haikuweza kupakia picha"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Hakuna kijipicha"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Anza tena"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Sawa"</string>
+    <string name="ok" msgid="5296833083983263293">"Sawa"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Gusa sura ili kuanza."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Inahifadhi picha…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Inahifadhi picha kwenye <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Haiwezi kuhifadhi picha iliyopogolewa."</string>
+    <string name="crop_label" msgid="521114301871349328">"Kata picha"</string>
+    <string name="trim_label" msgid="274203231381209979">"Punguza video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Chagua picha"</string>
+    <string name="select_video" msgid="4859510992798615076">"Chagua video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Chagua kipengee"</string>
+    <string name="select_album" msgid="1557063764849434077">"Chagua albamu"</string>
+    <string name="select_group" msgid="6744208543323307114">"Chagua kikundi"</string>
+    <string name="set_image" msgid="2331476809308010401">"Weka picha kama"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Weka mandhari"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Inaweka karatasi ya ukuta..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Mandhari"</string>
+    <string name="delete" msgid="2839695998251824487">"Futa"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Kipengee kilichoteuliwa kifutwe?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vipengee vilivyoteuliwa vifutwe?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Thibitisha"</string>
+    <string name="cancel" msgid="3637516880917356226">"Ghairi"</string>
+    <string name="share" msgid="3619042788254195341">"Shiriki"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Shiriki panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Shiriki kama picha"</string>
+    <string name="deleted" msgid="6795433049119073871">"Imefutwa"</string>
+    <string name="undo" msgid="2930873956446586313">"TENDUA"</string>
+    <string name="select_all" msgid="3403283025220282175">"Chagua zote"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Ghairi uteuzi kwa zote"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Onyesho la slaidi"</string>
+    <string name="details" msgid="8415120088556445230">"Maelezo"</string>
+    <string name="details_title" msgid="2611396603977441273">"Vipengee %1$d kati ya %2$d:"</string>
+    <string name="close" msgid="5585646033158453043">"Funga"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Badili  kwa Kamera"</string>
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_items_selected:zero (2142579311530586258) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_items_selected:one (2478365152745637768) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_items_selected:other (754722656147810487) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_albums_selected:zero (749292746814788132) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_albums_selected:one (6184377003099987825) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_albums_selected:other (53105607141906130) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_groups_selected:one (5030162638216034260) -->
+    <!-- String.format failed for translation -->
+    <!-- no translation found for number_of_groups_selected:other (3512041363942842738) -->
+    <string name="show_on_map" msgid="6157544221201750980">"Onyesha kwenye ramani"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Zungusha kushoto"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Zungusha kulia"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Hakuweza kupata kipengee."</string>
+    <string name="edit" msgid="1502273844748580847">"Hariri"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Maombi ya kuakibisha michakato"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Inaakibisha..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Punguza"</string>
+    <string name="trim_action" msgid="703098114452883524">"Punguza"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Zima sauti"</string>
+    <string name="set_as" msgid="3636764710790507868">"Weka kama"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Imeshindwa kuzima sauti ya video."</string>
+    <string name="video_err" msgid="7003051631792271009">"Video haiwezi kuchezwa."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Kwa mahali"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Kwa saa"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Kwa lebo"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Kwa watu"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Kwa albamu"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Kwa ukubwa"</string>
+    <string name="untagged" msgid="7281481064509590402">"Ondoa lebo"</string>
+    <string name="no_location" msgid="4043624857489331676">"Hakuna Mahali"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Baadhi ya maeneo haikuweza kutambuliwa kutokana na matatizo ya mtandao."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Haikuwezi kupakua picha zilizo kwenye albamu hii. Jaribu tena baadaye."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Picha tu"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Video tu"</string>
+    <string name="show_all" msgid="6963292714584735149">"Picha &amp; video"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Matunzio ya picha"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Hakuna picha."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Picha iliyopunguzwa imehifadhiwa kwa <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Hakuna albamu zinazopatikana."</string>
+    <string name="empty_album" msgid="4542880442593595494">"picha / video zilizopo 0."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Zilizowekwa"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Fanya ipatikane nje ya mtandao"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Onyesha upya"</string>
+    <string name="done" msgid="217672440064436595">"Kwisha"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"Vipengee %1$d kati ya %2$d:"</string>
+    <string name="title" msgid="7622928349908052569">"Kichwa"</string>
+    <string name="description" msgid="3016729318096557520">"Maelezo"</string>
+    <string name="time" msgid="1367953006052876956">"Saa"</string>
+    <string name="location" msgid="3432705876921618314">"Mahali"</string>
+    <string name="path" msgid="4725740395885105824">"Njia"</string>
+    <string name="width" msgid="9215847239714321097">"Upana"</string>
+    <string name="height" msgid="3648885449443787772">"Urefu"</string>
+    <string name="orientation" msgid="4958327983165245513">"Uelekezo"</string>
+    <string name="duration" msgid="8160058911218541616">"Muda"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Aina ya MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Ukubwa wa faili"</string>
+    <string name="maker" msgid="7921835498034236197">"Mtengenezaji"</string>
+    <string name="model" msgid="8240207064064337366">"Mtindo"</string>
+    <string name="flash" msgid="2816779031261147723">"Mmweko"</string>
+    <string name="aperture" msgid="5920657630303915195">"Kitundu cha kamera"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Urefu wa Lengo"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Usawazishaji wa weupe"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Muda ambao kilango cha kamera kinakaa wazi"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Mwongozo"</string>
+    <string name="auto" msgid="4296941368722892821">"Kiotomatiki"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Mmweko umeanzishwa"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Hakuna flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Usiojulikana"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Asili"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Ukale"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Papo hapo"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Pausha"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Samawati"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Nyeusi na Nyeupe"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Panchi"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Mchakato X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Lati"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Albamu inafanywa ipatikane nje ya mtandao"</item>
+    <item quantity="other" msgid="4948604338155959389">"Albamu zinafanywa zipatikane nje ya mtandao"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Imehifadhiwa kwenye kifaa hiki na inapatikana nje ya mtandao."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Albamu zote"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Albamu za kawaida"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Vifa vya MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Albumu za Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> hailipishwi"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> au chini"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> au juu"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> kwa <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Ingiza"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Kuingiza Kumekamilika"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Haijafaulu kuleta"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera imeunganishwa."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera imetenganishwa."</string>
+    <string name="click_import" msgid="6407959065464291972">"Gusa hapa kuleta"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Chagua albamu"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Changanya picha zote"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Chagua picha"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Chagua picha"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Onyesho la slaidi"</string>
+    <string name="albums" msgid="7320787705180057947">"Albamu"</string>
+    <string name="times" msgid="2023033894889499219">"Nyakati"</string>
+    <string name="locations" msgid="6649297994083130305">"Mahali"</string>
+    <string name="people" msgid="4114003823747292747">"Watu"</string>
+    <string name="tags" msgid="5539648765482935955">"Lebo"</string>
+    <string name="group_by" msgid="4308299657902209357">"Panga kwa kikundi na"</string>
+    <string name="settings" msgid="1534847740615665736">"Mipangilio"</string>
+    <string name="add_account" msgid="4271217504968243974">"Ongeza akaunti"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Pakua"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Picha Zilizohaririwa Mtandaoni"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Zilizoletwa"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Picha ya skrini"</string>
+    <string name="help" msgid="7368960711153618354">"Usaidizi"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Hakuna Hifadhi"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Hakuna hifadhi ya nje inayopatikana"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Mwonekano wa utepe wa filamu"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Mwonekano wa gridi"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Mwonekano wa skrini nzima"</string>
+    <string name="trimming" msgid="9122385768369143997">"Inapunguza"</string>
+    <string name="muting" msgid="5094925919589915324">"Inanyamazisha"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Tafadhali subiri"</string>
+    <string name="save_into" msgid="9155488424829609229">"Inahifadhi video kwenye <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Haiwezi kupunguza : video iliyolengwa ni fupi sana"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Inaonyesha panorama"</string>
+    <string name="save" msgid="613976532235060516">"Hifadhi"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Maudhui yanachanganuliwa..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d vipengee vimechanganuliwa"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d kipengee kimechanganuliwa"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d vipengee vimechanganuliwa"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Inapanga..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Uchanganuzi umefanyika"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Inaingiza..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Hakuna maudhui yanayopatikana kuingiza kwenye kifaa hiki."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Hakuna kifaa cha MTP kilichounganishwa"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Hitilafu ya kamera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Haiwezi kuungana na kamera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kamera imelemazwa kwa sababu ya sera za usalama."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Kamkoda"</string>
+    <string name="wait" msgid="8600187532323801552">"Tafadhali subiri…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Pachika hifadhi ya USB kabla ya kutumia kamera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Chomeka kadi ya SD kabla ya kutumia kamera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Inaandaa hifadhi ya USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Inatayarisha kadi ya SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Haikuweza kufikia hifadhi ya USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Haikuweza kufikia kadi ya SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"GHAIRI"</string>
+    <string name="review_ok" msgid="1156261588693116433">"IMEFANYWA"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Rekodi ya kupita kwa muda"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Chagua kamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Nyuma"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Mbele"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Mahali pa hifadhi"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Kipima muda cha kuhesabu"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"Sekunde 1"</item>
+    <item quantity="other" msgid="6455381617076792481">"Sekunde %d"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Toa mlio wakati wa kuhesabu"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Imezimwa"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Imewashwa"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Ubora wa video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Juu"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Chini"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Kupita kwa muda"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Mipangilio ya kamera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Mipangilio ya kamkoda"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Ukubwa wa picha"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"Pikseli 8M"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"Pikseli 5M"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"Pikseli 3M"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"Pikseli 2M"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"Pikseli 1.3M"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"Pikseli 1M"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Hali ya kulenga"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Otomatiki"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Pasipo mwisho"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Hali ya mweka"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Otomatiki"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Washa"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Zima"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Usawazishaji wa weupe"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Kioto"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"King\'arishaji"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Mwangaza wa mchana"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Kiakisi mwanga"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Mawingu"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"gumzo ya mandhari"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Otomatiki"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Kitendo"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Usiku"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Machweo"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Karamu"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Haiwezi kuchaguliwa ikiwa katika hali ya mandhari."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Kiasi cha mwangaza"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"SAWA"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Hifadhi yako ya USB inaishiwa na nafasi. Badilisha mipangilio ya ubora au futa baadhi ya picha au faili."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Nafasi ya kadi yako ya SD inaelekea kuisha. Badlisha mpangilio wa ubora au futa baadhi ya picha au faili zingine."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Upeo wa ukubwa umefikiwa."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Haraka mno"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Panorama inaandaliwa"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Imeshindwa kuhifadhi picha ya panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Inapiga picha ya panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Inasubiri picha ya awali ya panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Inahifadhi…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Inaonyesha panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Gusa ili kulenga."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Athari"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Hamna"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Finya"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Macho makubwa"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Mdomo mkubwa"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Mdomo mdogo"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Pua kubwa"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Macho Madogo"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Kwenya nafasi"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Jua kutua"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Video yako"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Weka kifaa chako chini."\n"Toka nje ya muonekano kwa muda."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Gusa ili upige picha wakati unarekodi."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Kurekodi kwa video kumeanza."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Kurekodi kwa video kumekomeshwa."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Picha ya video imelemazwa wakati athari maalum imewashwa."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Athari Wazi"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"SURA PUMBAVU"</string>
+    <string name="effect_background" msgid="6579360207378171022">"USULI"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Kitufe cha kilango cha kamera"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Kitufe cha menyu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Picha ya hivi karibuni"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Swichi ya mbele na nyuma ya kamera"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kichagua kamera, video au Picha ya mandhari"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Vidhibiti zaidi vya mpangilio"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Funga vidhibiti mipangilio"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Dhibiti kukuza"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Punguza %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Ongeza %1$s"</string>
+    <!-- String.format failed for translation -->
+    <!-- no translation found for accessibility_check_box (7317447218256584181) -->
+    <skip />
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Badilisha hadi kwa picha"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Badilisha hadi kwa video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Badilisha hadi panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Badili hadi kwenye panorama mpya"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Ukaguzi umeghairiwa"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Ukaguzi umekamilika"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Kagua ukaguzi"</string>
+    <string name="capital_on" msgid="5491353494964003567">"IMEWASHWA"</string>
+    <string name="capital_off" msgid="7231052688467970897">"IMEZIMWA"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Imezimwa"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"Sekunde 0.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"Sekunde 1"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"Sekunde 1.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"Sekunde 2"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"Sekunde 2.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"Sekunde 3"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"Sekunde 4"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"Sekunde 5"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"Sekunde 6"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"Sekunde 10"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"Sekunde 12"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"Sekunde 15"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"Sekunde 24"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"Dakika 0.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"Dakika 1"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"Dakika 1.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"Dakika 2"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"Dakika 2.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"Dakika 3"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"Dakika 4"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"Dakika 5"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"Dakika 6"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"Dakika 1"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"Dakika 12"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"Dakika 15"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"Dakika 24"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"Saa 0.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"Saa 1"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"Saa 1.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"Saa 2"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"Saa 2.5"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"Saa 3"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"Saa 4"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"Saa 5"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"Saa 6"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"Saa 10"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"Saa 12"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"Saa 15"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"Saa 24"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"sekunde"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"dakika"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"saa"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Imekamilika"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Weka Nafasi ya Muda"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Kipengele cha muda kupita kimezimika. Kiwashe ili kuweka nafasi ya muda."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Kipima muda cha kuhesabu kimezimwa. Kiwashe ili kihesabu kabla ya kupiga picha."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Weka muda katika sekunde"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Inahesabu ili kupiga picha"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Kumbuka maeneo ya picha?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Tambulisha picha na video zako kwa maeneo ambapo zinachukuliwa."\n\n"Programu nyingine zinaweza kufikia maelezo haya kando na picha zako zilizohifadhiwa."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"La, asante"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Ndiyo"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Tafuta"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Picha"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albamu"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"Picha %1$d"</item>
+    <item quantity="other" msgid="3813306834113858135">"Picha %1$d"</item>
+  </plurals>
+</resources>
diff --git a/res/values-sw320dp-land/photoeditor_dimens.xml b/res/values-sw320dp-land/photoeditor_dimens.xml
new file mode 100755
index 0000000..844c928
--- /dev/null
+++ b/res/values-sw320dp-land/photoeditor_dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:android="http://schemas.android.com/apk/res/android">
+    <dimen name="action_bar_icon_padding_vertical">4dp</dimen>
+    <dimen name="action_button_padding_vertical">4dp</dimen>
+</resources>
diff --git a/res/values-sw320dp-port/photoeditor_dimens.xml b/res/values-sw320dp-port/photoeditor_dimens.xml
new file mode 100755
index 0000000..85779d6
--- /dev/null
+++ b/res/values-sw320dp-port/photoeditor_dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:android="http://schemas.android.com/apk/res/android">
+    <dimen name="action_bar_icon_padding_vertical">8dp</dimen>
+    <dimen name="action_button_padding_vertical">8dp</dimen>
+</resources>
diff --git a/res/values-sw320dp/photoeditor_dimens.xml b/res/values-sw320dp/photoeditor_dimens.xml
new file mode 100755
index 0000000..a7d6a24
--- /dev/null
+++ b/res/values-sw320dp/photoeditor_dimens.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:android="http://schemas.android.com/apk/res/android">
+    <dimen name="effect_label_text_size">11.5sp</dimen>
+    <dimen name="effect_label_width">100dp</dimen>
+    <dimen name="effect_label_margin_top">3dp</dimen>
+    <dimen name="effect_icon_size">72dp</dimen>
+    <dimen name="effect_padding_horizontal">1dp</dimen>
+    <dimen name="effects_container_padding">8dp</dimen>
+    <dimen name="action_bar_arrow_margin_left">3dp</dimen>
+    <dimen name="action_bar_arrow_margin_right">-3dp</dimen>
+    <dimen name="action_bar_icon_padding_left">0dp</dimen>
+    <dimen name="action_bar_icon_padding_right">5dp</dimen>
+    <dimen name="action_button_padding_horizontal">13dp</dimen>
+    <dimen name="effects_menu_container_width">320dp</dimen>
+    <dimen name="effect_tool_panel_padding_top">3dp</dimen>
+    <dimen name="effect_tool_panel_padding_bottom">8dp</dimen>
+    <dimen name="seekbar_width">350dp</dimen>
+    <dimen name="seekbar_height">43dp</dimen>
+    <dimen name="seekbar_padding_horizontal">30dp</dimen>
+    <dimen name="seekbar_padding_vertical">8dp</dimen>
+    <dimen name="crop_indicator_size">35dp</dimen>
+</resources>
diff --git a/res/values-sw360dp-land/dimensions.xml b/res/values-sw360dp-land/dimensions.xml
new file mode 100644
index 0000000..fe3e6f5
--- /dev/null
+++ b/res/values-sw360dp-land/dimensions.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <dimen name="capture_margin_top">8dip</dimen>
+</resources>
diff --git a/res/values-sw360dp/dimensions.xml b/res/values-sw360dp/dimensions.xml
new file mode 100644
index 0000000..b18b934
--- /dev/null
+++ b/res/values-sw360dp/dimensions.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <dimen name="capture_margin_top">16dip</dimen>
+</resources>
diff --git a/res/values-sw600dp-hdpi/drawable.xml b/res/values-sw600dp-hdpi/drawable.xml
new file mode 100644
index 0000000..b810347
--- /dev/null
+++ b/res/values-sw600dp-hdpi/drawable.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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 name="btn_video_shutter_recording_holo" type="drawable">@drawable/btn_video_shutter_recording_holo_large</item>
+    <item name="btn_video_shutter_recording_pressed_holo" type="drawable">@drawable/btn_video_shutter_recording_pressed_holo_large</item>
+    <item name="ic_effects_holo_light" type="drawable">@drawable/ic_effects_holo_light_large</item>
+    <item name="ic_pan_border_fast" type="drawable">@drawable/ic_pan_border_fast_large</item>
+    <item name="ic_pan_left_indicator_fast" type="drawable">@drawable/ic_pan_left_indicator_fast_large</item>
+    <item name="ic_pan_left_indicator" type="drawable">@drawable/ic_pan_left_indicator_large</item>
+    <item name="ic_pan_progression" type="drawable">@drawable/ic_pan_progression_large</item>
+    <item name="ic_pan_right_indicator_fast" type="drawable">@drawable/ic_pan_right_indicator_fast_large</item>
+    <item name="ic_pan_right_indicator" type="drawable">@drawable/ic_pan_right_indicator_large</item>
+    <item name="ic_scn_holo_light" type="drawable">@drawable/ic_scn_holo_light_large</item>
+    <item name="ic_snapshot_border" type="drawable">@drawable/ic_snapshot_border_large</item>
+    <item name="ic_switch_photo_facing_holo_light" type="drawable">@drawable/ic_switch_photo_facing_holo_light_large</item>
+    <item name="ic_switch_video_facing_holo_light" type="drawable">@drawable/ic_switch_video_facing_holo_light_large</item>
+    <item name="ic_timelapse_none" type="drawable">@drawable/ic_timelapse_none_large</item>
+    <item name="list_divider" type="drawable">@drawable/list_divider_large</item>
+</resources>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..5ec2f19
--- /dev/null
+++ b/res/values-sw600dp/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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>
+    <dimen name="setting_popup_right_margin">@dimen/setting_popup_right_margin_large</dimen>
+    <dimen name="setting_row_height">@dimen/setting_row_height_large</dimen>
+    <dimen name="setting_popup_window_width">@dimen/setting_popup_window_width_large</dimen>
+    <dimen name="setting_item_icon_width">@dimen/setting_item_icon_width_large</dimen>
+    <dimen name="onscreen_indicators_height">@dimen/onscreen_indicators_height_large</dimen>
+    <dimen name="shutter_offset">-33dp</dimen>
+    <dimen name="capture_size">80dip</dimen>
+    <dimen name="capture_margin_top">16dip</dimen>
+    <dimen name="camera_controls_size">520dip</dimen>
+</resources>
diff --git a/res/values-sw600dp/dimensions.xml b/res/values-sw600dp/dimensions.xml
new file mode 100644
index 0000000..ff04de7
--- /dev/null
+++ b/res/values-sw600dp/dimensions.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <!--  configuration for filtershow UI -->
+    <dimen name="thumbnail_size">128dip</dimen>
+    <dimen name="thumbnail_margin">3dip</dimen>
+</resources>
diff --git a/res/values-sw600dp/photoeditor_dimens.xml b/res/values-sw600dp/photoeditor_dimens.xml
new file mode 100755
index 0000000..b861150
--- /dev/null
+++ b/res/values-sw600dp/photoeditor_dimens.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:android="http://schemas.android.com/apk/res/android">
+    <dimen name="effect_label_text_size">13.5sp</dimen>
+    <dimen name="effect_label_width">120dp</dimen>
+    <dimen name="effect_label_margin_top">3dp</dimen>
+    <dimen name="effect_icon_size">90dp</dimen>
+    <dimen name="effect_padding_horizontal">2dp</dimen>
+    <dimen name="effects_container_padding">11dp</dimen>
+    <dimen name="action_bar_arrow_margin_left">3dp</dimen>
+    <dimen name="action_bar_arrow_margin_right">-3dp</dimen>
+    <dimen name="action_bar_icon_padding_vertical">4dp</dimen>
+    <dimen name="action_bar_icon_padding_left">0dp</dimen>
+    <dimen name="action_bar_icon_padding_right">5dp</dimen>
+    <dimen name="action_button_padding_vertical">8dp</dimen>
+    <dimen name="action_button_padding_horizontal">22dp</dimen>
+    <dimen name="effects_menu_container_width">400dp</dimen>
+    <dimen name="effect_tool_panel_padding_top">4dp</dimen>
+    <dimen name="effect_tool_panel_padding_bottom">11dp</dimen>
+    <dimen name="seekbar_width">640dp</dimen>
+    <dimen name="seekbar_height">57dp</dimen>
+    <dimen name="seekbar_padding_horizontal">40dp</dimen>
+    <dimen name="seekbar_padding_vertical">11dp</dimen>
+    <dimen name="crop_indicator_size">43dp</dimen>
+</resources>
diff --git a/res/values-sw640dp/dimens.xml b/res/values-sw640dp/dimens.xml
new file mode 100644
index 0000000..51b3dad
--- /dev/null
+++ b/res/values-sw640dp/dimens.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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>
+    <dimen name="pano_mosaic_surface_height">@dimen/pano_mosaic_surface_height_xlarge</dimen>
+    <dimen name="pano_review_button_width">@dimen/pano_review_button_width_xlarge</dimen>
+    <dimen name="pano_review_button_height">@dimen/pano_review_button_height_xlarge</dimen>
+    <dimen name="setting_row_height">@dimen/setting_row_height_xlarge</dimen>
+    <dimen name="setting_item_text_size">@dimen/setting_item_text_size_xlarge</dimen>
+    <dimen name="setting_knob_width">@dimen/setting_knob_width_xlarge</dimen>
+    <dimen name="setting_item_text_width">@dimen/setting_item_text_width_xlarge</dimen>
+    <dimen name="setting_popup_window_width">@dimen/setting_popup_window_width_xlarge</dimen>
+    <dimen name="setting_item_list_margin">@dimen/setting_item_list_margin_xlarge</dimen>
+    <dimen name="indicator_bar_width">@dimen/indicator_bar_width_xlarge</dimen>
+    <dimen name="popup_title_text_size">@dimen/popup_title_text_size_xlarge</dimen>
+    <dimen name="popup_title_frame_min_height">@dimen/popup_title_frame_min_height_xlarge</dimen>
+    <dimen name="big_setting_popup_window_width">@dimen/big_setting_popup_window_width_xlarge</dimen>
+    <dimen name="setting_item_icon_width">@dimen/setting_item_icon_width_xlarge</dimen>
+    <dimen name="effect_setting_item_icon_width">@dimen/effect_setting_item_icon_width_xlarge</dimen>
+    <dimen name="effect_setting_item_text_size">@dimen/effect_setting_item_text_size_xlarge</dimen>
+    <dimen name="effect_setting_type_text_size">@dimen/effect_setting_type_text_size_xlarge</dimen>
+    <dimen name="effect_setting_type_text_min_height">@dimen/effect_setting_type_text_min_height_xlarge</dimen>
+    <dimen name="effect_setting_clear_text_size">@dimen/effect_setting_clear_text_size_xlarge</dimen>
+    <dimen name="effect_setting_clear_text_min_height">@dimen/effect_setting_clear_text_min_height_xlarge</dimen>
+    <dimen name="effect_setting_type_text_left_padding">@dimen/effect_setting_type_text_left_padding_xlarge</dimen>
+    <dimen name="onscreen_indicators_height">@dimen/onscreen_indicators_height_xlarge</dimen>
+    <dimen name="onscreen_exposure_indicator_text_size">@dimen/onscreen_exposure_indicator_text_size_xlarge</dimen>
+</resources>
+
diff --git a/res/values-sw640dp/drawable.xml b/res/values-sw640dp/drawable.xml
new file mode 100644
index 0000000..6a6e711
--- /dev/null
+++ b/res/values-sw640dp/drawable.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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 name="btn_video_shutter_recording_holo" type="drawable">@drawable/btn_video_shutter_recording_holo_xlarge</item>
+    <item name="btn_video_shutter_recording_pressed_holo" type="drawable">@drawable/btn_video_shutter_recording_pressed_holo_xlarge</item>
+    <item name="ic_effects_holo_light" type="drawable">@drawable/ic_effects_holo_light_xlarge</item>
+    <item name="ic_pan_border_fast" type="drawable">@drawable/ic_pan_border_fast_xlarge</item>
+    <item name="ic_pan_left_indicator_fast" type="drawable">@drawable/ic_pan_left_indicator_fast_xlarge</item>
+    <item name="ic_pan_left_indicator" type="drawable">@drawable/ic_pan_left_indicator_xlarge</item>
+    <item name="ic_pan_progression" type="drawable">@drawable/ic_pan_progression_xlarge</item>
+    <item name="ic_pan_right_indicator_fast" type="drawable">@drawable/ic_pan_right_indicator_fast_xlarge</item>
+    <item name="ic_pan_right_indicator" type="drawable">@drawable/ic_pan_right_indicator_xlarge</item>
+    <item name="ic_scn_holo_light" type="drawable">@drawable/ic_scn_holo_light_xlarge</item>
+    <item name="ic_snapshot_border" type="drawable">@drawable/ic_snapshot_border_xlarge</item>
+    <item name="ic_switch_photo_facing_holo_light" type="drawable">@drawable/ic_switch_photo_facing_holo_light_xlarge</item>
+    <item name="ic_switch_video_facing_holo_light" type="drawable">@drawable/ic_switch_video_facing_holo_light_xlarge</item>
+    <item name="ic_timelapse_none" type="drawable">@drawable/ic_timelapse_none_xlarge</item>
+</resources>
diff --git a/res/values-sw640dp/styles.xml b/res/values-sw640dp/styles.xml
new file mode 100644
index 0000000..6ab7063
--- /dev/null
+++ b/res/values-sw640dp/styles.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <style name="ReviewControlText" parent="@style/ReviewControlText_xlarge" />
+    <style name="PopupTitleText" parent="@style/PopupTitleText_xlarge" />
+    <style name="PanoCustomDialogText" parent="@style/PanoCustomDialogText_xlarge" />
+    <style name="ViewfinderLabelLayout" parent="@style/ViewfinderLabelLayout_xlarge" />
+    <style name="SettingPopupWindow" parent="@style/SettingPopupWindow_xlarge" />
+</resources>
diff --git a/res/values-sw800dp/dimens.xml b/res/values-sw800dp/dimens.xml
new file mode 100644
index 0000000..d1b8c6d
--- /dev/null
+++ b/res/values-sw800dp/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011, 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>
+    <dimen name="setting_row_height">54dp</dimen>
+    <dimen name="setting_item_text_size">21dp</dimen>
+    <dimen name="setting_knob_width">72dp</dimen>
+    <dimen name="setting_item_text_width">130dp</dimen>
+    <dimen name="setting_popup_window_width">410dp</dimen>
+    <dimen name="setting_item_list_margin">24dp</dimen>
+    <dimen name="popup_title_text_size">22dp</dimen>
+    <dimen name="popup_title_frame_min_height">64dp</dimen>
+    <dimen name="big_setting_popup_window_width">590dp</dimen>
+    <dimen name="setting_item_icon_width">35dp</dimen>
+</resources>
diff --git a/res/values-sw800dp/dimensions.xml b/res/values-sw800dp/dimensions.xml
new file mode 100644
index 0000000..ff04de7
--- /dev/null
+++ b/res/values-sw800dp/dimensions.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <!--  configuration for filtershow UI -->
+    <dimen name="thumbnail_size">128dip</dimen>
+    <dimen name="thumbnail_margin">3dip</dimen>
+</resources>
diff --git a/res/values-sw800dp/photoeditor_dimens.xml b/res/values-sw800dp/photoeditor_dimens.xml
new file mode 100755
index 0000000..804a8ca
--- /dev/null
+++ b/res/values-sw800dp/photoeditor_dimens.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:android="http://schemas.android.com/apk/res/android">
+    <dimen name="effect_label_text_size">14.5sp</dimen>
+    <dimen name="effect_label_width">140dp</dimen>
+    <dimen name="effect_label_margin_top">4dp</dimen>
+    <dimen name="effect_icon_size">100dp</dimen>
+    <dimen name="effect_padding_horizontal">2dp</dimen>
+    <dimen name="effects_container_padding">13dp</dimen>
+    <dimen name="action_bar_arrow_margin_left">3dp</dimen>
+    <dimen name="action_bar_arrow_margin_right">-3dp</dimen>
+    <dimen name="action_bar_icon_padding_vertical">4dp</dimen>
+    <dimen name="action_bar_icon_padding_left">0dp</dimen>
+    <dimen name="action_bar_icon_padding_right">5dp</dimen>
+    <dimen name="action_button_padding_vertical">8dp</dimen>
+    <dimen name="action_button_padding_horizontal">28dp</dimen>
+    <dimen name="effects_menu_container_width">400dp</dimen>
+    <dimen name="effect_tool_panel_padding_top">5dp</dimen>
+    <dimen name="effect_tool_panel_padding_bottom">13dp</dimen>
+    <dimen name="seekbar_width">640dp</dimen>
+    <dimen name="seekbar_height">61dp</dimen>
+    <dimen name="seekbar_padding_horizontal">40dp</dimen>
+    <dimen name="seekbar_padding_vertical">13dp</dimen>
+    <dimen name="crop_indicator_size">48dp</dimen>
+</resources>
diff --git a/res/values-th/filtershow_strings.xml b/res/values-th/filtershow_strings.xml
new file mode 100644
index 0000000..0b98128
--- /dev/null
+++ b/res/values-th/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"โปรแกรมตกแต่งรูปภาพ"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"ไม่สามารถโหลดภาพ!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"กำลังตั้งค่าวอลเปเปอร์"</string>
+    <string name="original" msgid="3524493791230430897">"ต้นฉบับ"</string>
+    <string name="borders" msgid="2067345080568684614">"ขอบ"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"เลิกทำ"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"ทำซ้ำ"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"แสดงประวัติ"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"ซ่อนประวัติ"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"สถานะแสดงภาพ"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"สถานะซ่อนภาพ"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"การตั้งค่า"</string>
+    <string name="unsaved" msgid="8704442449002374375">"มีการเปลี่ยนแปลงที่ไม่ได้บันทึกไปยังภาพนี้"</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"คุณต้องการบันทึกก่อนที่จะออกหรือไม่"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"บันทึกและออก"</string>
+    <string name="exit" msgid="242642957038770113">"ออก"</string>
+    <string name="history" msgid="455767361472692409">"ประวัติ"</string>
+    <string name="reset" msgid="9013181350779592937">"รีเซ็ต"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"เอฟเฟ็กต์ที่ใช้"</string>
+    <string name="compare_original" msgid="8140838959007796977">"เปรียบเทียบ"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"ใช้"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"รีเซ็ต"</string>
+    <string name="aspect" msgid="4025244950820813059">"อัตราส่วน"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"ไม่มี"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"คงที่"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"โลกใบเล็ก"</string>
+    <string name="exposure" msgid="6526397045949374905">"การรับแสง"</string>
+    <string name="sharpness" msgid="6463103068318055412">"ความคมชัด"</string>
+    <string name="contrast" msgid="2310908487756769019">"ความเปรียบต่าง"</string>
+    <string name="vibrance" msgid="3326744578577835915">"ความสด"</string>
+    <string name="saturation" msgid="7026791551032438585">"ความชัดเจน"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"ตัวกรองขาวดำ"</string>
+    <string name="wbalance" msgid="6346581563387083613">"ให้สีอัตโนมัติ"</string>
+    <string name="hue" msgid="6231252147971086030">"สี"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"เงา"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"ไฮไลต์"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"เส้นโค้ง"</string>
+    <string name="vignette" msgid="934721068851885390">"วิกเน็ตต์"</string>
+    <string name="redeye" msgid="4508883127049472069">"ตาแดง"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"วาดภาพ"</string>
+    <string name="straighten" msgid="26025591664983528">"ทำให้ตรง"</string>
+    <string name="crop" msgid="5781263790107850771">"ครอบตัด"</string>
+    <string name="rotate" msgid="2796802553793795371">"หมุน"</string>
+    <string name="mirror" msgid="5482518108154883096">"มิเรอร์"</string>
+    <string name="negative" msgid="6998313764388022201">"เนกาทีฟ"</string>
+    <string name="none" msgid="6633966646410296520">"ไม่มี"</string>
+    <string name="edge" msgid="7036064886242147551">"ขอบ"</string>
+    <string name="kmeans" msgid="1630263230946107457">"วอร์ฮอล"</string>
+    <string name="downsample" msgid="3552938534146980104">"ลดลง"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"สีแดง"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"สีเขียว"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"สีน้ำเงิน"</string>
+    <string name="draw_style" msgid="2036125061987325389">"รูปแบบ"</string>
+    <string name="draw_size" msgid="4360005386104151209">"ขนาด"</string>
+    <string name="draw_color" msgid="2119030386987211193">"สี"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"เส้น"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"สีเมจิก"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"สาดสี"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"ล้าง"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"เลือกสีที่กำหนดเอง"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"เลือกสี"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"เลือกขนาด"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"ตกลง"</string>
+</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
new file mode 100644
index 0000000..b62c2f9
--- /dev/null
+++ b/res/values-th/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"แกลเลอรี"</string>
+    <string name="gadget_title" msgid="259405922673466798">"กรอบภาพ"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"โปรแกรมเล่นวิดีโอ"</string>
+    <string name="loading_video" msgid="4013492720121891585">"กำลังโหลดวิดีโอ..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"กำลังโหลดภาพ..."</string>
+    <string name="loading_account" msgid="928195413034552034">"กำลังดาวน์โหลดบัญชี…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"เล่นวิดีโอต่อ"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"ต้องการเล่นต่อจาก %s หรือไม่"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"เล่นต่อ"</string>
+    <string name="loading" msgid="7038208555304563571">"กำลังโหลด…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"ไม่สามารถโหลด"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"ไม่สามารถโหลดภาพ"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"ไม่มีภาพขนาดย่อ"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"เริ่มต้นใหม่"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ตกลง"</string>
+    <string name="ok" msgid="5296833083983263293">"ตกลง"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"แตะใบหน้าเพื่อเริ่มต้น"</string>
+    <string name="saving_image" msgid="7270334453636349407">"กำลังบันทึกภาพ..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"กำลังบันทึกภาพลงใน <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="save_error" msgid="6857408774183654970">"ไม่สามารถบันทึกภาพที่ตัดได้"</string>
+    <string name="crop_label" msgid="521114301871349328">"ตัดภาพ"</string>
+    <string name="trim_label" msgid="274203231381209979">"ตัดวิดีโอ"</string>
+    <string name="select_image" msgid="7841406150484742140">"เลือกรูปภาพ"</string>
+    <string name="select_video" msgid="4859510992798615076">"เลือกวิดีโอ"</string>
+    <string name="select_item" msgid="2816923896202086390">"เลือกรายการ"</string>
+    <string name="select_album" msgid="1557063764849434077">"เลือกอัลบั้ม"</string>
+    <string name="select_group" msgid="6744208543323307114">"เลือกกลุ่ม"</string>
+    <string name="set_image" msgid="2331476809308010401">"ตั้งค่าภาพเป็น"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"ตั้งค่าวอลเปเปอร์"</string>
+    <string name="wallpaper" msgid="140165383777262070">"กำลังตั้งค่าวอลเปเปอร์..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"วอลเปเปอร์"</string>
+    <string name="delete" msgid="2839695998251824487">"ลบ"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"ลบรายการที่เลือกหรือไม่"</item>
+    <item quantity="other" msgid="5874316486520635333">"ลบรายการที่เลือกหรือไม่"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"ยืนยัน"</string>
+    <string name="cancel" msgid="3637516880917356226">"ยกเลิก"</string>
+    <string name="share" msgid="3619042788254195341">"แบ่งปัน"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"แบ่งปันภาพพาโนรามา"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"แบ่งปันเป็นรูปภาพ"</string>
+    <string name="deleted" msgid="6795433049119073871">"ลบแล้ว"</string>
+    <string name="undo" msgid="2930873956446586313">"เลิกทำ"</string>
+    <string name="select_all" msgid="3403283025220282175">"เลือกทั้งหมด"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"ยกเลิกการเลือกทั้งหมด"</string>
+    <string name="slideshow" msgid="4355906903247112975">"การนำเสนอภาพนิ่ง"</string>
+    <string name="details" msgid="8415120088556445230">"รายละเอียด"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d จาก %2$d รายการ:"</string>
+    <string name="close" msgid="5585646033158453043">"ปิด"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"สลับเป็นกล้องถ่ายรูป"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"เลือกไว้ %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"เลือกไว้ %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"เลือกไว้ %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"เลือกไว้ %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"เลือกไว้ %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"เลือกไว้ %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"เลือกไว้ %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"เลือกไว้ %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"เลือกไว้ %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"แสดงบนแผนที่"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"หมุนไปทางซ้าย"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"หมุนไปทางขวา"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"ไม่พบรายการ"</string>
+    <string name="edit" msgid="1502273844748580847">"แก้ไข"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"กำลังประมวลผลคำขอให้แคช"</string>
+    <string name="caching_label" msgid="4521059045896269095">"กำลังแคช..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"ตัด"</string>
+    <string name="trim_action" msgid="703098114452883524">"ตัดแต่ง"</string>
+    <string name="mute_action" msgid="5296241754753306251">"ปิดเสียง"</string>
+    <string name="set_as" msgid="3636764710790507868">"ตั้งค่าเป็น"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"ไม่สามารถปิดเสียงวิดีโอ"</string>
+    <string name="video_err" msgid="7003051631792271009">"ไม่สามารถเล่นวิดีโอ"</string>
+    <string name="group_by_location" msgid="316641628989023253">"ตามตำแหน่ง"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"ตามเวลา"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"ตามแท็ก"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"จัดกลุ่มตามใบหน้าบุคคล"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"ตามอัลบั้ม"</string>
+    <string name="group_by_size" msgid="153766174950394155">"ตามขนาด"</string>
+    <string name="untagged" msgid="7281481064509590402">"ยกเลิกการติดแท็ก"</string>
+    <string name="no_location" msgid="4043624857489331676">"ไม่มีสถานที่"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"ไม่สามารถระบุบางตำแหน่งได้เนื่องจากปัญหาเกี่ยวกับเครือข่าย"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"ไม่สามารถดาวน์โหลดรูปภาพในอัลบั้มนี้ โปรดลองอีกครั้งในภายหลัง"</string>
+    <string name="show_images_only" msgid="7263218480867672653">"เฉพาะภาพ"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"เฉพาะวิดีโอ"</string>
+    <string name="show_all" msgid="6963292714584735149">"ภาพและวิดีโอ"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"แกลเลอรีรูปภาพ"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"ไม่มีรูปภาพ"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"บันทึกภาพที่ครอบตัดไว้ที่ <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"ไม่มีอัลบั้ม"</string>
+    <string name="empty_album" msgid="4542880442593595494">"O ภาพ/วิดีโอ"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"โพสต์"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"ทำให้ใช้งานได้แบบออฟไลน์"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"รีเฟรช"</string>
+    <string name="done" msgid="217672440064436595">"เสร็จสิ้น"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d จาก %2$d รายการ:"</string>
+    <string name="title" msgid="7622928349908052569">"ชื่อ"</string>
+    <string name="description" msgid="3016729318096557520">"คำอธิบาย"</string>
+    <string name="time" msgid="1367953006052876956">"เวลา"</string>
+    <string name="location" msgid="3432705876921618314">"สถานที่"</string>
+    <string name="path" msgid="4725740395885105824">"เส้นทาง"</string>
+    <string name="width" msgid="9215847239714321097">"ความกว้าง"</string>
+    <string name="height" msgid="3648885449443787772">"ความสูง"</string>
+    <string name="orientation" msgid="4958327983165245513">"การวางแนว"</string>
+    <string name="duration" msgid="8160058911218541616">"ระยะเวลา"</string>
+    <string name="mimetype" msgid="8024168704337990470">"ประเภท MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"ขนาดไฟล์"</string>
+    <string name="maker" msgid="7921835498034236197">"ยี่ห้อ"</string>
+    <string name="model" msgid="8240207064064337366">"รุ่น"</string>
+    <string name="flash" msgid="2816779031261147723">"แฟลช"</string>
+    <string name="aperture" msgid="5920657630303915195">"รูรับแสง"</string>
+    <string name="focal_length" msgid="1291383769749877010">"ระยะโฟกัส"</string>
+    <string name="white_balance" msgid="1582509289994216078">"สมดุลแสงขาว"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"เวลารับแสง"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"มม."</string>
+    <string name="manual" msgid="6608905477477607865">"ปรับเอง"</string>
+    <string name="auto" msgid="4296941368722892821">"อัตโนมัติ"</string>
+    <string name="flash_on" msgid="7891556231891837284">"แฟลชทำงาน"</string>
+    <string name="flash_off" msgid="1445443413822680010">"ไม่เปิดแฟลช"</string>
+    <string name="unknown" msgid="3506693015896912952">"ไม่ทราบ"</string>
+    <string name="ffx_original" msgid="372686331501281474">"ต้นฉบับ"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"วินเทจ"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"รูปด่วน"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"ทำให้สีซีด"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"สีน้ำเงิน"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"ขาวดำ"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"พันช์"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"ครอสโพรเซส"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"ลาเต้"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"หิน"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"กำลังทำให้ใช้งานอัลบั้มแบบออฟไลน์ได้"</item>
+    <item quantity="other" msgid="4948604338155959389">"กำลังทำให้ใช้งานอัลบั้มแบบออฟไลน์ได้"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"รายการนี้จัดเก็บภายในเครื่องและสามารถใช้งานแบบออฟไลน์"</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"อัลบั้มทั้งหมด"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"อัลบั้มในเครื่อง"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"อุปกรณ์ MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"อัลบั้มใน Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ว่าง"</string>
+    <string name="size_below" msgid="2074956730721942260">"ไม่เกิน <xliff:g id="SIZE">%1$s</xliff:g>"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ขึ้นไป"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> ถึง <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"นำเข้า"</string>
+    <string name="import_complete" msgid="3875040287486199999">"นำเข้าเสร็จสมบูรณ์"</string>
+    <string name="import_fail" msgid="8497942380703298808">"นำเข้าไม่สำเร็จ"</string>
+    <string name="camera_connected" msgid="916021826223448591">"เชื่อมต่อกล้องถ่ายรูปแล้ว"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"หยุดการเชื่อมต่อกล้อง"</string>
+    <string name="click_import" msgid="6407959065464291972">"แตะที่นี่เพื่อนำเข้า"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"เลือกอัลบั้ม"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"สุ่มภาพทั้งหมด"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"เลือกภาพ"</string>
+    <string name="widget_type" msgid="1364653978966343448">"เลือกภาพ"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"สไลด์โชว์"</string>
+    <string name="albums" msgid="7320787705180057947">"อัลบั้ม"</string>
+    <string name="times" msgid="2023033894889499219">"เวลา"</string>
+    <string name="locations" msgid="6649297994083130305">"สถานที่"</string>
+    <string name="people" msgid="4114003823747292747">"บุคคล"</string>
+    <string name="tags" msgid="5539648765482935955">"แท็ก"</string>
+    <string name="group_by" msgid="4308299657902209357">"จัดกลุ่มตาม"</string>
+    <string name="settings" msgid="1534847740615665736">"การตั้งค่า"</string>
+    <string name="add_account" msgid="4271217504968243974">"เพิ่มบัญชี"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"กล้องถ่ายรูป"</string>
+    <string name="folder_download" msgid="7186215137642323932">"ดาวน์โหลด"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"รูปภาพออนไลน์ที่แก้ไขแล้ว"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"นำเข้าแล้ว"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"ภาพหน้าจอ"</string>
+    <string name="help" msgid="7368960711153618354">"ความช่วยเหลือ"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"ไม่มีที่เก็บข้อมูล"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"ไม่มีที่จัดเก็บข้อมูลภายนอกที่สามารถใช้ได้"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"มุมมองฟิล์มภาพยนตร์"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"มุมมองตาราง"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"มุมมองแบบเต็มหน้าจอ"</string>
+    <string name="trimming" msgid="9122385768369143997">"กำลังตัด"</string>
+    <string name="muting" msgid="5094925919589915324">"กำลังปิดเสียง"</string>
+    <string name="please_wait" msgid="7296066089146487366">"โปรดรอสักครู่"</string>
+    <string name="save_into" msgid="9155488424829609229">"กำลังบันทึกวิดีโอใน <xliff:g id="ALBUM_NAME">%1$s</xliff:g>…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"ไม่สามารถตัด : วิดีโอปลายทางสั้นเกินไป"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"กำลังแสดงภาพพาโนรามา"</string>
+    <string name="save" msgid="613976532235060516">"บันทึก"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"กำลังสแกนเนื้อหา..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"สแกน %1$d รายการแล้ว"</item>
+    <item quantity="one" msgid="4340019444460561648">"สแกน %1$d รายการแล้ว"</item>
+    <item quantity="other" msgid="3138021473860555499">"สแกน %1$d รายการแล้ว"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"กำลังจัดเรียง..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"สแกนเสร็จแล้ว"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"กำลังนำเข้า..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"ไม่มีเนื้อหาสำหรับการนำเข้าในอุปกรณ์นี้"</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"ไม่มีอุปกรณ์ MTP ที่เชื่อมต่อกัน"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"ข้อผิดพลาดกล้องถ่ายรูป"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"ไม่สามารถเชื่อมต่อกับกล้องถ่ายรูป"</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"กล้องถ่ายรูปถูกปิดใช้งานเนื่องจากนโยบายด้านความปลอดภัย"</string>
+    <string name="camera_label" msgid="6346560772074764302">"กล้อง"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"กล้องวิดีโอ"</string>
+    <string name="wait" msgid="8600187532323801552">"โปรดรอสักครู่..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"ต่อเชื่อมที่จัดเก็บข้อมูล USB ก่อนใช้กล้องถ่ายรูป"</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"ใส่การ์ด SD ก่อนใช้กล้องถ่ายรูป"</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"กำลังเตรียมที่เก็บข้อมูล USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"กำลังเตรียมการ์ด SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"ไม่สามารถเข้าถึงที่เก็บข้อมูล USB"</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"ไม่สามารถเข้าถึงการ์ด SD"</string>
+    <string name="review_cancel" msgid="8188009385853399254">"ยกเลิก"</string>
+    <string name="review_ok" msgid="1156261588693116433">"เสร็จสิ้น"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"การบันทึกเป็นช่วงเวลา"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"เลือกกล้องถ่ายรูป"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"ย้อนกลับ"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"ด้านหน้า"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"ตำแหน่งจัดเก็บ"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"ตัวจับเวลาถอยหลัง"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 วินาที"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d วินาที"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"บี๊ปตอนนับถอยหลัง"</string>
+    <string name="setting_off" msgid="4480039384202951946">"ปิด"</string>
+    <string name="setting_on" msgid="8602246224465348901">"เปิด"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"คุณภาพวิดีโอ"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"สูง"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"ต่ำ"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"ช่วงเวลา"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"การตั้งค่ากล้องถ่ายรูป"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"การตั้งค่ากล้องวิดีโอ"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"ขนาดของภาพ"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 ล้านพิกเซล"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 ล้านพิกเซล"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 ล้านพิกเซล"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 ล้านพิกเซล"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3 ล้านพิกเซล"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 ล้านพิกเซล"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"โหมดโฟกัส"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"อัตโนมัติ"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"อินฟินิตี"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"มาโคร"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"โหมดแฟลช"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"อัตโนมัติ"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"เปิด"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"ปิด"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"ไวต์บาลานซ์"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"อัตโนมัติ"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"หลอดไส้"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"กลางวัน"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"ฟลูออเรสเซนต์"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"เมฆมาก"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"โหมดสำเร็จรูป"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"อัตโนมัติ"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"การทำงาน"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"กลางคืน"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"ดวงอาทิตย์ตก"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"งานเลี้ยง"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"ไม่สามารถเลือกได้ในโหมดสำเร็จรูป"</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"การรับแสง"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"ตกลง"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"ที่เก็บข้อมูล USB ของคุณไม่มีพื้นที่เหลือ ให้เปลี่ยนการตั้งค่าคุณภาพหรือลบบางภาพหรือไฟล์อื่นๆ"</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"การ์ด SD ของคุณไม่มีพื้นที่เหลือ ให้เปลี่ยนการตั้งค่าคุณภาพหรือลบบางภาพหรือไฟล์อื่นๆ"</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"ขนาดถึงขีดจำกัดแล้ว"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"เร็วเกินไป"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"กำลังสร้างพาโนรามา"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"ไม่สามารถบันทึกภาพพาโนรามา"</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"พาโนรามา"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"กำลังจับภาพพาโนรามา"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"กำลังรอภาพพาโนรามาก่อนหน้า"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"บันทึก..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"กำลังแสดงภาพพาโนรามา"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"แตะเพื่อโฟกัส"</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"เอฟเฟ็กต์"</string>
+    <string name="effect_none" msgid="3601545724573307541">"ไม่มี"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"บีบ"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"ตาโต"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"ปากใหญ่"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"ปากจู๋"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"จมูกโต"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"ตาเล็ก"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"พื้นที่ว่าง"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"พระอาทิตย์ตก"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"วิดีโอของคุณ"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"พักอุปกรณ์ของคุณ"\n"ออกจากการแสดงผลเป็นเวลาสักครู่หนึ่ง"</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"แตะเพื่อถ่ายภาพในขณะบันทึก"</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"เริ่มบันทึกวิดีโอแล้ว"</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"หยุดบันทึกวิดีโอแล้ว"</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"การจับภาพวิดีโอจะถูกปิดใช้งานเมื่อเปิดใช้เอฟเฟ็กต์พิเศษ"</string>
+    <string name="clear_effects" msgid="5485339175014139481">"ล้างเอฟเฟ็กต์"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"หน้าตลก"</string>
+    <string name="effect_background" msgid="6579360207378171022">"พื้นหลัง"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"ปุ่มชัตเตอร์"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"ปุ่มเมนู"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"ภาพล่าสุด"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"สลับระหว่างกล้องด้านหน้าและด้านหลัง"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"ตัวเลือกกล้องถ่ายรูป วิดีโอ หรือภาพพาโนรามา"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"การควบคุมการตั้งค่าเพิ่มเติม"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"ปิดการควบคุมการตั้งค่า"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"การควบคุมการย่อ/ขยาย"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"ลดลง %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"เพิ่มขึ้น %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"ช่องทำเครื่องหมาย %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"เปลี่ยนเป็นภาพถ่าย"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"เปลี่ยนเป็นวิดีโอ"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"เปลี่ยนเป็นภาพพาโนรามา"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"เปลี่ยนเป็นภาพพาโนรามาใหม่"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"ไม่ผ่านการตรวจสอบ"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"ผ่านการตรวจสอบ"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"ตรวจสอบการถ่ายภาพ/วิดีโอใหม่"</string>
+    <string name="capital_on" msgid="5491353494964003567">"เปิด"</string>
+    <string name="capital_off" msgid="7231052688467970897">"ปิด"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"ปิด"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 วินาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 นาที"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 ชั่วโมง"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 ชั่วโมง"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"วินาที"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"นาที"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"ชั่วโมง"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"เสร็จสิ้น"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"ตั้งค่าช่วงเวลา"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"คุณลักษณะช่วงเวลาปิดอยู่ เปิดคุณลักษณะนี้เพื่อตั้งค่าช่วงเวลา"</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"ตัวจับเวลาถอยหลังปิดอยู่ เปิดตัวจับเวลาเพื่อนับถอยหลังก่อนถ่ายภาพ"</string>
+    <string name="set_duration" msgid="5578035312407161304">"ตั้งระยะเวลาเป็นวินาที"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"นับถอยหลังเพื่อถ่ายภาพ"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"จดจำตำแหน่งภาพหรือไม่"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"แท็กรูปภาพและวิดีโอด้วยตำแหน่งที่ถ่ายภาพ"\n\n"แอปพลิเคชันอื่นๆ สามารถเข้าถึงข้อมูลนี้ตลอดจนภาพที่บันทึกไว้ของคุณได้"</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"ไม่ ขอบคุณ"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"ใช่"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"กล้องถ่ายรูป"</string>
+    <string name="menu_search" msgid="7580008232297437190">"ค้นหา"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"รูปภาพ"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"อัลบั้ม"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d ภาพ"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d ภาพ"</item>
+  </plurals>
+</resources>
diff --git a/res/values-tl/filtershow_strings.xml b/res/values-tl/filtershow_strings.xml
new file mode 100644
index 0000000..947db42
--- /dev/null
+++ b/res/values-tl/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Editor ng Larawan"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Hindi ma-load ang larawan!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Itinatakda ang wallpaper"</string>
+    <string name="original" msgid="3524493791230430897">"Orihinal"</string>
+    <string name="borders" msgid="2067345080568684614">"Mga Border"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"I-undo"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"I-redo"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Ipakita Kasaysayan"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Kasaysayan Pagtago"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Ipakita Image State"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Itago Image State"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Mga Setting"</string>
+    <string name="unsaved" msgid="8704442449002374375">"May mga hindi naka-save na pagbabago sa larawang ito."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Gusto mo bang mag-save bago lumabas?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"I-save at Lumabas"</string>
+    <string name="exit" msgid="242642957038770113">"Lumabas"</string>
+    <string name="history" msgid="455767361472692409">"Kasaysayan"</string>
+    <string name="reset" msgid="9013181350779592937">"I-reset"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Mga Nakalapat na Effect"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Ihambing"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Ilapat"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"I-reset"</string>
+    <string name="aspect" msgid="4025244950820813059">"Aspeto"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Wala"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Nakapirmi"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Tiny Planet"</string>
+    <string name="exposure" msgid="6526397045949374905">"Exposure"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Sharpness"</string>
+    <string name="contrast" msgid="2310908487756769019">"Contrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Vibrance"</string>
+    <string name="saturation" msgid="7026791551032438585">"Saturation"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"BW na Filter"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Autocolor"</string>
+    <string name="hue" msgid="6231252147971086030">"Hue"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Mga Shadow"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Mga Highlight"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Mga Kurba"</string>
+    <string name="vignette" msgid="934721068851885390">"Vignette"</string>
+    <string name="redeye" msgid="4508883127049472069">"Red Eye"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Gumuhit"</string>
+    <string name="straighten" msgid="26025591664983528">"Ituwid"</string>
+    <string name="crop" msgid="5781263790107850771">"I-crop"</string>
+    <string name="rotate" msgid="2796802553793795371">"I-rotate"</string>
+    <string name="mirror" msgid="5482518108154883096">"Mirror"</string>
+    <string name="negative" msgid="6998313764388022201">"Negative"</string>
+    <string name="none" msgid="6633966646410296520">"Wala"</string>
+    <string name="edge" msgid="7036064886242147551">"Mga Gilid"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Downsample"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Pula"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Berde"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Asul"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Estilo"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Laki"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Kulay"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Mga Linya"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Marker"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Spatter"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"I-clear"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Pumili ng custom na kulay"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Pumili ng Kulay"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Pumili ng Laki"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
new file mode 100644
index 0000000..82c2839
--- /dev/null
+++ b/res/values-tl/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Gallery"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Frame ng larawan"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Player ng video"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Naglo-load ng video…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Nilo-load ang larawan…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Nilo-load ang account…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Ipagpatuloy ang video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Ipagpatuloy ang pag-play mula sa %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Ipagpatuloy ang pag-play"</string>
+    <string name="loading" msgid="7038208555304563571">"Naglo-load…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Hindi mai-load"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Hindi ma-load ang larawan"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Walang thumbnail"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Magsimula na"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Pumindot ng mukha upang magsimula."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Nagse-save ng larawan..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Sine-save ang larawan sa <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Hindi mai-save ang na-crop na larawan."</string>
+    <string name="crop_label" msgid="521114301871349328">"I-crop ang larawan"</string>
+    <string name="trim_label" msgid="274203231381209979">"I-trim ang video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Pumili ng larawan"</string>
+    <string name="select_video" msgid="4859510992798615076">"Pumili ng video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Pumili ng item"</string>
+    <string name="select_album" msgid="1557063764849434077">"Pumili ng album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Pumili ng pangkat"</string>
+    <string name="set_image" msgid="2331476809308010401">"Itakda ang larawan bilang"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Itakda ang wallpaper"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Itinatakda ang wallpaper…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string>
+    <string name="delete" msgid="2839695998251824487">"Tanggalin"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Tanggalin ang napiling item?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Tanggalin mga napiling item?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Kumpirmahin"</string>
+    <string name="cancel" msgid="3637516880917356226">"Kanselahin"</string>
+    <string name="share" msgid="3619042788254195341">"Ibahagi"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Ibahagi ang panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Ibahagi bilang larawan"</string>
+    <string name="deleted" msgid="6795433049119073871">"Tinanggal"</string>
+    <string name="undo" msgid="2930873956446586313">"I-UNDO"</string>
+    <string name="select_all" msgid="3403283025220282175">"Piliin lahat"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Alisin sa pagkakapili ang lahat"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Slideshow"</string>
+    <string name="details" msgid="8415120088556445230">"Mga Detalye"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d sa %2$d (na) item:"</string>
+    <string name="close" msgid="5585646033158453043">"Isara"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Lumipat sa Camera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d ang napili"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d ang napili"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d ang napili"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d ang napili"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d ang napili"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d ang napili"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d ang napili"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d ang napili"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d ang napili"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Ipakita sa mapa"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"I-rotate pakaliwa"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"I-rotate pakanan"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Hindi mahanap ang item."</string>
+    <string name="edit" msgid="1502273844748580847">"I-edit"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Pinoproseso ang mga kahilingan sa pag-cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Nagka-cache…"</string>
+    <string name="crop_action" msgid="3427470284074377001">"I-crop"</string>
+    <string name="trim_action" msgid="703098114452883524">"Trim"</string>
+    <string name="mute_action" msgid="5296241754753306251">"I-mute"</string>
+    <string name="set_as" msgid="3636764710790507868">"Itakda bilang"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Hindi ma-mute ang video."</string>
+    <string name="video_err" msgid="7003051631792271009">"Hindi ma-play ang video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Ayon sa lokasyon"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Ayon sa oras"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Ayon sa mga tag"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Ayon sa mga tao"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Ayon sa album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Ayon sa laki"</string>
+    <string name="untagged" msgid="7281481064509590402">"Hindi naka-tag"</string>
+    <string name="no_location" msgid="4043624857489331676">"Walang lokasyon"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Hindi matukoy ang ilang mga lokasyon dahil sa mga problema sa network."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Hindi ma-download ang mga larawan sa album na ito. Pakisubukang muli sa ibang pagkakataon."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Mga larawan lamang"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Mga video lamang"</string>
+    <string name="show_all" msgid="6963292714584735149">"Mga larawan &amp; video"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Photo Gallery"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Walang mga larawan."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Nai-save ang na-crop na larawan sa <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Walang available na mga album."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O na larawan/video ang available."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Mga Post"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Gawing available sa offline"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"I-refresh"</string>
+    <string name="done" msgid="217672440064436595">"Tapos na"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d ng %2$d na item:"</string>
+    <string name="title" msgid="7622928349908052569">"Pamagat"</string>
+    <string name="description" msgid="3016729318096557520">"Paglalarawan"</string>
+    <string name="time" msgid="1367953006052876956">"Oras"</string>
+    <string name="location" msgid="3432705876921618314">"Lokasyon"</string>
+    <string name="path" msgid="4725740395885105824">"Daanan"</string>
+    <string name="width" msgid="9215847239714321097">"Lapad"</string>
+    <string name="height" msgid="3648885449443787772">"Taas"</string>
+    <string name="orientation" msgid="4958327983165245513">"Pagsasaayos"</string>
+    <string name="duration" msgid="8160058911218541616">"Tagal"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Uri ng MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Laki ng file"</string>
+    <string name="maker" msgid="7921835498034236197">"Tagagawa"</string>
+    <string name="model" msgid="8240207064064337366">"Modelo"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Aperture"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Haba ng Focal"</string>
+    <string name="white_balance" msgid="1582509289994216078">"White balance"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Tagal exposure"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Manual"</string>
+    <string name="auto" msgid="4296941368722892821">"Auto"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flash fired"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Walang flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Hindi alam"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Orihinal"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Bleach"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Asul"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"B/W"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Ginagawang available ang album offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Ginagawang available ang mga album offline."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Nakaimbak ang item na ito sa lokal at available sa offline."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Lahat ng Album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Mga lokal na album"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Mga device na MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Mga Picasa album"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> libre"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o mababa"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o mataas"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> sa <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"I-import"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Kumpleto pag-import"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Hindi matagumpay ang pag-import"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Nakakonekta ang camera."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Nadiskonekta ang camera."</string>
+    <string name="click_import" msgid="6407959065464291972">"Tumapik dito upang mag-import"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Pumili ng album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"I-shuffle ang lahat ng larawan"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Pumili ng larawan"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Pumili mga larawan"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Slideshow"</string>
+    <string name="albums" msgid="7320787705180057947">"Mga Album"</string>
+    <string name="times" msgid="2023033894889499219">"Beses"</string>
+    <string name="locations" msgid="6649297994083130305">"Mga Lokasyon"</string>
+    <string name="people" msgid="4114003823747292747">"Mga Tao"</string>
+    <string name="tags" msgid="5539648765482935955">"Mga Tag"</string>
+    <string name="group_by" msgid="4308299657902209357">"Ipangkat ayon sa"</string>
+    <string name="settings" msgid="1534847740615665736">"Mga Setting"</string>
+    <string name="add_account" msgid="4271217504968243974">"Magdagdag ng account"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Camera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"I-download"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Mga Na-edit na Online na Larawan"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Na-import"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshot"</string>
+    <string name="help" msgid="7368960711153618354">"Tulong"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Walang Storage"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Walang available na panlabas na storage"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Filmstrip view"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Grid view"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Fullscreen na view"</string>
+    <string name="trimming" msgid="9122385768369143997">"Pag-trim"</string>
+    <string name="muting" msgid="5094925919589915324">"Minu-mute"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Mangyaring maghintay"</string>
+    <string name="save_into" msgid="9155488424829609229">"Sine-save ang video sa <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Hindi ma-trim : masyadong maikli ang target na video"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Nire-render ang panorama"</string>
+    <string name="save" msgid="613976532235060516">"I-save"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Inii-scan ang nilalaman..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d (na) item ang na-scan"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d (na) item ang na-scan"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d (na) item ang na-scan"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Pinagbubukud-bukod..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Tapos na ang pag-scan"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Ini-import..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Walang nilalamang available para sa pag-import sa device na ito."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Walang MTP device na nakakonekta"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Error sa kamera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Hindi makakonekta sa camera."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Hindi na pinagana ang camera dahil sa mga patakaran sa seguridad."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Camera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Camcorder"</string>
+    <string name="wait" msgid="8600187532323801552">"Pakihintay…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"I-mount ang USB storage bago gamitin ang camera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Magpasok ng isang SD card bago gamitin ang camera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Ihinahanda imbakan na USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Naghahanda ng SD card..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Hindi ma-access ang USB storage."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Hindi ma-access ang SD card."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"KANSELAHIN"</string>
+    <string name="review_ok" msgid="1156261588693116433">"TAPOS NA"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Pagre-record ng paglipas ng oras"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Pumili ng camera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Bumalik"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Harap"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Iimbak ang lokasyon"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Timer ng countdown"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 segundo"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d (na) segundo"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Beep sa pag-countdown"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Naka-off"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Naka-on"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Kalidad ng video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Mataas"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Mababa"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Lumipas na oras"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Mga setting ng kamera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Mga setting ng camcorder"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Laki ng larawan"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M pixels"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M pixels"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M pixels"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3M pixels"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M pixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Focus mode"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Auto"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Walang Katapusan"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Flash mode"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Auto"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Naka-on"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Naka-off"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"White balance"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Auto"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Napakaliwanag"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Liwanag ng araw"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Fluorescent"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Maulap"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Scene mode"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Auto"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Pagkilos"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Gabi"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Paglubog ng araw"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Partido"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Hindi mapipili sa mode ng scene."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Pagkakalantad"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Nauubusan na ng puwang ang iyong imbakan na USB. Baguhin ang setting ng kalidad o tanggalin ang ilan sa mga larawan o ibang mga file."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Nauubusan na ng puwang ang SD card. Baguhin ang setting ng kalidad o tanggalin ang ilan sa mga larawan o ibang mga file."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Naabot ang limitasyon ng laki."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Masyadong mabilis"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Ihinahanda ang panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Hindi ma-save ang panorama."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Kinukunan ang panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Hinihintay ang nakaraang panorama"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Sine-save…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Nire-render ang panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Pindutin upang tumuon."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Mga Effect"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Wala"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Squeeze"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Malalaking mata"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Malaking bibig"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Maliit na bibig"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Malaking ilong"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Maliliit na mata"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Sa kalawakan"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Paglubog ng araw"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Iyong video"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Huwag munang gamitin ang iyong device."\n"Umalis muna nang ilang sandali."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Pindutin upang kumuha ng larawan habang nagre-record."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Nagsimula na ang pag-record ng video."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Tumigil ang pag-record ng video."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Hindi pinapagana ang snapshot sa video kapag naka-on ang mga espesyal na effect."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Mga clear na effect"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"MGA KATAWA-TAWANG MUKHA"</string>
+    <string name="effect_background" msgid="6579360207378171022">"BACKGROUND"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Button ng shutter"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Button ng menu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Pinakakamakailang larawan"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Switch ng camera sa harap at likod"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Tagapili ng camera, video, o panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Higit pang mga kontrol ng setting"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Isara ang mga kontrol ng setting"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Kontrol ng pag-zoom"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Bawasan ang %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Dagdagan ang %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"check box ng %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Lumipat sa larawan"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Lumipat sa video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Lumipat sa panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Lumipat sa bagong panorama"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Suriin ang pagkansela"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Tapos na ang pagsusuri"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Suriin ang muling pagkuha"</string>
+    <string name="capital_on" msgid="5491353494964003567">"I-ON"</string>
+    <string name="capital_off" msgid="7231052688467970897">"I-OFF"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Pag-off"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 na segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 na segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 na segundo"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 na minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 na minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 na minuto"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 na oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 na oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 oras"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 na oras"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"mga segundo"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"mga minuto"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"mga oras"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Tapos na"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Itakda ang Palugit ng Oras"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Naka-off ang tampok sa lumipas na oras. I-on ito upang itakda ang palugit ng oras."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Naka-off ang timer ng countdown. I-on ito upang magbilang bago kumuha ng larawan."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Itakda ang tagal sa loob ng ilang segundo"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Nagbibilang upang kumuha ng larawan"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Tandaan ang mga lokasyon ng larawan?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"I-tag ang iyong mga larawan at video sa mga lokasyon kung saan kinunan ang mga iyon."\n\n"Maaaring i-access ng mga ibang app ang impormasyong ito kasama ng iyong mga na-save na larawan."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Hindi na, salamat"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Oo"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Camera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Maghanap"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Mga Larawan"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Mga Album"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d larawan"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d larawan"</item>
+  </plurals>
+</resources>
diff --git a/res/values-tr/filtershow_strings.xml b/res/values-tr/filtershow_strings.xml
new file mode 100644
index 0000000..b460ef6
--- /dev/null
+++ b/res/values-tr/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Fotoğraf Düzenleyici"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Resim yüklenemiyor"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Duvar kağıdı ayarlanıyor"</string>
+    <string name="original" msgid="3524493791230430897">"Orijinal"</string>
+    <string name="borders" msgid="2067345080568684614">"Kenarlıklar"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Geri al"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Yeniden yap"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Geçmişi Göster"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Geçmişi Gizle"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Resim Durumunu Göster"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Resim Durumunu Gizle"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Ayarlar"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Bu resimde kaydedilmemiş değişiklikler var."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Çıkmadan önce kaydetmek ister misiniz?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Kaydet ve Çık"</string>
+    <string name="exit" msgid="242642957038770113">"Çıkış"</string>
+    <string name="history" msgid="455767361472692409">"Geçmiş"</string>
+    <string name="reset" msgid="9013181350779592937">"Sıfırla"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Uygulanan Efektler"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Karşılaştır"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Uygula"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Sıfırla"</string>
+    <string name="aspect" msgid="4025244950820813059">"En Boy Oranı"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Hiçbiri"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Sabit"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Küçük Gezegen"</string>
+    <string name="exposure" msgid="6526397045949374905">"Pozlama"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Keskinlik"</string>
+    <string name="contrast" msgid="2310908487756769019">"Kontrast"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Titreşim"</string>
+    <string name="saturation" msgid="7026791551032438585">"Doygunluk"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"SB Filtresi"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Otomatik Renk"</string>
+    <string name="hue" msgid="6231252147971086030">"Hue"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Gölgeler"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Parlak Noktalar"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Eğriler"</string>
+    <string name="vignette" msgid="934721068851885390">"Vinyet"</string>
+    <string name="redeye" msgid="4508883127049472069">"Kırmızı Göz"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Çiz"</string>
+    <string name="straighten" msgid="26025591664983528">"Düzleştir"</string>
+    <string name="crop" msgid="5781263790107850771">"Kırp"</string>
+    <string name="rotate" msgid="2796802553793795371">"Döndür"</string>
+    <string name="mirror" msgid="5482518108154883096">"Ayna"</string>
+    <string name="negative" msgid="6998313764388022201">"Negatif"</string>
+    <string name="none" msgid="6633966646410296520">"Hiçbiri"</string>
+    <string name="edge" msgid="7036064886242147551">"Kenarlar"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Küçült"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Kırmızı"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Yeşil"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Mavi"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Stil"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Boyut"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Renk"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Çizgi"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Fosforlu kalem"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Serpme aracı"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Temizle"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Özel renk seç"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Renk Seçin"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Boyut Seçin"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"Tamam"</string>
+</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
new file mode 100644
index 0000000..90407c0
--- /dev/null
+++ b/res/values-tr/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeri"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Resim çerçevesi"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Video oynatıcı"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Video yükleniyor..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Resim yükleniyor…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Hesap yükleniyor..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Videoyu sürdür"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Yürütme şuradan devam ettirilsin mi: %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Yürütmeyi sürdür"</string>
+    <string name="loading" msgid="7038208555304563571">"Yükleniyor…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Yüklenemedi"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Resim yüklenemedi"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Küçük resim yok"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Başlat"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Tamam"</string>
+    <string name="ok" msgid="5296833083983263293">"Tamam"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Başlamak için bir yüze dokunun."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Resim kaydediliyor..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Resim <xliff:g id="ALBUM_NAME">%1$s</xliff:g> adlı albüme kaydediliyor…"</string>
+    <string name="save_error" msgid="6857408774183654970">"Kırpılmış resim kaydedilemedi."</string>
+    <string name="crop_label" msgid="521114301871349328">"Resmi kırp"</string>
+    <string name="trim_label" msgid="274203231381209979">"Videoyu kes"</string>
+    <string name="select_image" msgid="7841406150484742140">"Fotoğraf seçin"</string>
+    <string name="select_video" msgid="4859510992798615076">"Video seçin"</string>
+    <string name="select_item" msgid="2816923896202086390">"Öğe seçin"</string>
+    <string name="select_album" msgid="1557063764849434077">"Albüm seçin"</string>
+    <string name="select_group" msgid="6744208543323307114">"Grup seçin"</string>
+    <string name="set_image" msgid="2331476809308010401">"Resmi şu şekilde ayarla:"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Duvar kağıdını ayarla"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Duvar kağıdı ayarlanıyor..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Duvar Kağıdı"</string>
+    <string name="delete" msgid="2839695998251824487">"Sil"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Seçilen öğe silinsin mi?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Seçilen öğeler silinsin mi?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Onayla"</string>
+    <string name="cancel" msgid="3637516880917356226">"İptal"</string>
+    <string name="share" msgid="3619042788254195341">"Paylaş"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Panoramayı paylaş"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Fotoğraf olarak paylaş"</string>
+    <string name="deleted" msgid="6795433049119073871">"Silindi"</string>
+    <string name="undo" msgid="2930873956446586313">"GERİ AL"</string>
+    <string name="select_all" msgid="3403283025220282175">"Tümünü seç"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Tümünün seçimini kaldır"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Slayt Gösterisi"</string>
+    <string name="details" msgid="8415120088556445230">"Ayrıntılar"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d / %2$d öğe:"</string>
+    <string name="close" msgid="5585646033158453043">"Kapat"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Kameraya Geç"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d öğe seçildi"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d öğe seçildi"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d öğe seçildi"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d albüm seçildi"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d albüm seçildi"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d albüm seçildi"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d grup seçildi"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d grup seçildi"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d grup seçildi"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Haritada göster"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Sola döndür"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Sağa döndür"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Öğe bulunamadı."</string>
+    <string name="edit" msgid="1502273844748580847">"Düzenle"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Önbelleğe alma istekleri işleniyor"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Önbelleğe alınıyor..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Kırp"</string>
+    <string name="trim_action" msgid="703098114452883524">"Kırp"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Sesi kapat"</string>
+    <string name="set_as" msgid="3636764710790507868">"Şu şekilde ayarla:"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Videonun sesi kapatılamıyor."</string>
+    <string name="video_err" msgid="7003051631792271009">"Video oynatılamıyor."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Konuma göre"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Tarihe göre"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Etiketlere göre"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Kişilere göre"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Albüme göre"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Boyuta göre"</string>
+    <string name="untagged" msgid="7281481064509590402">"Etiketlenmemiş"</string>
+    <string name="no_location" msgid="4043624857489331676">"Konum bilgisi yok"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Ağ sorunları nedeniyle bazı konumlar tanımlanamadı."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Bu albümdeki fotoğraflar indirilemedi. Daha sonra tekrar deneyin."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Yalnızca resimler"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Yalnızca videolar"</string>
+    <string name="show_all" msgid="6963292714584735149">"Resimler ve videolar"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Fotoğraf Galerisi"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Fotoğraf yok."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Kırpılan resim <xliff:g id="FOLDER_NAME">%s</xliff:g> klasörüne kaydedildi."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Kullanılabilir albüm yok."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Kullanılabilir resim/video yok."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Yayınlar"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Çevrimdışı kullanılabilir yap"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Yenile"</string>
+    <string name="done" msgid="217672440064436595">"Bitti"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d / %2$d öğe:"</string>
+    <string name="title" msgid="7622928349908052569">"Başlık"</string>
+    <string name="description" msgid="3016729318096557520">"Açıklama"</string>
+    <string name="time" msgid="1367953006052876956">"Saat"</string>
+    <string name="location" msgid="3432705876921618314">"Konum"</string>
+    <string name="path" msgid="4725740395885105824">"Yol"</string>
+    <string name="width" msgid="9215847239714321097">"Genişlik"</string>
+    <string name="height" msgid="3648885449443787772">"Yükseklik"</string>
+    <string name="orientation" msgid="4958327983165245513">"Yön"</string>
+    <string name="duration" msgid="8160058911218541616">"Süre"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME türü"</string>
+    <string name="file_size" msgid="8486169301588318915">"Dosya boyutu"</string>
+    <string name="maker" msgid="7921835498034236197">"Yapımcı"</string>
+    <string name="model" msgid="8240207064064337366">"Model"</string>
+    <string name="flash" msgid="2816779031261147723">"Flaş"</string>
+    <string name="aperture" msgid="5920657630303915195">"Diyafram"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Odak Uzaklığı"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Beyaz dengesi"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Pozlama süresi"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"El ile"</string>
+    <string name="auto" msgid="4296941368722892821">"Otomatik"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Flaş patladı"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Flaş yok"</string>
+    <string name="unknown" msgid="3506693015896912952">"Bilinmiyor"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Orijinal"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Klasik"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Şipşak"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Beyazlatma"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Mavi"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Siyah Beyaz"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Zımba"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X İşleme"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Sararmış"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Taş Baskı"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Albüm çevrimdışı kullanıma hazırlanıyor."</item>
+    <item quantity="other" msgid="4948604338155959389">"Albümler çevrimdışı kullanıma hazırlanıyor."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Bu öğe yerel olarak depolandı ve çevrimdışı kullanılabilir."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Tüm albümler"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Yerel albümler"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP cihazlar"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa albümleri"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> boş"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> veya daha küçük"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> veya daha büyük"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> - <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"İçe aktar"</string>
+    <string name="import_complete" msgid="3875040287486199999">"İçe aktrm tamamlandı"</string>
+    <string name="import_fail" msgid="8497942380703298808">"İçe aktarılamadı"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Kamera bağlı."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Kamera bağlantısı kesildi."</string>
+    <string name="click_import" msgid="6407959065464291972">"İçe aktarmak için buraya dokunun"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Albüm seçin"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Tüm resimleri karıştır"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Bir resim seç"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Resimleri seçin"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Slayt gösterisi"</string>
+    <string name="albums" msgid="7320787705180057947">"Albümler"</string>
+    <string name="times" msgid="2023033894889499219">"Saatler"</string>
+    <string name="locations" msgid="6649297994083130305">"Konumlar"</string>
+    <string name="people" msgid="4114003823747292747">"Kişiler"</string>
+    <string name="tags" msgid="5539648765482935955">"Etiketler"</string>
+    <string name="group_by" msgid="4308299657902209357">"Grupla:"</string>
+    <string name="settings" msgid="1534847740615665736">"Ayarlar"</string>
+    <string name="add_account" msgid="4271217504968243974">"Hesap ekle"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"İndir"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Düzenlenen Çevrimiçi Fotoğraflar"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"İçe aktarıldı"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekran görüntüsü"</string>
+    <string name="help" msgid="7368960711153618354">"Yardım"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Depolama yok"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Kullanılabilir harici depolama yok"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Film şeridi görünümü"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Tablo görünümü"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Tam ekran görünümü"</string>
+    <string name="trimming" msgid="9122385768369143997">"Kırpma"</string>
+    <string name="muting" msgid="5094925919589915324">"Ses kapatılıyor"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Lütfen bekleyin"</string>
+    <string name="save_into" msgid="9155488424829609229">"Video <xliff:g id="ALBUM_NAME">%1$s</xliff:g> adlı albüme kaydediliyor…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Kırpılamaz: hedef video çok kısa"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Panorama oluşturuluyor"</string>
+    <string name="save" msgid="613976532235060516">"Kaydet"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"İçerik taranıyor..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d öğe tarandı"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d öğe tarandı"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d öğe tarandı"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Sıralanıyor..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Tarama tamamlandı"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"İçe aktarılıyor..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Bu cihazda içe aktarılacak içerik yok."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Bağlı MTP cihazı yok"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Kamera hatası"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Kameraya bağlanılamıyor."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Kamera, güvenlik politikaları nedeniyle devre dışı bırakıldı."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Kamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Video Kamera"</string>
+    <string name="wait" msgid="8600187532323801552">"Lütfen bekleyin..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Kamerayı kullanmadan önce USB bellek ekleyin."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Kamerayı kullanmadan önce bir SD kart takın."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"USB bellek hazırlanıyor…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"SD kart hazırlanıyor..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"USB belleğe erişilemedi."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"SD karta erişilemedi."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"İPTAL"</string>
+    <string name="review_ok" msgid="1156261588693116433">"BİTTİ"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Zaman atlamalı kayıt"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Kamera seç"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Arka"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Ön"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Depo konumu"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Zamanlayıcı"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 saniye"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d saniye"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Geri sayım sırasında bip sesi"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Kapalı"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Açık"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Video kalitesi"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Yüksek"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Düşük"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Zaman atlama"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Kamera ayarları"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Kamera ayarları"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Resim boyutu"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 M piksel"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M piksel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M piksel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M piksel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3M piksel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M piksel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Odak modu"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Otomatik"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Sonsuz"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Makro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Flaş modu"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Otomatik"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Açık"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Kapalı"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Beyaz dengesi"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Otomatik"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Ampul"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Gün Işığı"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Floresan"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Bulutlu"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Sahne modu"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Otomatik"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"İşlem"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Gece"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Gün batımı"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Parti"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Sahne modunda seçilemez."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Pozlama"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"Tamam"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USB belleğinizde boş alan azaldı. Kalite ayarını değiştirin veya bazı resimleri ya da diğer dosyaları silin."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"SD kartınızda boş alan azaldı. Kalite ayarını değiştirin veya bazı resimleri ya da diğer dosyaları silin."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Boyut sınırına ulaşıldı."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Çok hızlı"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Panorama hazırlanıyor"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Panorama kaydedilemedi."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Panorama kaydediliyor"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Önceki panorama bekleniyor"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Kaydediliyor..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Panorama oluşturuluyor"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Odaklamak için dokunun."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Efektler"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Yok"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Sıkıştır"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Büyük gözler"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Büyük ağız"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Küçük ağız"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Büyük burun"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Küçük gözler"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Uzayda"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Gün Batımı"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Videonuz"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Cihazınızı yerleştirin."\n"Kısa bir süre için görüntüden çıkın."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Kayıt sırasında fotoğraf çekmek için dokunun."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Video kaydı başladı."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Video kaydı durdu."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Özel efektler açıkken video anlık görüntü yakalama devre dışıdır."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Efektleri temizle"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"KOMİK SURATLAR"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ARKA PLAN"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Deklanşör düğmesi"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Menü düğmesi"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"En son fotoğraf"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Ön/arka kamera anahtarı"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Kamera, video veya panorama seçici"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Diğer ayar denetimleri"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Ayar denetimlerini kapat"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Zum denetimi"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"%1$s azalt"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"%1$s artır"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s onay kutusu"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Fotoğrafa geçiş yap"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Videoya geç"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Panorama moduna geç"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Yeni panoramaya geç"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Yorumu iptal et"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Yorum tamamlandı"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"İncelemede tekrar çek"</string>
+    <string name="capital_on" msgid="5491353494964003567">"AÇ"</string>
+    <string name="capital_off" msgid="7231052688467970897">"KAPAT"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Kapalı"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 saniye"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 dakika"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 saat"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 saat"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"saniye"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"dakika"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"saat"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Bitti"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Zaman Aralığını Ayarla"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Zaman atlama özelliği kapalı. Zaman aralığını ayarlamak için etkinleştirin."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Zamanlayıcı kapalı. Fotoğraf çekmeden önce geri saymak için açın."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Süreyi saniye cinsinden ayarlayın"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Fotoğraf çekmek için geri sayılıyor"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Fotoğrafların çekildiği yerler hatırlansın mı?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Fotoğraflarınızı ve videolarınızı çekildikleri konumlarla etiketleyin."\n\n"Diğer uygulamalar, kaydedilen görüntülerle birlikte bu bilgilere erişebilir."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Hayır, teşekkürler"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Evet"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Kamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Ara"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Fotoğraflar"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Albümler"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d fotoğraf"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d fotoğraf"</item>
+  </plurals>
+</resources>
diff --git a/res/values-uk/filtershow_strings.xml b/res/values-uk/filtershow_strings.xml
new file mode 100644
index 0000000..f35d760
--- /dev/null
+++ b/res/values-uk/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Редактор фотографій"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Неможливо завантажити зображення."</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Встановлення фонового малюнка"</string>
+    <string name="original" msgid="3524493791230430897">"Оригінал"</string>
+    <string name="borders" msgid="2067345080568684614">"Облямівка"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Відмінити"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Повторити"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Показати історію"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Сховати історію"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Показ. стан зображ."</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Сховати стан зображ."</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Налаштування"</string>
+    <string name="unsaved" msgid="8704442449002374375">"У зображенні є незбережені зміни."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Зберегти перед виходом?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Зберегти та вийти"</string>
+    <string name="exit" msgid="242642957038770113">"Вийти"</string>
+    <string name="history" msgid="455767361472692409">"Історія"</string>
+    <string name="reset" msgid="9013181350779592937">"Скинути"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Застосовані ефекти"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Порівняти"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Застосувати"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Скинути"</string>
+    <string name="aspect" msgid="4025244950820813059">"Співвідношення"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Немає"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Зафіксовано"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Мала планета"</string>
+    <string name="exposure" msgid="6526397045949374905">"Експозиція"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Різкість"</string>
+    <string name="contrast" msgid="2310908487756769019">"Контраст"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Барвистість"</string>
+    <string name="saturation" msgid="7026791551032438585">"Насиченість"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Ч/б фільтр"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Автоколір"</string>
+    <string name="hue" msgid="6231252147971086030">"Тон"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Тіні"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Затемнення"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Криві"</string>
+    <string name="vignette" msgid="934721068851885390">"Віньєтка"</string>
+    <string name="redeye" msgid="4508883127049472069">"Червоні очі"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Малювати"</string>
+    <string name="straighten" msgid="26025591664983528">"Вирівнювання"</string>
+    <string name="crop" msgid="5781263790107850771">"Обрізати"</string>
+    <string name="rotate" msgid="2796802553793795371">"Обертання"</string>
+    <string name="mirror" msgid="5482518108154883096">"Дзеркало"</string>
+    <string name="negative" msgid="6998313764388022201">"Негатив"</string>
+    <string name="none" msgid="6633966646410296520">"Нічого"</string>
+    <string name="edge" msgid="7036064886242147551">"Краї"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Ворхол"</string>
+    <string name="downsample" msgid="3552938534146980104">"Зниж.якість"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Червоний"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Зелений"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Синій"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Стиль"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Розмір"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Колір"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Лінії"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Маркер"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Розпилення"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Очистити"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Вибрати спеціальний колір"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Вибрати колір"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Вибрати розмір"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"ОК"</string>
+</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
new file mode 100644
index 0000000..fc01c4a
--- /dev/null
+++ b/res/values-uk/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Галерея"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Фото-рамка"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Відеопрогравач"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Завантаж. відео…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"Завантаж. зображ…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Завантаження облік. запису..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Відновити відео"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Продовж. відтворення з %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Віднов. відтвор."</string>
+    <string name="loading" msgid="7038208555304563571">"Завантаж…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Не вдалося завантажити"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Не вдалося завантажити зображення"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Немає ескізу"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Почати знову"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"ОК"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Торкніться обличчя, щоб почати."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Зберіг-ня фото…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Зображення зберігається в альбом <xliff:g id="ALBUM_NAME">%1$s</xliff:g>..."</string>
+    <string name="save_error" msgid="6857408774183654970">"Не вдалося зберегти обрізане зображення."</string>
+    <string name="crop_label" msgid="521114301871349328">"Обрізати фото"</string>
+    <string name="trim_label" msgid="274203231381209979">"Обрізати відео"</string>
+    <string name="select_image" msgid="7841406150484742140">"Виберіть фото"</string>
+    <string name="select_video" msgid="4859510992798615076">"Виберіть відео"</string>
+    <string name="select_item" msgid="2816923896202086390">"Виберіть елемент"</string>
+    <string name="select_album" msgid="1557063764849434077">"Виберіть альбом"</string>
+    <string name="select_group" msgid="6744208543323307114">"Виберіть групу"</string>
+    <string name="set_image" msgid="2331476809308010401">"Устан. фото як"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Фонове зображення"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Встановлення фонового малюнка..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Фоновий мал."</string>
+    <string name="delete" msgid="2839695998251824487">"Видалити"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Видалити вибраний елемент?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Видалити вибрані елементи?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Підтвердити"</string>
+    <string name="cancel" msgid="3637516880917356226">"Скасувати"</string>
+    <string name="share" msgid="3619042788254195341">"Надісл."</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Поділитися панорамою"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Поділитися як фотографією"</string>
+    <string name="deleted" msgid="6795433049119073871">"Видалено"</string>
+    <string name="undo" msgid="2930873956446586313">"ВІДМІНИТИ"</string>
+    <string name="select_all" msgid="3403283025220282175">"Вибрати всі"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Відмінити всі"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Слайд-шоу"</string>
+    <string name="details" msgid="8415120088556445230">"Деталі"</string>
+    <string name="details_title" msgid="2611396603977441273">"Елементи: %1$d з %2$d"</string>
+    <string name="close" msgid="5585646033158453043">"Закрити"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Перейти до програми Камера"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"Вибрано %1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"Вибрано %1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"Вибрано %1$d"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"Вибрано %1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"Вибрано %1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"Вибрано %1$d"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"Вибрано %1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"Вибрано %1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"Вибрано %1$d"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Показ. на карті"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Поверн. вліво"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Поверн. вправо"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Не вдалося знайти елемент."</string>
+    <string name="edit" msgid="1502273844748580847">"Редагувати"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Виконується обробка запитів кешування"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Кешування..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Обрізати"</string>
+    <string name="trim_action" msgid="703098114452883524">"Обрізати"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Вимкнути звук"</string>
+    <string name="set_as" msgid="3636764710790507868">"Устан. як"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Неможливо вимкнути звук."</string>
+    <string name="video_err" msgid="7003051631792271009">"Неможливо відтворити відео."</string>
+    <string name="group_by_location" msgid="316641628989023253">"За місцезнаходженням"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"За часом"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"За тегами"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"За обличчями"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"За альбомами"</string>
+    <string name="group_by_size" msgid="153766174950394155">"За розміром"</string>
+    <string name="untagged" msgid="7281481064509590402">"Без тегів"</string>
+    <string name="no_location" msgid="4043624857489331676">"Без місцезнаходження"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Не вдалося визначити деякі місцезнаходження через проблеми з мережею."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Не вдалося завантажити фото цього альбому. Повторіть спробу пізніше."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Лише зображення"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Лише відео"</string>
+    <string name="show_all" msgid="6963292714584735149">"Зображення й відео"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерея"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Фотографій немає."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Обрізане зображення збережено в папці <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Немає доступних альбомів."</string>
+    <string name="empty_album" msgid="4542880442593595494">"Немає доступних зображень або відео."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Публікації"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Завантажити для доступу офлайн"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Оновити"</string>
+    <string name="done" msgid="217672440064436595">"Готово"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d з %2$d елем.:"</string>
+    <string name="title" msgid="7622928349908052569">"Назва"</string>
+    <string name="description" msgid="3016729318096557520">"Опис"</string>
+    <string name="time" msgid="1367953006052876956">"Час"</string>
+    <string name="location" msgid="3432705876921618314">"Місце"</string>
+    <string name="path" msgid="4725740395885105824">"Шлях"</string>
+    <string name="width" msgid="9215847239714321097">"Ширина"</string>
+    <string name="height" msgid="3648885449443787772">"Висота"</string>
+    <string name="orientation" msgid="4958327983165245513">"Орієнтація"</string>
+    <string name="duration" msgid="8160058911218541616">"Тривалість"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Тип MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Розмір файлу"</string>
+    <string name="maker" msgid="7921835498034236197">"Автор"</string>
+    <string name="model" msgid="8240207064064337366">"Модель"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Апертура"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Фокусна відст."</string>
+    <string name="white_balance" msgid="1582509289994216078">"Баланс білого"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Час експозиції"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"мм"</string>
+    <string name="manual" msgid="6608905477477607865">"Вручну"</string>
+    <string name="auto" msgid="4296941368722892821">"Автомат."</string>
+    <string name="flash_on" msgid="7891556231891837284">"Викор. спалах"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Без спалаху"</string>
+    <string name="unknown" msgid="3506693015896912952">"Невідомо"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Оригінал"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Ретро"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Миттєво"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Вибілення"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Синява"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Чорно-біле"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Стиснути"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X-процес"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Латте"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Літографія"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Надання доступу до альбому в режимі офлайн."</item>
+    <item quantity="other" msgid="4948604338155959389">"Надання доступу до альбомів у режимі офлайн."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Цей елемент зберігається локально та доступний у режимі офлайн."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Усі альбоми"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Локальні альбоми"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Пристрої MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Альбоми Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"Вільно <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> або менше"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> або більше"</string>
+    <string name="size_between" msgid="8779660840898917208">"від <xliff:g id="MIN_SIZE">%1$s</xliff:g> до <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Імпортувати"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Імпорт завершено"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Помилка імпорту"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Камеру підключено."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Камеру відключено."</string>
+    <string name="click_import" msgid="6407959065464291972">"Торкніться тут, щоб імпортувати"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Вибрати альбом"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Перемішати всі зображення"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Вибрати зображення"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Вибрати зображення"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Слайд-шоу"</string>
+    <string name="albums" msgid="7320787705180057947">"Альбоми"</string>
+    <string name="times" msgid="2023033894889499219">"Час"</string>
+    <string name="locations" msgid="6649297994083130305">"Місцезнах."</string>
+    <string name="people" msgid="4114003823747292747">"Люди"</string>
+    <string name="tags" msgid="5539648765482935955">"Теги"</string>
+    <string name="group_by" msgid="4308299657902209357">"Групувати за"</string>
+    <string name="settings" msgid="1534847740615665736">"Налаштування"</string>
+    <string name="add_account" msgid="4271217504968243974">"Додати обліковий запис"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"З камери"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Звантаження"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Фото, відредаговані онлайн"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Імпортовані"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Знімки екрана"</string>
+    <string name="help" msgid="7368960711153618354">"Довідка"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Немає пам’яті"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Доступної зовнішньої пам’яті немає"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Діафільм"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Ескізи"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"На весь екран"</string>
+    <string name="trimming" msgid="9122385768369143997">"Обрізання"</string>
+    <string name="muting" msgid="5094925919589915324">"Вимкнення звуку"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Зачекайте"</string>
+    <string name="save_into" msgid="9155488424829609229">"Зберігання відео в альбом <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Неможливо обрізати: цільове відео закоротке"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Обробка панорами"</string>
+    <string name="save" msgid="613976532235060516">"Зберегти"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Сканування вмісту..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"Проскановано елементів: %1$d"</item>
+    <item quantity="one" msgid="4340019444460561648">"Проскановано елементів: %1$d"</item>
+    <item quantity="other" msgid="3138021473860555499">"Проскановано елементів: %1$d"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Сортування..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Сканування завершено"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Імпортування..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Немає вмісту для імпортування на цей пристрій."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Немає жодного під’єднаного носія MTP"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Помилка камери"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Неможливо підключитися до камери."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Камеру вимкнено відповідно до правил безпеки."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Камера"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Відеокамера"</string>
+    <string name="wait" msgid="8600187532323801552">"Зачекайте…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Підключіть носій USB перед тим, як користуватися камерою."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Вставте карту SD перед тим, як користуватися камерою."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Підготовка носія USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Підготовка карти SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Не вдалося отримати доступ до носія USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Не вдалося отримати доступ до карти SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"СКАСУВАТИ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"ГОТОВО"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Запис уповільненої зйомки"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Вибрати камеру"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Задня камера"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Передня камера"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Геотеги"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Таймер зворотного відліку"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 с"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d с"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Сигнал під час відліку"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Вимкнено"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Увімкнено"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Якість відео"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Висока"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Низька"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Уповільнена зйомка"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Налаштування камери"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Налашт-ня відеокамери"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Розмір фото"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8 мегапікселів"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5 Мпікс"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3 Мпікс"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2 Мпікс"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3 Mпікс"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1 Мпікс"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Режим фокусу"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Автом."</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Безкінечн."</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Макро"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Режим спалаху"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Автом."</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Увімк."</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Вимк."</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Баланс білого"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Автом."</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Лампа розжар."</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Сонячно"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Флуоресцентний"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Хмарно"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Режим зйомки"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Автом."</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Спорт"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Ніч"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Захід сонця"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"У приміщенні"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Неможливо вибрати в режимі зйомки."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Експозиція"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"ОК"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"На носії USB недостатньо місця. Змініть налаштування якості чи видаліть зображення або інші файли."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"На карті SD недостатньо місця. Змініть налаштування якості чи видаліть зображення або інші файли."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Досягн. макс. розмір."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Зашвидко"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Підготовка панорами"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Не вдалося зберегти панораму."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Панорама"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Панорамна зйомка"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Очікування на попередню панораму"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Збереження..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Обробка панорами"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Торкніться, щоб фокусувати."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Ефекти"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Немає"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Стиснення"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Великі очі"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Великий рот"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Маленький рот"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Великий ніс"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Маленькі очі"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"У космосі"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Захід сонця"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Ваше відео"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Покладіть свій пристрій."\n"На декілька секунд вийдіть із поля зору."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Торкніться, щоб сфотографувати під час запису."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Запис відео розпочався."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Запис відео припинився."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Функцію миттєвого знімка відео вимкнено, коли ввімк. спецефекти."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Очистити ефекти"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"КУМЕДНІ ОБЛИЧЧЯ"</string>
+    <string name="effect_background" msgid="6579360207378171022">"ФОН"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Кнопка \"Витримка\""</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Кнопка меню"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Останні фото"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Перемикач між передньою та задньою камерами"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Перемикач фото, відео чи панорами"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Інші елементи керування налаштуваннями"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Закрити елементи керування налаштуваннями"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Керувати масштабом"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Зменшити %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Збільшити %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Прапорець %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Перейти в режим фото"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Перейти в режим відео"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Перейти в режим панорами"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Перейти в режим нової панорами"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Cкасувати перегляд"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Виконати перегляд"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Повторити в режимі перегляду"</string>
+    <string name="capital_on" msgid="5491353494964003567">"УВІМКНЕНО"</string>
+    <string name="capital_off" msgid="7231052688467970897">"ВИМКНЕНО"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Вимкнено"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 секунда"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 секунд"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 секунди"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 хвилини"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 хвилина"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 хвилини"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 хвилини"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 хвилини"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 хвилини"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 хвилини"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 хвилин"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 хвилин"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 хвилин"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 хвилин"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 хвилин"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 хвилини"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 години"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 година"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 години"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 години"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 години"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 години"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 години"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 годин"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 годин"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 годин"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 годин"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 годин"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 години"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"с."</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"хв."</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"год."</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Готово"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Установити інтервал часу"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Функцію уповільненої зйомки вимкнено. Щоб установити інтервал часу, увімкніть її."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Таймер зворотного відліку вимкнено. Увімкніть його, щоб розпочати відлік перед зйомкою."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Установити тривалість у секундах"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Відлік перед зйомкою"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Зберігати місця з фотографій?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Додавайте до своїх фотографій і відео теги про місця, де їх було зроблено."\n\n"Інші програми можуть отримувати доступ до цієї інформації, а також ваших збережених зображень."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Ні, дякую"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Так"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Камера"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Пошук"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Фотографії"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Альбоми"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d фото"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d фото"</item>
+  </plurals>
+</resources>
diff --git a/res/values-v11/styles.xml b/res/values-v11/styles.xml
new file mode 100644
index 0000000..d4105a3
--- /dev/null
+++ b/res/values-v11/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android">
+    <!-- When an activity requests a theme with an action bar from its manifest,
+         the activity preview window created by the system process while the
+         real activity is loading will also contain an action bar. Set this to
+         NoActionBar and change the theme in onCreate. -->
+    <style name="Theme.CameraBase" parent="android:Theme.Holo.NoActionBar.Fullscreen"/>
+    <style name="Widget.Button.Borderless" parent="android:Widget.Holo.Button.Borderless"/>
+</resources>
+
diff --git a/res/values-v13/styles.xml b/res/values-v13/styles.xml
new file mode 100644
index 0000000..10162b0
--- /dev/null
+++ b/res/values-v13/styles.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <style name="TextAppearance.DialogWindowTitle" parent="@android:style/TextAppearance.Holo.DialogWindowTitle"/>
+    <style name="TextAppearance.Medium" parent="@android:style/TextAppearance.Holo.Medium"/>
+</resources>
+
diff --git a/res/values-v14/styles.xml b/res/values-v14/styles.xml
new file mode 100644
index 0000000..c05bf30
--- /dev/null
+++ b/res/values-v14/styles.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <style name="Theme.GalleryBase" parent="android:Theme.Holo">
+        <item name="listPreferredItemHeightSmall">?android:attr/listPreferredItemHeightSmall</item>
+        <item name="switchStyle">@android:style/Widget.CompoundButton</item>
+    </style>
+    <style name="ActionBarTwoLineItem">
+        <item name="android:background">?android:attr/activatedBackgroundIndicator</item>
+    </style>
+    <style name="Theme.Photos.Gallery" parent="android:Theme.Holo.Light">
+    </style>
+    <style name="Theme.Photos.Fullscreen" parent="android:Theme.Holo">
+    </style>
+</resources>
diff --git a/res/values-vi/filtershow_strings.xml b/res/values-vi/filtershow_strings.xml
new file mode 100644
index 0000000..60b2f03
--- /dev/null
+++ b/res/values-vi/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Trình chỉnh sửa ảnh"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Không thể tải hình ảnh!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Đang đặt hình nền"</string>
+    <string name="original" msgid="3524493791230430897">"Gốc"</string>
+    <string name="borders" msgid="2067345080568684614">"Đường viền"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Hoàn tác"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Làm lại"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Hiển thị lịch sử"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Ẩn lịch sử"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Hiện tr.thái hình ảnh"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Ẩn tr.thái hình ảnh"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Cài đặt"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Có các thay đổi chưa được lưu đối với hình ảnh này."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Bạn có muốn lưu trước khi thoát không?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Lưu và thoát"</string>
+    <string name="exit" msgid="242642957038770113">"Thoát"</string>
+    <string name="history" msgid="455767361472692409">"Lịch sử"</string>
+    <string name="reset" msgid="9013181350779592937">"Đặt lại"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Các hiệu ứng được áp dụng"</string>
+    <string name="compare_original" msgid="8140838959007796977">"So sánh"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Áp dụng"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Đặt lại"</string>
+    <string name="aspect" msgid="4025244950820813059">"Tỷ lệ"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Không có"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Cố định"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Hành tinh nhỏ"</string>
+    <string name="exposure" msgid="6526397045949374905">"Độ phơi sáng"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Độ sắc nét"</string>
+    <string name="contrast" msgid="2310908487756769019">"Độ tương phản"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Dao động"</string>
+    <string name="saturation" msgid="7026791551032438585">"Bão hòa"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Bộ lọc ĐT"</string>
+    <string name="wbalance" msgid="6346581563387083613">"Màu tự động"</string>
+    <string name="hue" msgid="6231252147971086030">"Màu sắc"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Bóng"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Vùng sáng"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Đồ thị màu"</string>
+    <string name="vignette" msgid="934721068851885390">"Làm mờ nét ảnh"</string>
+    <string name="redeye" msgid="4508883127049472069">"Mắt đỏ"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Vẽ"</string>
+    <string name="straighten" msgid="26025591664983528">"Làm thẳng"</string>
+    <string name="crop" msgid="5781263790107850771">"Cắt"</string>
+    <string name="rotate" msgid="2796802553793795371">"Xoay"</string>
+    <string name="mirror" msgid="5482518108154883096">"Phản chiếu"</string>
+    <string name="negative" msgid="6998313764388022201">"Âm bản"</string>
+    <string name="none" msgid="6633966646410296520">"Không có"</string>
+    <string name="edge" msgid="7036064886242147551">"Cạnh"</string>
+    <string name="kmeans" msgid="1630263230946107457">"Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"Giảm mẫu"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Đỏ"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Xanh lục"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Xanh lam"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Kiểu"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Kích thước"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Màu"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Đường vẽ"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Bút dạ"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"Bút vẽ"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Xóa"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Chọn màu tùy chỉnh"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Chọn màu"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Chọn kích thước"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"OK"</string>
+</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
new file mode 100644
index 0000000..6672f13
--- /dev/null
+++ b/res/values-vi/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Thư viện"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Khung ảnh"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Trình phát video"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Đang tải video..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Đang tải ảnh…"</string>
+    <string name="loading_account" msgid="928195413034552034">"Đang tải tài khoản…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Tiếp tục video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Tiếp tục phát từ %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Tiếp tục phát"</string>
+    <string name="loading" msgid="7038208555304563571">"Đang tải…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Không thể tải"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Không thể tải hình ảnh"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Không có hình thu nhỏ"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Bắt đầu lại"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Chạm vào một khuôn  mặt để bắt đầu."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Đang lưu ảnh…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Đang lưu ảnh vào <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="save_error" msgid="6857408774183654970">"Không thể lưu hình ảnh được cắt."</string>
+    <string name="crop_label" msgid="521114301871349328">"Cắt ảnh"</string>
+    <string name="trim_label" msgid="274203231381209979">"Cắt ngắn video"</string>
+    <string name="select_image" msgid="7841406150484742140">"Chọn ảnh"</string>
+    <string name="select_video" msgid="4859510992798615076">"Chọn video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Chọn mục"</string>
+    <string name="select_album" msgid="1557063764849434077">"Chọn album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Chọn nhóm"</string>
+    <string name="set_image" msgid="2331476809308010401">"Đặt ảnh làm"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Đặt hình nền"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Đang đặt hình nền..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Hình nền"</string>
+    <string name="delete" msgid="2839695998251824487">"Xóa"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Xóa mục đã chọn?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Xóa các mục đã chọn?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Xác nhận"</string>
+    <string name="cancel" msgid="3637516880917356226">"Hủy"</string>
+    <string name="share" msgid="3619042788254195341">"Chia sẻ"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Chia sẻ ảnh toàn cảnh"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Chia sẻ dưới dạng ảnh"</string>
+    <string name="deleted" msgid="6795433049119073871">"Đã xóa"</string>
+    <string name="undo" msgid="2930873956446586313">"HOÀN TÁC"</string>
+    <string name="select_all" msgid="3403283025220282175">"Chọn tất cả"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Bỏ chọn tất cả"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Trình chiếu"</string>
+    <string name="details" msgid="8415120088556445230">"Chi tiết"</string>
+    <string name="details_title" msgid="2611396603977441273">"%1$d trong tổng số %2$d mục:"</string>
+    <string name="close" msgid="5585646033158453043">"Đóng"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Chuyển sang máy ảnh"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d mục được chọn"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d mục được chọn"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d mục được chọn"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d album được chọn"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d album được chọn"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d album được chọn"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d nhóm được chọn"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d nhóm được chọn"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d nhóm được chọn"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Hiển thị trên bản đồ"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Xoay trái"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Xoay phải"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Không thể tìm thấy mục."</string>
+    <string name="edit" msgid="1502273844748580847">"Chỉnh sửa"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Đang xử lý yêu cầu lưu vào bộ nhớ cache"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Lưu cache..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Cắt"</string>
+    <string name="trim_action" msgid="703098114452883524">"Cắt"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Ẩn"</string>
+    <string name="set_as" msgid="3636764710790507868">"Đặt làm"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Không thể ẩn video."</string>
+    <string name="video_err" msgid="7003051631792271009">"Không thể phát video."</string>
+    <string name="group_by_location" msgid="316641628989023253">"Theo vị trí"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Theo thời gian"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Theo thẻ"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Theo người"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Theo album"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Theo kích thước"</string>
+    <string name="untagged" msgid="7281481064509590402">"Không được gắn thẻ"</string>
+    <string name="no_location" msgid="4043624857489331676">"Không có vị trí nào"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Không thể xác định một số vị trí do sự cố mạng."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Không thể tải xuống ảnh trong album này. Hãy thử lại sau."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Chỉ hình ảnh"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Chỉ video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Hình ảnh &amp; video"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Thư viện ảnh"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Không có ảnh."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Đã lưu hình ảnh được cắt vào <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Không có album nào."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O có hình ảnh/video nào."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Bài đăng"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Cho phép ngoại tuyến"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Làm mới"</string>
+    <string name="done" msgid="217672440064436595">"Xong"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d mục:"</string>
+    <string name="title" msgid="7622928349908052569">"Tiêu đề"</string>
+    <string name="description" msgid="3016729318096557520">"Mô tả"</string>
+    <string name="time" msgid="1367953006052876956">"Thời gian"</string>
+    <string name="location" msgid="3432705876921618314">"Vị trí"</string>
+    <string name="path" msgid="4725740395885105824">"Đường dẫn"</string>
+    <string name="width" msgid="9215847239714321097">"Chiều rộng"</string>
+    <string name="height" msgid="3648885449443787772">"Chiều cao"</string>
+    <string name="orientation" msgid="4958327983165245513">"Hướng"</string>
+    <string name="duration" msgid="8160058911218541616">"Thời lượng"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Loại MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Kích thước tệp"</string>
+    <string name="maker" msgid="7921835498034236197">"Trình tạo"</string>
+    <string name="model" msgid="8240207064064337366">"Mẫu"</string>
+    <string name="flash" msgid="2816779031261147723">"Flash"</string>
+    <string name="aperture" msgid="5920657630303915195">"Khẩu độ"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Tiêu cự"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Cân bằng trắng"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Thời gian phơi sáng"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Thủ công"</string>
+    <string name="auto" msgid="4296941368722892821">"Tự động"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Sử dụng flash"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Không có flash"</string>
+    <string name="unknown" msgid="3506693015896912952">"Không xác định"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Gốc"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"Cổ điển"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Instant"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Làm phai màu"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Màu xanh dương"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"Đen/trắng"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"Quá trình X"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Đặt album khả dụng ở chế độ ngoại tuyến."</item>
+    <item quantity="other" msgid="4948604338155959389">"Đặt album khả dụng ở chế độ ngoại tuyến."</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Mục này được lưu cục bộ và khả dụng ngoại tuyến."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Tất cả album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Album cục bộ"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Thiết bị MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Album Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> trống"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> trở xuống"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> hoặc cao hơn"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> tới <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Nhập"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Nhập xong"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Nhập không thành công"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Đã kết nối máy ảnh."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Đã ngắt kết nối máy ảnh."</string>
+    <string name="click_import" msgid="6407959065464291972">"Chạm vào đây để nhập"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Chọn album"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Hiển thị ngẫu nhiên tất cả hình ảnh"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Chọn ảnh"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Chọn hình ảnh"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Trình chiếu"</string>
+    <string name="albums" msgid="7320787705180057947">"Album"</string>
+    <string name="times" msgid="2023033894889499219">"Lần"</string>
+    <string name="locations" msgid="6649297994083130305">"Vị trí"</string>
+    <string name="people" msgid="4114003823747292747">"Danh bạ"</string>
+    <string name="tags" msgid="5539648765482935955">"Thẻ"</string>
+    <string name="group_by" msgid="4308299657902209357">"Nhóm theo"</string>
+    <string name="settings" msgid="1534847740615665736">"Cài đặt"</string>
+    <string name="add_account" msgid="4271217504968243974">"Thêm tài khoản"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Máy ảnh"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Tải xuống"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Ảnh trực tuyến đã chỉnh sửa"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Đã nhập"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ảnh chụp màn hình"</string>
+    <string name="help" msgid="7368960711153618354">"Trợ giúp"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Không có bộ nhớ nào"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Không có bộ nhớ ngoài nào"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Chế độ xem cuộn phim"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Chế độ xem lưới"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Xem toàn màn hình"</string>
+    <string name="trimming" msgid="9122385768369143997">"Đang cắt ngắn"</string>
+    <string name="muting" msgid="5094925919589915324">"Đang ẩn"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Vui lòng chờ"</string>
+    <string name="save_into" msgid="9155488424829609229">"Đang lưu video vào <xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Không thể cắt ngắn : video đích quá ngắn"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Hiển thị ảnh toàn cảnh"</string>
+    <string name="save" msgid="613976532235060516">"Lưu"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Đang quét nội dung..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d mục được quét"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d mục được quét"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d mục được quét"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Đang sắp xếp..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Đã quét xong"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Đang nhập..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Không có nội dung nào để nhập vào thiết bị này."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Không có thiết bị MTP nào được kết nối"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Lỗi máy ảnh"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Không thể kết nối với máy ảnh."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Máy ảnh đã bị tắt do chính sách bảo mật."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Máy ảnh"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Máy quay video"</string>
+    <string name="wait" msgid="8600187532323801552">"Vui lòng đợi..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Kết nối bộ nhớ USB trước khi sử dụng máy ảnh."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Lắp thẻ SD trước khi sử dụng máy ảnh."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Đang chuẩn bị bộ nhớ USB…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Đang chuẩn bị thẻ SD…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Không thể truy cập bộ nhớ USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Không thể truy cập thẻ SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"HUỶ"</string>
+    <string name="review_ok" msgid="1156261588693116433">"XONG"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Đang ghi âm khoảng thời gian"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Chọn máy ảnh"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Quay lại"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Trước"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Vị trí lưu trữ"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Đồng hồ đếm ngược"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 giây"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d giây"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Kêu bíp khi đếm ngược"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Tắt"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Bật"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Chất lượng video"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Cao"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Thấp"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Chế độ thời gian trôi"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Cài đặt máy ảnh"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Cài đặt máy quay video"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Kích thước ảnh"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M pixel"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M pixel"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M pixel"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M pixel"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1,3M pixel"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M pixel"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Chế độ tiêu điểm"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Tự động"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Vô cùng"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Macro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Chế độ flash"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Tự động"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Bật"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Tắt"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Cân bằng trắng"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Tự động"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Ánh sáng nóng"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Ánh sáng ban ngày"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Huỳnh quang"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Nhiều mây"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Chế độ chụp cảnh"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Tự động"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Tác vụ"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Ban đêm"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Hoàng hôn"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Bữa tiệc"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Không thể chọn trong chế độ cảnh."</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Phơi sáng"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"OK"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Bộ nhớ USB của bạn sắp hết dung lượng. Thay đổi cài đặt chất lượng hoặc xóa một số ảnh hoặc tệp khác."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Thẻ SD của bạn sắp hết dung lượng. Thay đổi cài đặt chất lượng hoặc xóa một số ảnh hoặc tệp khác."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Đã đạt tới giới hạn kích thước."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Quá nhanh"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Đang tạo xem trước toàn cảnh"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Không thể lưu toàn cảnh."</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"Toàn cảnh"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Đang chụp toàn cảnh"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Đang đợi ảnh toàn cảnh trước đó"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Đang lưu…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Đang hiển thị ảnh toàn cảnh"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Chạm để lấy tiêu cự."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Hiệu ứng"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Bỏ chọn tất cả"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Xoa"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Đôi mắt to"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Miệng lớn"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Miệng nhỏ"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Mũi to"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Đôi mắt nhỏ"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Trong không gian"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Hoàng hôn"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Video của bạn"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Dừng thiết bị của bạn."\n"Thoát khỏi chế độ xem trong giây lát."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Chạm để chụp ảnh trong khi quay."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Đã bắt đầu quay video."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Đã dừng quay video."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Chụp nhanh video bị vô hiệu khi các hiệu ứng đặc biệt được bật."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Xóa hiệu ứng"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"MẶT XẤU"</string>
+    <string name="effect_background" msgid="6579360207378171022">"MÀU NỀN"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Nút chụp"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Nút trình đơn"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Ảnh gần đây nhất"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Chuyển đổi giữa máy ảnh trước và sau"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Bộ chọn chế độ máy ảnh, video hoặc toàn cảnh"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Kiểm soát cài đặt khác"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Đóng kiểm soát cài đặt"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Điều khiển thu phóng"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Giảm %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Tăng %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"Hộp kiểm %1$s"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Chuyển sang ảnh"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Chuyển sang video"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Chuyển sang chế độ toàn cảnh"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Chuyển sang chế độ toàn cảnh mới"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Hủy bài đánh giá"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Đánh giá xong"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Xem lại ảnh chụp lại"</string>
+    <string name="capital_on" msgid="5491353494964003567">"BẬT"</string>
+    <string name="capital_off" msgid="7231052688467970897">"TẮT"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Tắt"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0,5 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1,5 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2,5 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 giây"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0,5 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1,5 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2,5 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 phút"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0,5 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1,5 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2,5 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 giờ"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 giờ"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"giây"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"phút"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"giờ"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Xong"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Đặt khoảng thời gian"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Tính năng thời gian trôi bị tắt. Bật tính năng này để đặt khoảng thời gian."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Đồng hồ đếm ngược bị tắt. Bật đồng hồ để đếm ngược trước khi chụp ảnh."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Đặt thời gian trong vài giây"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Đếm ngược để chụp ảnh"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Nhớ vị trí chụp ảnh?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Gắn thẻ cho ảnh và video của bạn với những địa điểm mà ảnh đó được chụp và video đó được quay."\n\n"Các ứng dụng khác có thể truy cập vào thông tin này cùng với các hình ảnh đã lưu của bạn."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Không, cảm ơn"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Có"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Máy ảnh"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Tìm kiếm"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Ảnh"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Album"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d ảnh"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d ảnh"</item>
+  </plurals>
+</resources>
diff --git a/res/values-w1024dp/strings.xml b/res/values-w1024dp/strings.xml
new file mode 100644
index 0000000..39903f4
--- /dev/null
+++ b/res/values-w1024dp/strings.xml
@@ -0,0 +1,42 @@
+<?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">
+    <!-- String indicating how many media item(s) is(are) selected
+            eg. 1 item selected [CHAR LIMIT=30] -->
+    <plurals name="number_of_items_selected">
+        <item quantity="zero">%1$d item selected</item>
+        <item quantity="one">%1$d item selected</item>
+        <item quantity="other">%1$d items selected</item>
+    </plurals>
+
+    <!-- String indicating how many media album(s) is(are) selected
+            eg. 1 album selected [CHAR LIMIT=30] -->
+    <plurals name="number_of_albums_selected">
+        <item quantity="zero">%1$d album selected</item>
+        <item quantity="one">%1$d album selected</item>
+        <item quantity="other">%1$d albums selected</item>
+    </plurals>
+
+    <!-- String indicating how many media group(s) is(are) selected
+            eg. 1 group selected [CHAR LIMIT=30] -->
+    <plurals name="number_of_groups_selected">
+        <item quantity="zero">%1$d group selected</item>
+        <item quantity="one">%1$d group selected</item>
+        <item quantity="other">%1$d groups selected</item>
+    </plurals>
+
+</resources>
\ No newline at end of file
diff --git a/res/values-w480dp/bool.xml b/res/values-w480dp/bool.xml
new file mode 100644
index 0000000..6d69b44
--- /dev/null
+++ b/res/values-w480dp/bool.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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>
+    <bool name="show_action_bar_title">true</bool>
+</resources>
\ No newline at end of file
diff --git a/res/values-xlarge-land/drawable.xml b/res/values-xlarge-land/drawable.xml
new file mode 100644
index 0000000..2994479
--- /dev/null
+++ b/res/values-xlarge-land/drawable.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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.
+-->
+<!-- Caution: Don't merge with res/values-xlarge-port/drawable.xml, otherwise
+     the resources listed in this file wouldn't be reloaded when device
+     orientation changes. -->
+<resources>
+    <item name="btn_video_shutter_recording_holo" type="drawable">@drawable/btn_video_shutter_recording_holo_xlarge</item>
+    <item name="btn_video_shutter_recording_pressed_holo" type="drawable">@drawable/btn_video_shutter_recording_pressed_holo_xlarge</item>
+</resources>
diff --git a/res/values-xlarge-port/drawable.xml b/res/values-xlarge-port/drawable.xml
new file mode 100644
index 0000000..caacf47
--- /dev/null
+++ b/res/values-xlarge-port/drawable.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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.
+-->
+<!-- Caution: Don't merge with res/values-xlarge-land/drawable.xml, otherwise
+     the resources listed in this file wouldn't be reloaded when device
+     orientation changes. -->
+<resources>
+    <item name="btn_video_shutter_recording_holo" type="drawable">@drawable/btn_video_shutter_recording_holo_xlarge</item>
+    <item name="btn_video_shutter_recording_pressed_holo" type="drawable">@drawable/btn_video_shutter_recording_pressed_holo_xlarge</item>
+</resources>
diff --git a/res/values-xlarge/dimens.xml b/res/values-xlarge/dimens.xml
new file mode 100644
index 0000000..51b3dad
--- /dev/null
+++ b/res/values-xlarge/dimens.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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>
+    <dimen name="pano_mosaic_surface_height">@dimen/pano_mosaic_surface_height_xlarge</dimen>
+    <dimen name="pano_review_button_width">@dimen/pano_review_button_width_xlarge</dimen>
+    <dimen name="pano_review_button_height">@dimen/pano_review_button_height_xlarge</dimen>
+    <dimen name="setting_row_height">@dimen/setting_row_height_xlarge</dimen>
+    <dimen name="setting_item_text_size">@dimen/setting_item_text_size_xlarge</dimen>
+    <dimen name="setting_knob_width">@dimen/setting_knob_width_xlarge</dimen>
+    <dimen name="setting_item_text_width">@dimen/setting_item_text_width_xlarge</dimen>
+    <dimen name="setting_popup_window_width">@dimen/setting_popup_window_width_xlarge</dimen>
+    <dimen name="setting_item_list_margin">@dimen/setting_item_list_margin_xlarge</dimen>
+    <dimen name="indicator_bar_width">@dimen/indicator_bar_width_xlarge</dimen>
+    <dimen name="popup_title_text_size">@dimen/popup_title_text_size_xlarge</dimen>
+    <dimen name="popup_title_frame_min_height">@dimen/popup_title_frame_min_height_xlarge</dimen>
+    <dimen name="big_setting_popup_window_width">@dimen/big_setting_popup_window_width_xlarge</dimen>
+    <dimen name="setting_item_icon_width">@dimen/setting_item_icon_width_xlarge</dimen>
+    <dimen name="effect_setting_item_icon_width">@dimen/effect_setting_item_icon_width_xlarge</dimen>
+    <dimen name="effect_setting_item_text_size">@dimen/effect_setting_item_text_size_xlarge</dimen>
+    <dimen name="effect_setting_type_text_size">@dimen/effect_setting_type_text_size_xlarge</dimen>
+    <dimen name="effect_setting_type_text_min_height">@dimen/effect_setting_type_text_min_height_xlarge</dimen>
+    <dimen name="effect_setting_clear_text_size">@dimen/effect_setting_clear_text_size_xlarge</dimen>
+    <dimen name="effect_setting_clear_text_min_height">@dimen/effect_setting_clear_text_min_height_xlarge</dimen>
+    <dimen name="effect_setting_type_text_left_padding">@dimen/effect_setting_type_text_left_padding_xlarge</dimen>
+    <dimen name="onscreen_indicators_height">@dimen/onscreen_indicators_height_xlarge</dimen>
+    <dimen name="onscreen_exposure_indicator_text_size">@dimen/onscreen_exposure_indicator_text_size_xlarge</dimen>
+</resources>
+
diff --git a/res/values-xlarge/dimensions.xml b/res/values-xlarge/dimensions.xml
new file mode 100644
index 0000000..85329e0
--- /dev/null
+++ b/res/values-xlarge/dimensions.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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>
+    <dimen name="appwidget_width">240dp</dimen>
+    <dimen name="appwidget_height">240dp</dimen>
+    <dimen name="stack_photo_width">220dp</dimen>
+    <dimen name="stack_photo_height">165dp</dimen>
+
+    <!-- configuration for album set page -->
+    <integer name="albumset_rows_land">3</integer>
+    <integer name="albumset_rows_port">5</integer>
+    <dimen name="albumset_title_font_size">14sp</dimen>
+    <dimen name="albumset_count_font_size">11sp</dimen>
+    <dimen name="albumset_title_right_margin">23dp</dimen>
+    <dimen name="albumset_icon_size">27dp</dimen>
+
+    <!-- configuration for album page -->
+    <integer name="album_rows_land">3</integer>
+    <integer name="album_rows_port">5</integer>
+</resources>
diff --git a/res/values-xlarge/drawable.xml b/res/values-xlarge/drawable.xml
new file mode 100644
index 0000000..648f1d7
--- /dev/null
+++ b/res/values-xlarge/drawable.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2012, 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 name="ic_effects_holo_light" type="drawable">@drawable/ic_effects_holo_light_xlarge</item>
+    <item name="ic_pan_border_fast" type="drawable">@drawable/ic_pan_border_fast_xlarge</item>
+    <item name="ic_pan_left_indicator_fast" type="drawable">@drawable/ic_pan_left_indicator_fast_xlarge</item>
+    <item name="ic_pan_left_indicator" type="drawable">@drawable/ic_pan_left_indicator_xlarge</item>
+    <item name="ic_pan_progression" type="drawable">@drawable/ic_pan_progression_xlarge</item>
+    <item name="ic_pan_right_indicator_fast" type="drawable">@drawable/ic_pan_right_indicator_fast_xlarge</item>
+    <item name="ic_pan_right_indicator" type="drawable">@drawable/ic_pan_right_indicator_xlarge</item>
+    <item name="ic_scn_holo_light" type="drawable">@drawable/ic_scn_holo_light_xlarge</item>
+    <item name="ic_snapshot_border" type="drawable">@drawable/ic_snapshot_border_xlarge</item>
+    <item name="ic_switch_photo_facing_holo_light" type="drawable">@drawable/ic_switch_photo_facing_holo_light_xlarge</item>
+    <item name="ic_switch_video_facing_holo_light" type="drawable">@drawable/ic_switch_video_facing_holo_light_xlarge</item>
+    <item name="ic_timelapse_none" type="drawable">@drawable/ic_timelapse_none_xlarge</item>
+</resources>
diff --git a/res/values-xlarge/filtershow_values.xml b/res/values-xlarge/filtershow_values.xml
new file mode 100644
index 0000000..c6fe399
--- /dev/null
+++ b/res/values-xlarge/filtershow_values.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <!-- Specify the screen orientation -->
+    <bool name="only_use_portrait">false</bool>
+
+    <!-- Category Panel Height -->
+    <dimen name="category_panel_height">106dip</dimen>
+
+    <!-- Category Panel Icon Size -->
+    <dimen name="category_panel_icon_size">84dip</dimen>
+
+    <!-- Category Panel Text Size -->
+    <dimen name="category_panel_text_size">14dip</dimen>
+
+    <!-- Category Panel Text Size -->
+    <dimen name="category_panel_margin">4dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-xlarge/styles.xml b/res/values-xlarge/styles.xml
new file mode 100644
index 0000000..55b29b1
--- /dev/null
+++ b/res/values-xlarge/styles.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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">
+    <style name="DialogPickerTheme" parent="android:Theme.Holo.Dialog">
+    </style>
+    <bool name="picker_is_dialog">true</bool>
+
+    <!-- Camera resources below -->
+
+    <style name="ReviewControlText" parent="@style/ReviewControlText_xlarge" />
+    <style name="PopupTitleText" parent="@style/PopupTitleText_xlarge" />
+    <style name="PanoCustomDialogText" parent="@style/PanoCustomDialogText_xlarge" />
+    <style name="ViewfinderLabelLayout" parent="@style/ViewfinderLabelLayout_xlarge" />
+    <style name="SettingPopupWindow" parent="@style/SettingPopupWindow_xlarge" />
+
+</resources>
diff --git a/res/values-zh-rCN/filtershow_strings.xml b/res/values-zh-rCN/filtershow_strings.xml
new file mode 100644
index 0000000..ec5f099
--- /dev/null
+++ b/res/values-zh-rCN/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"照片编辑器"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"无法加载该图片!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"正在设置壁纸"</string>
+    <string name="original" msgid="3524493791230430897">"原图"</string>
+    <string name="borders" msgid="2067345080568684614">"边框"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"撤消"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"重做"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"显示历史记录"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"隐藏历史记录"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"显示图片状态"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"隐藏图片状态"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"设置"</string>
+    <string name="unsaved" msgid="8704442449002374375">"此图片的更改尚未保存。"</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"要在退出之前保存更改吗?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"保存并退出"</string>
+    <string name="exit" msgid="242642957038770113">"退出"</string>
+    <string name="history" msgid="455767361472692409">"历史记录"</string>
+    <string name="reset" msgid="9013181350779592937">"重置"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"运用的效果"</string>
+    <string name="compare_original" msgid="8140838959007796977">"比较"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"应用"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"重置"</string>
+    <string name="aspect" msgid="4025244950820813059">"宽高比"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"无"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"固定"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"小星球"</string>
+    <string name="exposure" msgid="6526397045949374905">"曝光"</string>
+    <string name="sharpness" msgid="6463103068318055412">"锐度"</string>
+    <string name="contrast" msgid="2310908487756769019">"对比度"</string>
+    <string name="vibrance" msgid="3326744578577835915">"自然饱和度"</string>
+    <string name="saturation" msgid="7026791551032438585">"饱和度"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"黑白过滤器"</string>
+    <string name="wbalance" msgid="6346581563387083613">"自动调整色彩"</string>
+    <string name="hue" msgid="6231252147971086030">"色调"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"阴影"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"强光"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"曲线"</string>
+    <string name="vignette" msgid="934721068851885390">"晕影"</string>
+    <string name="redeye" msgid="4508883127049472069">"红眼"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"绘图"</string>
+    <string name="straighten" msgid="26025591664983528">"拉直"</string>
+    <string name="crop" msgid="5781263790107850771">"裁剪"</string>
+    <string name="rotate" msgid="2796802553793795371">"旋转"</string>
+    <string name="mirror" msgid="5482518108154883096">"镜像"</string>
+    <string name="negative" msgid="6998313764388022201">"负片"</string>
+    <string name="none" msgid="6633966646410296520">"无"</string>
+    <string name="edge" msgid="7036064886242147551">"边缘亮化"</string>
+    <string name="kmeans" msgid="1630263230946107457">"波普效果"</string>
+    <string name="downsample" msgid="3552938534146980104">"下采样"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"红色"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"绿色"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"蓝色"</string>
+    <string name="draw_style" msgid="2036125061987325389">"样式"</string>
+    <string name="draw_size" msgid="4360005386104151209">"大小"</string>
+    <string name="draw_color" msgid="2119030386987211193">"颜色"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"线条"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"记号笔"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"喷笔"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"清除"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"选择自定义颜色"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"选择颜色"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"选择大小"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"确定"</string>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..cb89a4d
--- /dev/null
+++ b/res/values-zh-rCN/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"图库"</string>
+    <string name="gadget_title" msgid="259405922673466798">"相框"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"视频播放器"</string>
+    <string name="loading_video" msgid="4013492720121891585">"正在加载视频..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"正在加载图片..."</string>
+    <string name="loading_account" msgid="928195413034552034">"正在加载帐户…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"继续播放视频"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"从 %s 开始继续播放?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"继续播放"</string>
+    <string name="loading" msgid="7038208555304563571">"正在加载..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"无法加载"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"无法加载此图片"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"无缩略图"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"重新开始"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"确定"</string>
+    <string name="ok" msgid="5296833083983263293">"确定"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"触摸一张脸开始裁剪。"</string>
+    <string name="saving_image" msgid="7270334453636349407">"正在保存照片..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"正在将照片保存到“<xliff:g id="ALBUM_NAME">%1$s</xliff:g>”…"</string>
+    <string name="save_error" msgid="6857408774183654970">"无法保存经过裁剪的图片。"</string>
+    <string name="crop_label" msgid="521114301871349328">"修剪照片"</string>
+    <string name="trim_label" msgid="274203231381209979">"剪辑视频"</string>
+    <string name="select_image" msgid="7841406150484742140">"选择照片"</string>
+    <string name="select_video" msgid="4859510992798615076">"选择视频"</string>
+    <string name="select_item" msgid="2816923896202086390">"选择条目"</string>
+    <string name="select_album" msgid="1557063764849434077">"选择相册"</string>
+    <string name="select_group" msgid="6744208543323307114">"选择群组"</string>
+    <string name="set_image" msgid="2331476809308010401">"将照片设置为"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"设置壁纸"</string>
+    <string name="wallpaper" msgid="140165383777262070">"正在设置壁纸..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁纸"</string>
+    <string name="delete" msgid="2839695998251824487">"删除"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"要删除所选内容吗?"</item>
+    <item quantity="other" msgid="5874316486520635333">"要删除所选内容吗?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"确认"</string>
+    <string name="cancel" msgid="3637516880917356226">"取消"</string>
+    <string name="share" msgid="3619042788254195341">"分享"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"分享全景图"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"以照片形式分享"</string>
+    <string name="deleted" msgid="6795433049119073871">"已删除"</string>
+    <string name="undo" msgid="2930873956446586313">"撤消"</string>
+    <string name="select_all" msgid="3403283025220282175">"全选"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"取消全选"</string>
+    <string name="slideshow" msgid="4355906903247112975">"播放幻灯片"</string>
+    <string name="details" msgid="8415120088556445230">"详细信息"</string>
+    <string name="details_title" msgid="2611396603977441273">"第 %1$d 项(共 %2$d 项):"</string>
+    <string name="close" msgid="5585646033158453043">"关闭"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"切换到相机"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"选中了 %1$d 项"</item>
+    <item quantity="one" msgid="2478365152745637768">"选中了 %1$d 项"</item>
+    <item quantity="other" msgid="754722656147810487">"选中了 %1$d 项"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"选中了 %1$d 本相册"</item>
+    <item quantity="one" msgid="6184377003099987825">"选中了 %1$d 本相册"</item>
+    <item quantity="other" msgid="53105607141906130">"选中了 %1$d 本相册"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"选中了 %1$d 组"</item>
+    <item quantity="one" msgid="5030162638216034260">"选中了 %1$d 组"</item>
+    <item quantity="other" msgid="3512041363942842738">"选中了 %1$d 组"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"显示在地图上"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"向左旋转"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"向右旋转"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"找不到指定的项。"</string>
+    <string name="edit" msgid="1502273844748580847">"编辑"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"正在处理缓存请求"</string>
+    <string name="caching_label" msgid="4521059045896269095">"正在缓存..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"修剪"</string>
+    <string name="trim_action" msgid="703098114452883524">"修剪"</string>
+    <string name="mute_action" msgid="5296241754753306251">"静音"</string>
+    <string name="set_as" msgid="3636764710790507868">"设置为"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"无法将视频静音。"</string>
+    <string name="video_err" msgid="7003051631792271009">"无法播放视频。"</string>
+    <string name="group_by_location" msgid="316641628989023253">"按位置分组"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"按时间分组"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"按标签分组"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"按人物"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"按相册分组"</string>
+    <string name="group_by_size" msgid="153766174950394155">"按大小分组"</string>
+    <string name="untagged" msgid="7281481064509590402">"未加标签"</string>
+    <string name="no_location" msgid="4043624857489331676">"无位置信息"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"出现网络问题,系统无法识别某些位置。"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"无法下载此相册中的照片,请稍后重试。"</string>
+    <string name="show_images_only" msgid="7263218480867672653">"仅限图片"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"仅限视频"</string>
+    <string name="show_all" msgid="6963292714584735149">"图片和视频"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"照片"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"没有照片。"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"经过裁剪的图片已保存至“<xliff:g id="FOLDER_NAME">%s</xliff:g>”。"</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"没有可用的相册。"</string>
+    <string name="empty_album" msgid="4542880442593595494">"没有图片/视频。"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"帖子"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"允许离线查看"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"刷新"</string>
+    <string name="done" msgid="217672440064436595">"完成"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"第 %1$d 个项(共 %2$d 个):"</string>
+    <string name="title" msgid="7622928349908052569">"标题"</string>
+    <string name="description" msgid="3016729318096557520">"描述"</string>
+    <string name="time" msgid="1367953006052876956">"时间"</string>
+    <string name="location" msgid="3432705876921618314">"地点"</string>
+    <string name="path" msgid="4725740395885105824">"路径"</string>
+    <string name="width" msgid="9215847239714321097">"宽度"</string>
+    <string name="height" msgid="3648885449443787772">"高度"</string>
+    <string name="orientation" msgid="4958327983165245513">"浏览模式"</string>
+    <string name="duration" msgid="8160058911218541616">"时长"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME 类型"</string>
+    <string name="file_size" msgid="8486169301588318915">"文件大小"</string>
+    <string name="maker" msgid="7921835498034236197">"制造商"</string>
+    <string name="model" msgid="8240207064064337366">"模型"</string>
+    <string name="flash" msgid="2816779031261147723">"闪光灯"</string>
+    <string name="aperture" msgid="5920657630303915195">"光圈"</string>
+    <string name="focal_length" msgid="1291383769749877010">"焦距"</string>
+    <string name="white_balance" msgid="1582509289994216078">"白平衡"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"曝光时间"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"手动"</string>
+    <string name="auto" msgid="4296941368722892821">"自动"</string>
+    <string name="flash_on" msgid="7891556231891837284">"使用了闪光灯"</string>
+    <string name="flash_off" msgid="1445443413822680010">"未使用闪光灯"</string>
+    <string name="unknown" msgid="3506693015896912952">"未知"</string>
+    <string name="ffx_original" msgid="372686331501281474">"原照片"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"复古"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"即时出相"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"漂除银影"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"蓝色"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"黑白"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"冲压"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"负冲"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"拿铁咖啡"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"版印"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"允许离线查看相册。"</item>
+    <item quantity="other" msgid="4948604338155959389">"允许离线查看相册。"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"该项是存储在本地的,可在离线状态下使用。"</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"所有相册"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"本地相册"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP 设备"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa 相册"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"可用空间:<xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> 或更小"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> 或更大"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> 到 <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"导入"</string>
+    <string name="import_complete" msgid="3875040287486199999">"导入已完成"</string>
+    <string name="import_fail" msgid="8497942380703298808">"导入失败"</string>
+    <string name="camera_connected" msgid="916021826223448591">"相机已连接。"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"相机已断开连接。"</string>
+    <string name="click_import" msgid="6407959065464291972">"触摸此处可导入"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"选择相册"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"随机显示所有图片"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"选择图片"</string>
+    <string name="widget_type" msgid="1364653978966343448">"选择图片"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"幻灯片"</string>
+    <string name="albums" msgid="7320787705180057947">"相册"</string>
+    <string name="times" msgid="2023033894889499219">"时间"</string>
+    <string name="locations" msgid="6649297994083130305">"地点"</string>
+    <string name="people" msgid="4114003823747292747">"人物"</string>
+    <string name="tags" msgid="5539648765482935955">"标签"</string>
+    <string name="group_by" msgid="4308299657902209357">"分组依据"</string>
+    <string name="settings" msgid="1534847740615665736">"设置"</string>
+    <string name="add_account" msgid="4271217504968243974">"添加帐户"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"相机"</string>
+    <string name="folder_download" msgid="7186215137642323932">"下载"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"编辑过的在线照片"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"已导入"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"屏幕截图"</string>
+    <string name="help" msgid="7368960711153618354">"帮助"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"没有存储设备"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"没有可用的外部存储设备"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"幻灯片视图"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"网格视图"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"全屏视图"</string>
+    <string name="trimming" msgid="9122385768369143997">"正在剪辑"</string>
+    <string name="muting" msgid="5094925919589915324">"正在静音"</string>
+    <string name="please_wait" msgid="7296066089146487366">"请稍候"</string>
+    <string name="save_into" msgid="9155488424829609229">"正在将视频保存到“<xliff:g id="ALBUM_NAME">%1$s</xliff:g>”…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"无法剪辑:目标视频太短"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"正在渲染全景图"</string>
+    <string name="save" msgid="613976532235060516">"保存"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"正在扫描内容..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"已扫描%1$d项"</item>
+    <item quantity="one" msgid="4340019444460561648">"已扫描%1$d项"</item>
+    <item quantity="other" msgid="3138021473860555499">"已扫描%1$d项"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"正在排序..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"扫描已完成"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"正在导入..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"这台设备上没有可导入的内容。"</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"未连接 MTP 设备"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"相机故障"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"无法连接到相机。"</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"由于安全政策的限制,相机已被停用。"</string>
+    <string name="camera_label" msgid="6346560772074764302">"相机"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"摄像机"</string>
+    <string name="wait" msgid="8600187532323801552">"请稍候..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"使用相机前请先装载 USB 存储设备。"</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"使用相机前请先插入 SD 卡。"</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"正在准备 USB 存储设备..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"正在准备 SD 卡..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"无法访问 USB 存储设备。"</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"无法访问 SD 卡。"</string>
+    <string name="review_cancel" msgid="8188009385853399254">"取消"</string>
+    <string name="review_ok" msgid="1156261588693116433">"完成"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"延时录制"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"选择摄像头"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"背面相机"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"正面相机"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"保存所在位置"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"倒计时器"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1秒"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d秒"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"倒计时过程中发出提示音"</string>
+    <string name="setting_off" msgid="4480039384202951946">"关闭"</string>
+    <string name="setting_on" msgid="8602246224465348901">"打开"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"视频画质"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"高画质"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"低画质"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"延时"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"相机设置"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"摄像机设置"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"照片大小"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"800 万像素"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"500 万像素"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"300 万像素"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"200 万像素"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"130 万像素"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"100 万像素"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"对焦方式"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"自动"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"无限远"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"微距"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"闪光模式"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"自动"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"开"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"关"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"白平衡"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"自动"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"白炽光"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"日光"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"荧光"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"阴天"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"取景模式"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"自动"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"运动"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"夜景"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"日落"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"派对"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"无法在取景模式下选择。"</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"曝光"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"确定"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"USB 存储设备空间不足,请更改照片品质设置,或删除某些照片或者其他文件。"</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"SD 卡空间不足,请更改照片品质设置,或删除某些照片或者其他文件。"</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"已达到大小上限。"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"过快"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"正在生成全景图"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"无法保存全景照片。"</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"全景图"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"正在拍摄全景照片"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"正在等待上一幅全景照片处理完毕"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"正在保存..."</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"正在渲染全景图"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"触摸对焦。"</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"效果"</string>
+    <string name="effect_none" msgid="3601545724573307541">"无"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"面部哈哈镜"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"大眼睛"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"大嘴巴"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"小嘴巴"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"大鼻子"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"小眼睛"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"太空背景"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"日落"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"您的视频"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"把设备放好。"\n"离开镜头前片刻。"</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"在录制视频过程中,轻触一下即可拍一张照片。"</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"已开始录制视频。"</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"已停止录制视频。"</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"启用特殊效果时停用视频快照。"</string>
+    <string name="clear_effects" msgid="5485339175014139481">"清除效果"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"趣味表情"</string>
+    <string name="effect_background" msgid="6579360207378171022">"背景"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"“快门”按钮"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"菜单按钮"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"最新照片"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"前视和后视相机开关"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"相机、视频或全景模式选择器"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"更多设置控件"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"关闭设置控件"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"缩放控件"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"减少%1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"增加%1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"“%1$s”复选框"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"切换到拍照模式"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"切换到视频模式"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"切换到全景模式"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"切换到新的全景模式"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"取消"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"完成"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"重拍"</string>
+    <string name="capital_on" msgid="5491353494964003567">"打开"</string>
+    <string name="capital_off" msgid="7231052688467970897">"关闭"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"关闭"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 分钟"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 小时"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 小时"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"秒"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"分钟"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"小时"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"完成"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"设置时间间隔"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"延时拍摄功能已关闭,要设置时间间隔,请先开启该功能。"</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"倒计时器功能目前已关闭。要在拍照前倒计时,请先开启该功能。"</string>
+    <string name="set_duration" msgid="5578035312407161304">"设置倒数时间(秒)"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"拍照倒计时中"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"是否记住照片拍摄地点?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"为您的照片和视频标明拍摄地点。"\n\n"其他应用在查看您保存的图片时将可以访问这些信息。"</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"不用了"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"是"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"相机"</string>
+    <string name="menu_search" msgid="7580008232297437190">"搜索"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"照片"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"相册"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d张照片"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d张照片"</item>
+  </plurals>
+</resources>
diff --git a/res/values-zh-rTW/filtershow_strings.xml b/res/values-zh-rTW/filtershow_strings.xml
new file mode 100644
index 0000000..7bb6fb0
--- /dev/null
+++ b/res/values-zh-rTW/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"相片編輯器"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"無法載入圖片!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"正在設定桌布"</string>
+    <string name="original" msgid="3524493791230430897">"原始"</string>
+    <string name="borders" msgid="2067345080568684614">"邊框"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"復原"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"重做"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"顯示紀錄"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"隱藏紀錄"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"顯示圖片狀態"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"隱藏圖片狀態"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"設定"</string>
+    <string name="unsaved" msgid="8704442449002374375">"這張圖片的變更尚未儲存。"</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"您要在結束前儲存變更嗎?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"儲存並結束"</string>
+    <string name="exit" msgid="242642957038770113">"結束"</string>
+    <string name="history" msgid="455767361472692409">"紀錄"</string>
+    <string name="reset" msgid="9013181350779592937">"重設"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"套用的效果"</string>
+    <string name="compare_original" msgid="8140838959007796977">"比較"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"套用"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"重設"</string>
+    <string name="aspect" msgid="4025244950820813059">"長寬比"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"無"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"固定"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"小星球"</string>
+    <string name="exposure" msgid="6526397045949374905">"曝光"</string>
+    <string name="sharpness" msgid="6463103068318055412">"銳利度"</string>
+    <string name="contrast" msgid="2310908487756769019">"對比"</string>
+    <string name="vibrance" msgid="3326744578577835915">"鮮明化"</string>
+    <string name="saturation" msgid="7026791551032438585">"飽和度"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"黑白濾鏡"</string>
+    <string name="wbalance" msgid="6346581563387083613">"自動色彩校正"</string>
+    <string name="hue" msgid="6231252147971086030">"色調"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"陰影"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"強光"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"曲線"</string>
+    <string name="vignette" msgid="934721068851885390">"暈影"</string>
+    <string name="redeye" msgid="4508883127049472069">"紅眼"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"繪圖"</string>
+    <string name="straighten" msgid="26025591664983528">"拉正"</string>
+    <string name="crop" msgid="5781263790107850771">"裁剪"</string>
+    <string name="rotate" msgid="2796802553793795371">"旋轉"</string>
+    <string name="mirror" msgid="5482518108154883096">"鏡像"</string>
+    <string name="negative" msgid="6998313764388022201">"負片效果"</string>
+    <string name="none" msgid="6633966646410296520">"無"</string>
+    <string name="edge" msgid="7036064886242147551">"邊緣特效"</string>
+    <string name="kmeans" msgid="1630263230946107457">"安迪沃荷"</string>
+    <string name="downsample" msgid="3552938534146980104">"縮小取樣"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"紅色"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"綠色"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"藍色"</string>
+    <string name="draw_style" msgid="2036125061987325389">"樣式"</string>
+    <string name="draw_size" msgid="4360005386104151209">"尺寸"</string>
+    <string name="draw_color" msgid="2119030386987211193">"顏色"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"線條"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"彩色筆"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"潑灑"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"清除"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"選擇自訂顏色"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"選擇顏色"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"選擇尺寸"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"確定"</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..079ea5d
--- /dev/null
+++ b/res/values-zh-rTW/strings.xml
@@ -0,0 +1,400 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"圖片庫"</string>
+    <string name="gadget_title" msgid="259405922673466798">"相框"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"影片播放器"</string>
+    <string name="loading_video" msgid="4013492720121891585">"正在載入影片…"</string>
+    <string name="loading_image" msgid="1200894415793838191">"正在載入圖片…"</string>
+    <string name="loading_account" msgid="928195413034552034">"正在載入帳戶…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"繼續播放影片"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"要從 %s 繼續播放嗎?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"繼續播放"</string>
+    <string name="loading" msgid="7038208555304563571">"載入中…"</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"無法載入"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"無法載入圖片"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"無縮圖"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"重新開始"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"確定"</string>
+    <string name="ok" msgid="5296833083983263293">"確定"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"輕觸臉孔即可開始。"</string>
+    <string name="saving_image" msgid="7270334453636349407">"正在儲存相片…"</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"正在將圖片儲存至「<xliff:g id="ALBUM_NAME">%1$s</xliff:g>」…"</string>
+    <string name="save_error" msgid="6857408774183654970">"無法儲存裁剪的圖片。"</string>
+    <string name="crop_label" msgid="521114301871349328">"裁剪相片"</string>
+    <string name="trim_label" msgid="274203231381209979">"修剪影片"</string>
+    <string name="select_image" msgid="7841406150484742140">"選取相片"</string>
+    <string name="select_video" msgid="4859510992798615076">"選取影片"</string>
+    <string name="select_item" msgid="2816923896202086390">"選取項目"</string>
+    <string name="select_album" msgid="1557063764849434077">"選取相簿"</string>
+    <string name="select_group" msgid="6744208543323307114">"選取群組"</string>
+    <string name="set_image" msgid="2331476809308010401">"將相片設為"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"設定桌布"</string>
+    <string name="wallpaper" msgid="140165383777262070">"正在設定桌布…"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"桌布"</string>
+    <string name="delete" msgid="2839695998251824487">"刪除"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"刪除選取的項目?"</item>
+    <item quantity="other" msgid="5874316486520635333">"刪除選取的項目?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"確認"</string>
+    <string name="cancel" msgid="3637516880917356226">"取消"</string>
+    <string name="share" msgid="3619042788254195341">"分享"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"分享全景"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"以相片形式分享"</string>
+    <string name="deleted" msgid="6795433049119073871">"已刪除"</string>
+    <string name="undo" msgid="2930873956446586313">"復原"</string>
+    <string name="select_all" msgid="3403283025220282175">"全選"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"取消全選"</string>
+    <string name="slideshow" msgid="4355906903247112975">"投影播放"</string>
+    <string name="details" msgid="8415120088556445230">"詳細資料"</string>
+    <string name="details_title" msgid="2611396603977441273">"第 %1$d 個項目 (共 %2$d 個項目):"</string>
+    <string name="close" msgid="5585646033158453043">"關閉"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"切換至相機"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"已選取 %1$d 個"</item>
+    <item quantity="one" msgid="2478365152745637768">"已選取 %1$d 個"</item>
+    <item quantity="other" msgid="754722656147810487">"已選取 %1$d 個"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"已選取 %1$d 本"</item>
+    <item quantity="one" msgid="6184377003099987825">"已選取 %1$d 本"</item>
+    <item quantity="other" msgid="53105607141906130">"已選取 %1$d 本"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"已選取 %1$d 個"</item>
+    <item quantity="one" msgid="5030162638216034260">"已選取 %1$d 個"</item>
+    <item quantity="other" msgid="3512041363942842738">"已選取 %1$d 個"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"在地圖上顯示"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"向左旋轉"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"向右旋轉"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"找不到項目。"</string>
+    <string name="edit" msgid="1502273844748580847">"編輯"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"正在處理快取要求"</string>
+    <string name="caching_label" msgid="4521059045896269095">"快取中…"</string>
+    <string name="crop_action" msgid="3427470284074377001">"裁剪"</string>
+    <string name="trim_action" msgid="703098114452883524">"修剪"</string>
+    <string name="mute_action" msgid="5296241754753306251">"靜音"</string>
+    <string name="set_as" msgid="3636764710790507868">"設為"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"無法將影片設為靜音。"</string>
+    <string name="video_err" msgid="7003051631792271009">"無法播放影片。"</string>
+    <string name="group_by_location" msgid="316641628989023253">"依位置"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"依時間"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"依標記"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"依人物分組"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"依專輯"</string>
+    <string name="group_by_size" msgid="153766174950394155">"依大小分類"</string>
+    <string name="untagged" msgid="7281481064509590402">"無標記"</string>
+    <string name="no_location" msgid="4043624857489331676">"無位置資訊"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"網路發生問題,因此無法辨識部分位置。"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"無法下載這個相簿中的相片,請稍後再試。"</string>
+    <string name="show_images_only" msgid="7263218480867672653">"僅顯示圖片"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"僅顯示影片"</string>
+    <string name="show_all" msgid="6963292714584735149">"圖片和影片"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"相片庫"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"沒有任何相片。"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"裁剪的圖片已儲存至「<xliff:g id="FOLDER_NAME">%s</xliff:g>」。"</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"沒有相簿。"</string>
+    <string name="empty_album" msgid="4542880442593595494">"O 個可用的圖片/影片。"</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"訊息中的相片"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"可在離線時使用"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"重新整理"</string>
+    <string name="done" msgid="217672440064436595">"完成"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"第 %1$d 個項目,共 %2$d 個項目:"</string>
+    <string name="title" msgid="7622928349908052569">"標題"</string>
+    <string name="description" msgid="3016729318096557520">"說明"</string>
+    <string name="time" msgid="1367953006052876956">"時間"</string>
+    <string name="location" msgid="3432705876921618314">"地點"</string>
+    <string name="path" msgid="4725740395885105824">"路徑"</string>
+    <string name="width" msgid="9215847239714321097">"寬度"</string>
+    <string name="height" msgid="3648885449443787772">"高度"</string>
+    <string name="orientation" msgid="4958327983165245513">"瀏覽模式"</string>
+    <string name="duration" msgid="8160058911218541616">"影片長度"</string>
+    <string name="mimetype" msgid="8024168704337990470">"MIME 類型"</string>
+    <string name="file_size" msgid="8486169301588318915">"檔案大小"</string>
+    <string name="maker" msgid="7921835498034236197">"製造商"</string>
+    <string name="model" msgid="8240207064064337366">"型號"</string>
+    <string name="flash" msgid="2816779031261147723">"閃光燈"</string>
+    <string name="aperture" msgid="5920657630303915195">"光圈"</string>
+    <string name="focal_length" msgid="1291383769749877010">"焦距"</string>
+    <string name="white_balance" msgid="1582509289994216078">"白平衡"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"曝光時間"</string>
+    <string name="iso" msgid="5028296664327335940">"ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"釐米"</string>
+    <string name="manual" msgid="6608905477477607865">"手動"</string>
+    <string name="auto" msgid="4296941368722892821">"自動"</string>
+    <string name="flash_on" msgid="7891556231891837284">"使用閃光燈"</string>
+    <string name="flash_off" msgid="1445443413822680010">"未使用閃光燈"</string>
+    <string name="unknown" msgid="3506693015896912952">"不明"</string>
+    <string name="ffx_original" msgid="372686331501281474">"原始"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"復古"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"拍立得"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"漂白"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"藍色"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"黑白"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"凹魚眼"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"X 光處理"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"褐色"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"石版"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"正在將相簿設為可離線瀏覽。"</item>
+    <item quantity="other" msgid="4948604338155959389">"正在將相簿設為可離線瀏覽。"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"這個項目已儲存在本機上,並且可供離線使用。"</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"所有相簿"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"本機相簿"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"MTP 裝置"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Picasa 相簿"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"可用空間:<xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> 以下"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> 以上"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> 至 <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"匯入"</string>
+    <string name="import_complete" msgid="3875040287486199999">"匯入完成"</string>
+    <string name="import_fail" msgid="8497942380703298808">"匯入失敗"</string>
+    <string name="camera_connected" msgid="916021826223448591">"相機已連線。"</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"相機已中斷連線。"</string>
+    <string name="click_import" msgid="6407959065464291972">"輕觸這裡即可匯入相簿"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"選擇相簿"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"隨機播放所有圖片"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"選擇圖片"</string>
+    <string name="widget_type" msgid="1364653978966343448">"選擇圖片"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"投影播放"</string>
+    <string name="albums" msgid="7320787705180057947">"相簿"</string>
+    <string name="times" msgid="2023033894889499219">"時間"</string>
+    <string name="locations" msgid="6649297994083130305">"位置"</string>
+    <string name="people" msgid="4114003823747292747">"人物"</string>
+    <string name="tags" msgid="5539648765482935955">"標記"</string>
+    <string name="group_by" msgid="4308299657902209357">"分組依據"</string>
+    <string name="settings" msgid="1534847740615665736">"設定"</string>
+    <string name="add_account" msgid="4271217504968243974">"新增帳戶"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"相機"</string>
+    <string name="folder_download" msgid="7186215137642323932">"下載"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"已編輯的線上相片"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"匯入"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"螢幕擷取畫面"</string>
+    <string name="help" msgid="7368960711153618354">"說明"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"沒有儲存裝置"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"沒有可用的外部儲存裝置"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"幻燈片檢視"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"格狀檢視"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"全螢幕檢視"</string>
+    <string name="trimming" msgid="9122385768369143997">"修剪中"</string>
+    <string name="muting" msgid="5094925919589915324">"正在設為靜音"</string>
+    <string name="please_wait" msgid="7296066089146487366">"請稍候"</string>
+    <string name="save_into" msgid="9155488424829609229">"正在將影片儲存至「<xliff:g id="ALBUM_NAME">%1$s</xliff:g>」…"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"無法修剪:目標影片長度過短"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"正在進行全景成像作業"</string>
+    <string name="save" msgid="613976532235060516">"儲存"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"正在掃描內容..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"已掃描 %1$d 個項目"</item>
+    <item quantity="one" msgid="4340019444460561648">"已掃描 %1$d 個項目"</item>
+    <item quantity="other" msgid="3138021473860555499">"已掃描 %1$d 個項目"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"排序中..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"掃描完成"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"匯入中..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"沒有任何內容可供這個裝置匯入。"</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"未連接任何 MTP 裝置"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"相機發生錯誤"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"無法連接相機。"</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"由於安全性政策規定,相機已遭停用。"</string>
+    <string name="camera_label" msgid="6346560772074764302">"相機"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"攝錄影機"</string>
+    <string name="wait" msgid="8600187532323801552">"請稍候…"</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"使用相機前,請先插入 USB 儲存裝置。"</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"使用相機前,請先插入 SD 卡。"</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"正在準備 USB 儲存裝置…"</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"正在準備 SD 卡…"</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"無法存取 USB 儲存裝置。"</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"無法存取 SD 卡。"</string>
+    <string name="review_cancel" msgid="8188009385853399254">"取消"</string>
+    <string name="review_ok" msgid="1156261588693116433">"完成"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"延時攝影錄製"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"選擇相機"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"後置鏡頭"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"前置鏡頭"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"儲存位置"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"倒數計時器"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 秒"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d 秒"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"倒數時發出提示音"</string>
+    <string name="setting_off" msgid="4480039384202951946">"關閉"</string>
+    <string name="setting_on" msgid="8602246224465348901">"開啟"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"影片品質"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"高畫質"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"低畫質"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"延時攝影"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"相機設定"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"攝錄影機設定"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"相片大小"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"800 萬像素"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"500 萬像素"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"300 萬像素"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"200 萬像素"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"130 萬像素"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"100 萬像素"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"對焦模式"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"自動"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"無限遠"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"微距"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"閃光模式"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"自動"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"開啟"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"關閉"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"白平衡"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"自動"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"鎢絲燈"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"日光"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"螢光燈"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"陰天"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"場景模式"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"自動"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"動態"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"夜景"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"黃昏"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"派對模式"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"在場景模式中無法選取。"</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"曝光"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"確定"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"您的 USB 儲存裝置的空間即將用盡,請變更圖片的品質設定,或是刪除部分圖片或其他檔案。"</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"您 SD 卡的空間即將不足,請變更圖片的品質設定,或是刪除部分圖片或其他檔案。"</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"已達大小上限。"</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"速度過快"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"正在準備全景預覽"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"無法儲存全景。"</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"全景"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"全景拍攝中"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"正在等待先前的全景處理完成"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"儲存中…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"正在進行全景成像作業"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"輕觸即可對焦。"</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"效果"</string>
+    <string name="effect_none" msgid="3601545724573307541">"無"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"擠眉弄眼"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"大眼睛"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"大嘴巴"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"小嘴巴"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"大鼻子"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"小眼睛"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"太空"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"黃昏"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"您的影片"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"放下您的裝置"\n"暫時離開畫面。"</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"輕觸即可在錄製期間拍照。"</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"錄影程序已啟動。"</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"錄影程序已暫停。"</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"特殊效果啟用時無法使用影片快照。"</string>
+    <string name="clear_effects" msgid="5485339175014139481">"清除效果"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"耍笨臉"</string>
+    <string name="effect_background" msgid="6579360207378171022">"背景"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"[快門] 按鈕"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"選單按鈕"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"最近的相片"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"前置和後置鏡頭開關"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"相機、影片或全景選取工具"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"更多設定控制"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"關閉設定控制"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"縮放控制"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"縮小 %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"放大 %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s 核取方塊"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"切換至相片模式"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"切換至影片模式"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"切換至全景模式"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"切換為新全景"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"檢閱取消"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"檢閱完成"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"檢查重拍"</string>
+    <string name="capital_on" msgid="5491353494964003567">"開啟"</string>
+    <string name="capital_off" msgid="7231052688467970897">"關閉"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"關閉"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 秒"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 分鐘"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 小時"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 小時"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"秒"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"分鐘"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"小時"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"完成"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"設定時間間隔"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"延時攝影功能已關閉,請開啟以設定時間間格。"</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"倒數計時器目前為關閉狀態,開啓即可在拍照前倒數計時。"</string>
+    <string name="set_duration" msgid="5578035312407161304">"設定倒數時間 (以秒為單位)"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"拍照倒數計時中"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"記錄拍攝地點?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"為您的相片和影片標記拍攝地點。"\n\n"其他應用程式可存取這項資訊及您所儲存的相片。"</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"不用了,謝謝"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"是"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"相機"</string>
+    <string name="menu_search" msgid="7580008232297437190">"搜尋"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"相片"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"相簿"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d 張相片"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d 張相片"</item>
+  </plurals>
+</resources>
diff --git a/res/values-zu/filtershow_strings.xml b/res/values-zu/filtershow_strings.xml
new file mode 100644
index 0000000..8bdcbf0
--- /dev/null
+++ b/res/values-zu/filtershow_strings.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2012 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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="title_activity_filter_show" msgid="2036539130684382763">"Isihleli sesithombe"</string>
+    <string name="cannot_load_image" msgid="5023634941212959976">"Ayikwazi ukulayisha isithombe!"</string>
+    <!-- no translation found for original_picture_text (3076213290079909698) -->
+    <skip />
+    <string name="setting_wallpaper" msgid="4679087092300036632">"Isetha isithombe sangemuva"</string>
+    <string name="original" msgid="3524493791230430897">"Oluqobo"</string>
+    <string name="borders" msgid="2067345080568684614">"Imingcele"</string>
+    <string name="filtershow_undo" msgid="6781743189243585101">"Hlehlisa"</string>
+    <string name="filtershow_redo" msgid="4219489910543059747">"Yenza kabusha"</string>
+    <string name="show_history_panel" msgid="7785810372502120090">"Bonisa umlando"</string>
+    <string name="hide_history_panel" msgid="2082672248771133871">"Fihla umlando"</string>
+    <string name="show_imagestate_panel" msgid="7132294085840948243">"Bonisa isimo sesithombe"</string>
+    <string name="hide_imagestate_panel" msgid="1135313661068111161">"Fihla isimo sesithombe"</string>
+    <string name="menu_settings" msgid="6428291655769260831">"Izilungiselelo"</string>
+    <string name="unsaved" msgid="8704442449002374375">"Kukhona ushintsho olungalondolozwanga kulesi sithombe."</string>
+    <string name="save_before_exit" msgid="2680660633675916712">"Ufuna ukulondoloza ngaphambi kokuphuma?"</string>
+    <string name="save_and_exit" msgid="3628425023766687419">"Londoloza uphinde uphume"</string>
+    <string name="exit" msgid="242642957038770113">"Phuma"</string>
+    <string name="history" msgid="455767361472692409">"Umlando"</string>
+    <string name="reset" msgid="9013181350779592937">"Setha kabusha"</string>
+    <!-- no translation found for history_original (150973253194312841) -->
+    <skip />
+    <string name="imageState" msgid="8632586742752891968">"Imiphumela esetshenzisiwe"</string>
+    <string name="compare_original" msgid="8140838959007796977">"Qhathanisa"</string>
+    <string name="apply_effect" msgid="1218288221200568947">"Sebenzisa"</string>
+    <string name="reset_effect" msgid="7712605581024929564">"Setha kabusha"</string>
+    <string name="aspect" msgid="4025244950820813059">"I-Aspect"</string>
+    <string name="aspect1to1_effect" msgid="1159104543795779123">"1:1"</string>
+    <string name="aspect4to3_effect" msgid="7968067847241223578">"4:3"</string>
+    <string name="aspect3to4_effect" msgid="7078163990979248864">"3:4"</string>
+    <string name="aspect4to6_effect" msgid="1410129351686165654">"4:6"</string>
+    <string name="aspect5to7_effect" msgid="5122395569059384741">"5:7"</string>
+    <string name="aspect7to5_effect" msgid="5780001758108328143">"7:5"</string>
+    <string name="aspect9to16_effect" msgid="7740468012919660728">"16:9"</string>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Akunalutho"</string>
+    <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
+    <skip />
+    <string name="Fixed" msgid="8017376448916924565">"Okungashintshi"</string>
+    <string name="tinyplanet" msgid="2783694326474415761">"Inkanyezi ezungeza ilanga encane"</string>
+    <string name="exposure" msgid="6526397045949374905">"Ukuboniswa"</string>
+    <string name="sharpness" msgid="6463103068318055412">"Ubukhali"</string>
+    <string name="contrast" msgid="2310908487756769019">"Ukugqama"</string>
+    <string name="vibrance" msgid="3326744578577835915">"Ukudlidliza"</string>
+    <string name="saturation" msgid="7026791551032438585">"Gcwalisa isikhala"</string>
+    <string name="bwfilter" msgid="8927492494576933793">"Isihlungi se-BW"</string>
+    <string name="wbalance" msgid="6346581563387083613">"I-Autocolor"</string>
+    <string name="hue" msgid="6231252147971086030">"I-Hue"</string>
+    <string name="shadow_recovery" msgid="3928572915300287152">"Izithunzi"</string>
+    <string name="highlight_recovery" msgid="8262208470735204243">"Okubekwe obala"</string>
+    <string name="curvesRGB" msgid="915010781090477550">"Ukugobeka"</string>
+    <string name="vignette" msgid="934721068851885390">"I-Vignette"</string>
+    <string name="redeye" msgid="4508883127049472069">"Iso elibomvu"</string>
+    <string name="imageDraw" msgid="6918552177844486656">"Dweba"</string>
+    <string name="straighten" msgid="26025591664983528">"Qondisa"</string>
+    <string name="crop" msgid="5781263790107850771">"Sika"</string>
+    <string name="rotate" msgid="2796802553793795371">"Phendula"</string>
+    <string name="mirror" msgid="5482518108154883096">"Isibuko"</string>
+    <string name="negative" msgid="6998313764388022201">"Inegethivu"</string>
+    <string name="none" msgid="6633966646410296520">"Akunalutho"</string>
+    <string name="edge" msgid="7036064886242147551">"Imiphetho"</string>
+    <string name="kmeans" msgid="1630263230946107457">"I-Warhol"</string>
+    <string name="downsample" msgid="3552938534146980104">"I-Downsample"</string>
+    <string name="curves_channel_rgb" msgid="7909209509638333690">"I-RGB"</string>
+    <string name="curves_channel_red" msgid="4199710104162111357">"Okubomvu"</string>
+    <string name="curves_channel_green" msgid="3733003466905031016">"Okuluhlaza"</string>
+    <string name="curves_channel_blue" msgid="9129211507395079371">"Okuluhlaza sasibhakabhaka"</string>
+    <string name="draw_style" msgid="2036125061987325389">"Isitayela"</string>
+    <string name="draw_size" msgid="4360005386104151209">"Usayizi"</string>
+    <string name="draw_color" msgid="2119030386987211193">"Umbala"</string>
+    <string name="draw_style_line" msgid="9216476853904429628">"Imigqa"</string>
+    <string name="draw_style_brush_spatter" msgid="7612691122932981554">"Isiphawuli"</string>
+    <string name="draw_style_brush_marker" msgid="8468302322165644292">"I-Spatter"</string>
+    <string name="draw_clear" msgid="6728155515454921052">"Sula"</string>
+    <string name="color_pick_select" msgid="734312818059057394">"Khetha umbala wangokwezifiso"</string>
+    <string name="color_pick_title" msgid="6195567431995308876">"Khetha umbala"</string>
+    <string name="draw_size_title" msgid="3121649039610273977">"Khetha usayizi"</string>
+    <string name="draw_size_accept" msgid="6781529716526190028">"KULUNGILE"</string>
+</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
new file mode 100644
index 0000000..7e88404
--- /dev/null
+++ b/res/values-zu/strings.xml
@@ -0,0 +1,402 @@
+<?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:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Igalari"</string>
+    <string name="gadget_title" msgid="259405922673466798">"Uhlaka lwesithombe"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="3526526872644898229">"Isidlali sevidiyo"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Ilayisha ividiyo..."</string>
+    <string name="loading_image" msgid="1200894415793838191">"Iyalayisha isithombe..."</string>
+    <string name="loading_account" msgid="928195413034552034">"Ilayisha i-akhawunti"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Qalisa ividiyo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Qalisa ukudlala kusuka %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Qalisa ukudlala"</string>
+    <string name="loading" msgid="7038208555304563571">"Iyalayisha..."</string>
+    <string name="fail_to_load" msgid="8394392853646664505">"Ayikwazanga ukulayisha."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Ayikwazanga ukulayisha umfanekiso"</string>
+    <string name="no_thumbnail" msgid="284723185546429750">"Asikho isithoombe esincane"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Qala phansi"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Kulungile"</string>
+    <string name="ok" msgid="5296833083983263293">"KULUNGILE"</string>
+    <string name="multiface_crop_help" msgid="2554690102655855657">"Cindezela ubuso ukuze uqale."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Ilondoloza isithombe..."</string>
+    <string name="filtershow_saving_image" msgid="6659463980581993016">"Kulondolozwa isithombe ku-<xliff:g id="ALBUM_NAME">%1$s</xliff:g> ..."</string>
+    <string name="save_error" msgid="6857408774183654970">"Yehlulekile ukulondoloza umfanekiso onqampuniwe."</string>
+    <string name="crop_label" msgid="521114301871349328">"Nqampuna isithombe"</string>
+    <string name="trim_label" msgid="274203231381209979">"Sika ividiyo"</string>
+    <string name="select_image" msgid="7841406150484742140">"Khetha isithombe"</string>
+    <string name="select_video" msgid="4859510992798615076">"Khetha ividiyo"</string>
+    <string name="select_item" msgid="2816923896202086390">"Khetha intwana"</string>
+    <string name="select_album" msgid="1557063764849434077">"Khetha i-albhamu"</string>
+    <string name="select_group" msgid="6744208543323307114">"Khetha iqembu"</string>
+    <string name="set_image" msgid="2331476809308010401">"Hlela isithombe njenge"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Setha isithombe sangemuva"</string>
+    <string name="wallpaper" msgid="140165383777262070">"Isetha iphephadonga..."</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Iphephadonga"</string>
+    <string name="delete" msgid="2839695998251824487">"Susa"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Sula into ekhethiwe?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Sula izinto ezikhethiwe?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Qinisekisa"</string>
+    <string name="cancel" msgid="3637516880917356226">"Khansela"</string>
+    <string name="share" msgid="3619042788254195341">"Yabelana"</string>
+    <string name="share_panorama" msgid="2569029972820978718">"Yaba i-panorama"</string>
+    <string name="share_as_photo" msgid="8959225188897026149">"Yaba njengesithombe"</string>
+    <!-- no translation found for deleted (6795433049119073871) -->
+    <skip />
+    <!-- no translation found for undo (2930873956446586313) -->
+    <skip />
+    <string name="select_all" msgid="3403283025220282175">"Khetha konke"</string>
+    <string name="deselect_all" msgid="5758897506061723684">"Ungakhethi konke"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Umbukiso weslaydi"</string>
+    <string name="details" msgid="8415120088556445230">"Imininingwane"</string>
+    <string name="details_title" msgid="2611396603977441273">"izintwana ezingu-%1$d kwezingu-%2$d:"</string>
+    <string name="close" msgid="5585646033158453043">"Vala"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Shintshela kwikhamera"</string>
+  <plurals name="number_of_items_selected">
+    <item quantity="zero" msgid="2142579311530586258">"%1$d khethiwe"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d khethiwe"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d khethiwe"</item>
+  </plurals>
+  <plurals name="number_of_albums_selected">
+    <item quantity="zero" msgid="749292746814788132">"%1$d khethiwe"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d khethiwe"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d khethiwe"</item>
+  </plurals>
+  <plurals name="number_of_groups_selected">
+    <item quantity="zero" msgid="3466388370310869238">"%1$d khethiwe"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d khethiwe"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d khethiwe"</item>
+  </plurals>
+    <string name="show_on_map" msgid="6157544221201750980">"Bonisa kwimephu"</string>
+    <string name="rotate_left" msgid="5888273317282539839">"Phendukisela kwesokunxele"</string>
+    <string name="rotate_right" msgid="6776325835923384839">"Phendukisela kwesokudla"</string>
+    <string name="no_such_item" msgid="5315144556325243400">"Yehlulekile ukuthola into yakho."</string>
+    <string name="edit" msgid="1502273844748580847">"Hlela"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Ilungisa izicelo zokufaka kwinqolobane"</string>
+    <string name="caching_label" msgid="4521059045896269095">"Ukulondoloza isikhashana..."</string>
+    <string name="crop_action" msgid="3427470284074377001">"Khropha"</string>
+    <string name="trim_action" msgid="703098114452883524">"Lungisa"</string>
+    <string name="mute_action" msgid="5296241754753306251">"Thulisa"</string>
+    <string name="set_as" msgid="3636764710790507868">"Hlela njenge"</string>
+    <string name="video_mute_err" msgid="6392457611270600908">"Ayikwazi ukuthulisa ividiyo."</string>
+    <string name="video_err" msgid="7003051631792271009">"Ayikwazi ukudlala ividiyo"</string>
+    <string name="group_by_location" msgid="316641628989023253">"Ngendawo"</string>
+    <string name="group_by_time" msgid="9046168567717963573">"Ngesikhathi"</string>
+    <string name="group_by_tags" msgid="3568731317210676160">"Ngamamaki"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Ngabantu"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Nge-albhamu"</string>
+    <string name="group_by_size" msgid="153766174950394155">"Ngosayizi"</string>
+    <string name="untagged" msgid="7281481064509590402">"Akunasilengiso"</string>
+    <string name="no_location" msgid="4043624857489331676">"Ayikho indawo"</string>
+    <string name="no_connectivity" msgid="7164037617297293668">"Ezinye izindawo azikwazanga ukubonakala ngenxa yezinkinga zokuxhumeka kuhleloxhumano"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Ayikwazanga ukulanda izithombe kule albhamu. Zama ngemva kwesikhashana."</string>
+    <string name="show_images_only" msgid="7263218480867672653">"Izithombe kuphela"</string>
+    <string name="show_videos_only" msgid="3850394623678871697">"Amavidiyo kuphela"</string>
+    <string name="show_all" msgid="6963292714584735149">"Izithombe namavidiyo"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"Igalari Yesithombe"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"Azikho izithombe"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Umfanekiso onqampuniwe ulondolozelwe e <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="no_albums_alert" msgid="4111744447491690896">"Awekho ama-albhamu atholalakalayo."</string>
+    <string name="empty_album" msgid="4542880442593595494">"O izithombe/amavidiyo okutholakalayo."</string>
+    <string name="picasa_posts" msgid="1497721615718760613">"Okuposiwe"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Yenza kutholakale ungaxhumekile kwi-inthanethi"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Vuselela"</string>
+    <string name="done" msgid="217672440064436595">"Kwenziwe"</string>
+    <string name="sequence_in_set" msgid="7235465319919457488">"izintwana ezingu-%1$d kwezingu-%2$d:"</string>
+    <string name="title" msgid="7622928349908052569">"Isihloko"</string>
+    <string name="description" msgid="3016729318096557520">"Incazelo"</string>
+    <string name="time" msgid="1367953006052876956">"Isikhathi"</string>
+    <string name="location" msgid="3432705876921618314">"Indawo"</string>
+    <string name="path" msgid="4725740395885105824">"Indlela"</string>
+    <string name="width" msgid="9215847239714321097">"Ububanzi"</string>
+    <string name="height" msgid="3648885449443787772">"Ubude"</string>
+    <string name="orientation" msgid="4958327983165245513">"Ukujikeleza"</string>
+    <string name="duration" msgid="8160058911218541616">"Ubude besikhathi"</string>
+    <string name="mimetype" msgid="8024168704337990470">"Uhlobo lwe-MIME"</string>
+    <string name="file_size" msgid="8486169301588318915">"Usayizi wefayela:"</string>
+    <string name="maker" msgid="7921835498034236197">"Umenzi"</string>
+    <string name="model" msgid="8240207064064337366">"Imodili"</string>
+    <string name="flash" msgid="2816779031261147723">"Ifuleshi"</string>
+    <string name="aperture" msgid="5920657630303915195">"Imbobo"</string>
+    <string name="focal_length" msgid="1291383769749877010">"Ubude Befokasi"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Ukulingana kokumhlophe"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Isikhathi esisobala"</string>
+    <string name="iso" msgid="5028296664327335940">"i-ISO"</string>
+    <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
+    <string name="manual" msgid="6608905477477607865">"Ngokulawulwa"</string>
+    <string name="auto" msgid="4296941368722892821">"Okuzenzakalelayo"</string>
+    <string name="flash_on" msgid="7891556231891837284">"Ifuleshi iqhafaziwe"</string>
+    <string name="flash_off" msgid="1445443413822680010">"Ayikho ifuleshi"</string>
+    <string name="unknown" msgid="3506693015896912952">"Akwaziwa"</string>
+    <string name="ffx_original" msgid="372686331501281474">"Oluqobo"</string>
+    <string name="ffx_vintage" msgid="8348759951363844780">"I-Vintage"</string>
+    <string name="ffx_instant" msgid="726968618715691987">"Ngokuzenzakalela"</string>
+    <string name="ffx_bleach" msgid="8946700451603478453">"Enza mhlophe"</string>
+    <string name="ffx_blue_crush" msgid="6034283412305561226">"Okuluhlaza"</string>
+    <string name="ffx_bw_contrast" msgid="517988490066217206">"B/W"</string>
+    <string name="ffx_punch" msgid="1343475517872562639">"I-Punch"</string>
+    <string name="ffx_x_process" msgid="4779398678661811765">"i_X Process"</string>
+    <string name="ffx_washout" msgid="4594160692176642735">"i-Latte"</string>
+    <string name="ffx_washout_color" msgid="8034075742195795219">"i-Litho"</string>
+  <plurals name="make_albums_available_offline">
+    <item quantity="one" msgid="2171596356101611086">"Yenza i-albhamu itholakale ungaxhumekile kwi-inthanethi"</item>
+    <item quantity="other" msgid="4948604338155959389">"Yenza ama-albhamu atholakale ungaxhumekile kwi-inthanethi"</item>
+  </plurals>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Le ntwana igcinwa endaweni bese itholakala ungaxhunyiwe kwi-intanethi."</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Wonke ama-albhamu"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Ama-albhamu asendaweni"</string>
+    <string name="set_label_mtp_devices" msgid="1283513183744896368">"Amadivayisi e-MTP"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Ama-albhamu e-Picasa"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> vulekile"</string>
+    <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> noma ngaphansi"</string>
+    <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> noma ngaphezulu"</string>
+    <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> kuya ku-<xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
+    <string name="Import" msgid="3985447518557474672">"Ngenisa"</string>
+    <string name="import_complete" msgid="3875040287486199999">"Ukungenisa kuqedile"</string>
+    <string name="import_fail" msgid="8497942380703298808">"Kwehlulekile ukulethwa kwento"</string>
+    <string name="camera_connected" msgid="916021826223448591">"Ikhamera ixhunyiwe."</string>
+    <string name="camera_disconnected" msgid="2100559901676329496">"Ikhamera ayixhunywanga"</string>
+    <string name="click_import" msgid="6407959065464291972">"Thinta lana ukuze uthumele"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Khetha i-albhamu"</string>
+    <string name="widget_type_shuffle" msgid="8594622705019763768">"Shova zonke izithombe"</string>
+    <string name="widget_type_photo" msgid="6267065337367795355">"Khetha isithombe"</string>
+    <string name="widget_type" msgid="1364653978966343448">"Khetha izithombe"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Umbukiso weslaydi"</string>
+    <string name="albums" msgid="7320787705180057947">"Ama-albhamu"</string>
+    <string name="times" msgid="2023033894889499219">"Izikhathi"</string>
+    <string name="locations" msgid="6649297994083130305">"Izindawo"</string>
+    <string name="people" msgid="4114003823747292747">"Abantu"</string>
+    <string name="tags" msgid="5539648765482935955">"Amamaki"</string>
+    <string name="group_by" msgid="4308299657902209357">"Qoqa nge-"</string>
+    <string name="settings" msgid="1534847740615665736">"Izilungiselelo"</string>
+    <string name="add_account" msgid="4271217504968243974">"Yengeza i-akhawunti"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Ikhamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Laysha"</string>
+    <string name="folder_edited_online_photos" msgid="6278215510236800181">"Izithombe ezihleliwe eziku-inthanethi"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Okulandiwe"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Isithombe-skrini"</string>
+    <string name="help" msgid="7368960711153618354">"Usizo"</string>
+    <string name="no_external_storage_title" msgid="2408933644249734569">"Asikho isilondolozi"</string>
+    <string name="no_external_storage" msgid="95726173164068417">"Asikho isilondolozi sangaphandle esikhona"</string>
+    <string name="switch_photo_filmstrip" msgid="8227883354281661548">"Ukubuka nge-Filmstrip"</string>
+    <string name="switch_photo_grid" msgid="3681299459107925725">"Ukubuka ngegridi"</string>
+    <string name="switch_photo_fullscreen" msgid="8360489096099127071">"Ukubukwa kwesikrini esigcwele"</string>
+    <string name="trimming" msgid="9122385768369143997">"Ukusika"</string>
+    <string name="muting" msgid="5094925919589915324">"Ukuthulisa"</string>
+    <string name="please_wait" msgid="7296066089146487366">"Sicela ulinde"</string>
+    <string name="save_into" msgid="9155488424829609229">"Kulondolozwa ividiyo ku-<xliff:g id="ALBUM_NAME">%1$s</xliff:g> …"</string>
+    <string name="trim_too_short" msgid="751593965620665326">"Awukwazi ukusika : ividiyo eqondisiwe yifushane kakhulu"</string>
+    <string name="pano_progress_text" msgid="1586851614586678464">"Ukufaka i-panorama"</string>
+    <string name="save" msgid="613976532235060516">"Londoloza"</string>
+    <string name="ingest_scanning" msgid="1062957108473988971">"Kuskenwa okuqukethwe..."</string>
+  <plurals name="ingest_number_of_items_scanned">
+    <item quantity="zero" msgid="2623289390474007396">"%1$d izinto eziskeniwe"</item>
+    <item quantity="one" msgid="4340019444460561648">"%1$d into eskeniwe"</item>
+    <item quantity="other" msgid="3138021473860555499">"%1$d izinto eziskeniwe"</item>
+  </plurals>
+    <string name="ingest_sorting" msgid="1028652103472581918">"Kuyahlungwa..."</string>
+    <string name="ingest_scanning_done" msgid="8911916277034483430">"Ukuskena kuqediwe"</string>
+    <string name="ingest_importing" msgid="7456633398378527611">"Iyangenisa..."</string>
+    <string name="ingest_empty_device" msgid="2010470482779872622">"Akukho okuqukethwe okutholakalela ukungeniswa kule divayisi."</string>
+    <string name="ingest_no_device" msgid="3054128223131382122">"Ayikho idivayisi ye-MTP exhunyiwe"</string>
+    <string name="camera_error_title" msgid="6484667504938477337">"Iphutha lekhamera"</string>
+    <string name="cannot_connect_camera" msgid="955440687597185163">"Ayikwazi ukuxhuma ekhamereni."</string>
+    <string name="camera_disabled" msgid="8923911090533439312">"Ikhamera inqunyiwe ngenxa yepolisi yezokuphepha."</string>
+    <string name="camera_label" msgid="6346560772074764302">"Ikhamera"</string>
+    <string name="video_camera_label" msgid="2899292505526427293">"Ikhamera enerekhoda"</string>
+    <string name="wait" msgid="8600187532323801552">"Sicela ulinde..."</string>
+    <string name="no_storage" product="nosdcard" msgid="7335975356349008814">"Sicela ukhweze isitoreji se-USB ngaphambi kokusebenzisa ikhamera."</string>
+    <string name="no_storage" product="default" msgid="5137703033746873624">"Sicela ufake ikhadi le-SD ngaphambi kokusebenzisa ikhamera."</string>
+    <string name="preparing_sd" product="nosdcard" msgid="6104019983528341353">"Ilungiselela isitoreji se-USB..."</string>
+    <string name="preparing_sd" product="default" msgid="2914969119574812666">"Ilungiselela ikhadi le-SD..."</string>
+    <string name="access_sd_fail" product="nosdcard" msgid="8147993984037859354">"Yehlulekile ukufinyelela kwindawo egcina i-USB."</string>
+    <string name="access_sd_fail" product="default" msgid="1584968646870054352">"Yehlukekile ukufinyelela kwikhadi le-SD."</string>
+    <string name="review_cancel" msgid="8188009385853399254">"KHANSELA"</string>
+    <string name="review_ok" msgid="1156261588693116433">"KUQEDIWE"</string>
+    <string name="time_lapse_title" msgid="4360632427760662691">"Iqopha ukuphela kwesikhathi"</string>
+    <string name="pref_camera_id_title" msgid="4040791582294635851">"Khetha ikhamera"</string>
+    <string name="pref_camera_id_entry_back" msgid="5142699735103692485">"Emuva"</string>
+    <string name="pref_camera_id_entry_front" msgid="5668958706828733669">"Phambili"</string>
+    <string name="pref_camera_recordlocation_title" msgid="371208839215448917">"Gcina indawo"</string>
+    <string name="pref_camera_timer_title" msgid="3105232208281893389">"Isikali sesikhathi esibala ngokwehla"</string>
+  <plurals name="pref_camera_timer_entry">
+    <item quantity="one" msgid="1654523400981245448">"1 isekhondi"</item>
+    <item quantity="other" msgid="6455381617076792481">"%d amasekhondi"</item>
+  </plurals>
+    <!-- no translation found for pref_camera_timer_sound_default (7066624532144402253) -->
+    <skip />
+    <string name="pref_camera_timer_sound_title" msgid="2469008631966169105">"Bhipha ngesikhathi sokubala ngokwehla"</string>
+    <string name="setting_off" msgid="4480039384202951946">"Kucishile"</string>
+    <string name="setting_on" msgid="8602246224465348901">"Kuyakhanya"</string>
+    <string name="pref_video_quality_title" msgid="8245379279801096922">"Ikhwalithi yevidiyo"</string>
+    <string name="pref_video_quality_entry_high" msgid="8664038216234805914">"Phezulu"</string>
+    <string name="pref_video_quality_entry_low" msgid="7258507152393173784">"Phansi"</string>
+    <string name="pref_video_time_lapse_frame_interval_title" msgid="6245716906744079302">"Ukudlula kwesikhathi"</string>
+    <string name="pref_camera_settings_category" msgid="2576236450859613120">"Izilungiselelo zekhamera"</string>
+    <string name="pref_camcorder_settings_category" msgid="460313486231965141">"Izilungiselelo zekhamera enerekhoda"</string>
+    <string name="pref_camera_picturesize_title" msgid="4333724936665883006">"Usayizi wesithombe"</string>
+    <string name="pref_camera_picturesize_entry_8mp" msgid="259953780932849079">"8M amaphikseli"</string>
+    <string name="pref_camera_picturesize_entry_5mp" msgid="2882928212030661159">"5M amaphikseli"</string>
+    <string name="pref_camera_picturesize_entry_3mp" msgid="741415860337400696">"3M amaphikseli"</string>
+    <string name="pref_camera_picturesize_entry_2mp" msgid="1753709802245460393">"2M amaphikseli"</string>
+    <string name="pref_camera_picturesize_entry_1_3mp" msgid="829109608140747258">"1.3M amaphikseli"</string>
+    <string name="pref_camera_picturesize_entry_1mp" msgid="1669725616780375066">"1M amaphikseli"</string>
+    <string name="pref_camera_picturesize_entry_vga" msgid="806934254162981919">"VGA"</string>
+    <string name="pref_camera_picturesize_entry_qvga" msgid="8576186463069770133">"QVGA"</string>
+    <string name="pref_camera_focusmode_title" msgid="2877248921829329127">"Imodi yefokhasi"</string>
+    <string name="pref_camera_focusmode_entry_auto" msgid="7374820710300362457">"Okuzenzakalelayo"</string>
+    <string name="pref_camera_focusmode_entry_infinity" msgid="3413922419264967552">"Kwaphakade"</string>
+    <string name="pref_camera_focusmode_entry_macro" msgid="4424489110551866161">"Imakhro"</string>
+    <string name="pref_camera_flashmode_title" msgid="2287362477238791017">"Imodi yokufulesh"</string>
+    <string name="pref_camera_flashmode_entry_auto" msgid="7288383434237457709">"Okuzenzakalelayo"</string>
+    <string name="pref_camera_flashmode_entry_on" msgid="5330043918845197616">"Vuliwe"</string>
+    <string name="pref_camera_flashmode_entry_off" msgid="867242186958805761">"Valiwe"</string>
+    <string name="pref_camera_whitebalance_title" msgid="677420930596673340">"Bhalansa ubumhlophe"</string>
+    <string name="pref_camera_whitebalance_entry_auto" msgid="6580665476983469293">"Okuzenzakalelayo"</string>
+    <string name="pref_camera_whitebalance_entry_incandescent" msgid="8856667786449549938">"Mhlophe komlilo"</string>
+    <string name="pref_camera_whitebalance_entry_daylight" msgid="2534757270149561027">"Emini"</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent" msgid="2435332872847454032">"Ukukhanya okumibalabala"</string>
+    <string name="pref_camera_whitebalance_entry_cloudy" msgid="3531996716997959326">"Kunamafu"</string>
+    <string name="pref_camera_scenemode_title" msgid="1420535844292504016">"Imodi yesigcawu"</string>
+    <string name="pref_camera_scenemode_entry_auto" msgid="7113995286836658648">"Okuzenzakalelayo"</string>
+    <string name="pref_camera_scenemode_entry_hdr" msgid="2923388802899511784">"i-HDR"</string>
+    <string name="pref_camera_scenemode_entry_action" msgid="616748587566110484">"Isenzo"</string>
+    <string name="pref_camera_scenemode_entry_night" msgid="7606898503102476329">"Ebusuku"</string>
+    <string name="pref_camera_scenemode_entry_sunset" msgid="181661154611507212">"Ukushona kwelanga"</string>
+    <string name="pref_camera_scenemode_entry_party" msgid="907053529286788253">"Phathi"</string>
+    <string name="not_selectable_in_scene_mode" msgid="2970291701448555126">"Akukhetheki esimweni sokubuka"</string>
+    <string name="pref_exposure_title" msgid="1229093066434614811">"Isibonelelo"</string>
+    <!-- no translation found for pref_camera_hdr_default (1336869406134365882) -->
+    <skip />
+    <string name="dialog_ok" msgid="6263301364153382152">"KULUNGILE"</string>
+    <string name="spaceIsLow_content" product="nosdcard" msgid="4401325203349203177">"Isitoreji se-USB yakho siphelelwa yisikhala. Shintsha ilungiselelo lekhwalithi noma susa ezinye izithombe noma amanye amafayela."</string>
+    <string name="spaceIsLow_content" product="default" msgid="1732882643101247179">"Ikhadi lakho le-SD liphelelwa isikhathi. Shintsha ilungiselelo lekhwalithi noma susa eminye imifanekiso noma amanye amafayela."</string>
+    <string name="video_reach_size_limit" msgid="6179877322015552390">"Umkhwawulo wosayizi ufinyelelwe."</string>
+    <string name="pano_too_fast_prompt" msgid="2823839093291374709">"Ishesha Kakhulu"</string>
+    <string name="pano_dialog_prepare_preview" msgid="4788441554128083543">"Ilungiselela i-panorama"</string>
+    <string name="pano_dialog_panorama_failed" msgid="2155692796549642116">"Yehlulekile ukulondoloza i-panaroma"</string>
+    <string name="pano_dialog_title" msgid="5755531234434437697">"I-Panorama"</string>
+    <string name="pano_capture_indication" msgid="8248825828264374507">"Ilondoloza i-Panorama"</string>
+    <string name="pano_dialog_waiting_previous" msgid="7800325815031423516">"Ilinde i-panaroma yangaphambilini"</string>
+    <string name="pano_review_saving_indication_str" msgid="2054886016665130188">"Iyalondoloza…"</string>
+    <string name="pano_review_rendering" msgid="2887552964129301902">"Kufakwa i-panorama"</string>
+    <string name="tap_to_focus" msgid="8863427645591903760">"Thinta ukuze kume kahle isithombe."</string>
+    <string name="pref_video_effect_title" msgid="8243182968457289488">"Imithelela"</string>
+    <string name="effect_none" msgid="3601545724573307541">"Lutho"</string>
+    <string name="effect_goofy_face_squeeze" msgid="1207235692524289171">"Shutheka"</string>
+    <string name="effect_goofy_face_big_eyes" msgid="3945182409691408412">"Amehlo amakhulu"</string>
+    <string name="effect_goofy_face_big_mouth" msgid="7528748779754643144">"Umlomo Omkhulu"</string>
+    <string name="effect_goofy_face_small_mouth" msgid="3848209817806932565">"Umlomo Omncane"</string>
+    <string name="effect_goofy_face_big_nose" msgid="5180533098740577137">"Ikhala Elikhulu"</string>
+    <string name="effect_goofy_face_small_eyes" msgid="1070355596290331271">"Amehlo Amancane"</string>
+    <string name="effect_backdropper_space" msgid="7935661090723068402">"Eskhaleni"</string>
+    <string name="effect_backdropper_sunset" msgid="45198943771777870">"Ukushona kwelanga"</string>
+    <string name="effect_backdropper_gallery" msgid="959158844620991906">"Ividiyo yakho"</string>
+    <string name="bg_replacement_message" msgid="9184270738916564608">"Setha idivayisi yakho phansi."\n"Isethe ingabonakali okwesikhashana."</string>
+    <string name="video_snapshot_hint" msgid="18833576851372483">"Thinta ukuze uthwebule isithombe ngenkathi uqopha."</string>
+    <string name="video_recording_started" msgid="4132915454417193503">"Ukuqoshwa kwevidiyo sekuqalile."</string>
+    <string name="video_recording_stopped" msgid="5086919511555808580">"Ukuqoshwa kwevidiyo sekumisiwe."</string>
+    <string name="disable_video_snapshot_hint" msgid="4957723267826476079">"Umfanekiso wevidyo awusebenzi uma izinandisi ezikhethekile zivuliwe."</string>
+    <string name="clear_effects" msgid="5485339175014139481">"Izinandisi ezicacile"</string>
+    <string name="effect_silly_faces" msgid="8107732405347155777">"UBUSO OBUNGASILE"</string>
+    <string name="effect_background" msgid="6579360207378171022">"INGEMUVA"</string>
+    <string name="accessibility_shutter_button" msgid="2664037763232556307">"Inkinobho Yokuvala"</string>
+    <string name="accessibility_menu_button" msgid="7140794046259897328">"Inkinobho yemenyu"</string>
+    <string name="accessibility_review_thumbnail" msgid="8961275263537513017">"Isithombe sakamuva"</string>
+    <string name="accessibility_camera_picker" msgid="8807945470215734566">"Iswishi yekhamera yangaphambili kanye nangemuva"</string>
+    <string name="accessibility_mode_picker" msgid="3278002189966833100">"Ikhamera, ividiyo noma ukhetho lwe-panorama"</string>
+    <string name="accessibility_second_level_indicators" msgid="3855951632917627620">"Izilawuli zezilungiselelo ezingaphezulu"</string>
+    <string name="accessibility_back_to_first_level" msgid="5234411571109877131">"Vala izilawulo zezilungiso"</string>
+    <string name="accessibility_zoom_control" msgid="1339909363226825709">"Ulawulo lokulwiza"</string>
+    <string name="accessibility_decrement" msgid="1411194318538035666">"Nciphisa %1$s"</string>
+    <string name="accessibility_increment" msgid="8447850530444401135">"Yandisa %1$s"</string>
+    <string name="accessibility_check_box" msgid="7317447218256584181">"%1$s ibhokisi lokuhlola"</string>
+    <string name="accessibility_switch_to_camera" msgid="5951340774212969461">"Shintshela esithombeni"</string>
+    <string name="accessibility_switch_to_video" msgid="4991396355234561505">"Shintshela kwividiyo"</string>
+    <string name="accessibility_switch_to_panorama" msgid="604756878371875836">"Shintshela kwi-panorama"</string>
+    <string name="accessibility_switch_to_new_panorama" msgid="8116783308051524188">"Shintshela ku-panorama entsha"</string>
+    <string name="accessibility_review_cancel" msgid="9070531914908644686">"Buyekeza ukukhansela"</string>
+    <string name="accessibility_review_ok" msgid="7793302834271343168">"Ukubuyekeza kuphelile"</string>
+    <string name="accessibility_review_retake" msgid="659300290054705484">"Ukuphindwa kuthathwe kwesibuyekezo"</string>
+    <string name="capital_on" msgid="5491353494964003567">"KUVULIWE"</string>
+    <string name="capital_off" msgid="7231052688467970897">"KUCINYIWE"</string>
+    <string name="pref_video_time_lapse_frame_interval_off" msgid="3490489191038309496">"Valiwe"</string>
+    <string name="pref_video_time_lapse_frame_interval_500" msgid="2949719376111679816">"0.5 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_1000" msgid="1672458758823855874">"1 isekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_1500" msgid="3415071702490624802">"1.5 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_2000" msgid="827813989647794389">"2 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_2500" msgid="5750464143606788153">"2.5 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_3000" msgid="2664846627499751396">"3 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_4000" msgid="7303255804306382651">"4 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_5000" msgid="6800566761690741841">"5 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_6000" msgid="8545447466540319539">"6 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_10000" msgid="3105568489694909852">"10 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_12000" msgid="6055574367392821047">"12 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_15000" msgid="2656164845371833761">"15 amsekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_24000" msgid="2192628967233421512">"24 amasekhondi"</string>
+    <string name="pref_video_time_lapse_frame_interval_30000" msgid="5923393773260634461">"0.5 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_60000" msgid="4678581247918524850">"1 iminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_90000" msgid="1187029705069674152">"1.5 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_120000" msgid="145301938098991278">"2 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_150000" msgid="793707078196731912">"2.5 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_180000" msgid="1785467676466542095">"3 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_240000" msgid="3734507766184666356">"4 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_300000" msgid="7442765761995328639">"5 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_360000" msgid="6724596937972563920">"6 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_600000" msgid="6563665954471001352">"10 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_720000" msgid="8969801372893266408">"12 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_900000" msgid="5803172407245902896">"15 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_1440000" msgid="6286246349698492186">"24 amaminithi"</string>
+    <string name="pref_video_time_lapse_frame_interval_1800000" msgid="5042628461448570758">"0.5 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_3600000" msgid="6366071632666482636">"1 ihora"</string>
+    <string name="pref_video_time_lapse_frame_interval_5400000" msgid="536117788694519019">"1.5 ihora"</string>
+    <string name="pref_video_time_lapse_frame_interval_7200000" msgid="6846617415182608533">"2 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_9000000" msgid="4242839574025261419">"2.5 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_10800000" msgid="2766886102170605302">"3 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_14400000" msgid="7497934659667867582">"4 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_18000000" msgid="8783643014853837140">"5 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_21600000" msgid="5005078879234015432">"6 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_36000000" msgid="69942198321578519">"10 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_43200000" msgid="285992046818504906">"12 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_54000000" msgid="5740227373848829515">"15 amahora"</string>
+    <string name="pref_video_time_lapse_frame_interval_86400000" msgid="9040201678470052298">"24 amahora"</string>
+    <string name="time_lapse_seconds" msgid="2105521458391118041">"amasekhondi"</string>
+    <string name="time_lapse_minutes" msgid="7738520349259013762">"amaminithi"</string>
+    <string name="time_lapse_hours" msgid="1776453661704997476">"amahora"</string>
+    <string name="time_lapse_interval_set" msgid="2486386210951700943">"Kwenziwe"</string>
+    <string name="set_time_interval" msgid="2970567717633813771">"Setha umkhawulo wesikhathi"</string>
+    <string name="set_time_interval_help" msgid="6665849510484821483">"Isici sesikhathi esidlulile sicimile. Sikhanyise ukuze usethe umkhawulo wesikhathi."</string>
+    <string name="set_timer_help" msgid="5007708849404589472">"Isikali sesikhathi esibala ngokwehla sivaliwe. Sivule ukuze ubale ngokwehla ngaphambi kokuthatha isithombe."</string>
+    <string name="set_duration" msgid="5578035312407161304">"Setha ubude besikhathi bube amasekhondi"</string>
+    <string name="count_down_title_text" msgid="4976386810910453266">"Kubalwa ngokwehla kuze kufike ekuthatheni isithombe"</string>
+    <string name="remember_location_title" msgid="9060472929006917810">"Khumbula izindawo zezithombe?"</string>
+    <string name="remember_location_prompt" msgid="724592331305808098">"Maka izithombe zakho namavidiyo ngezindawo lapha zithathwe khona."\n\n"Ezinye izinhlelo zokusebenza zingafinyelela lolu lwazi nezithombe zakho ezilondoloziwe."</string>
+    <string name="remember_location_no" msgid="7541394381714894896">"Cha ngiyabonga"</string>
+    <string name="remember_location_yes" msgid="862884269285964180">"Yebo"</string>
+    <string name="menu_camera" msgid="3476709832879398998">"Ikhamera"</string>
+    <string name="menu_search" msgid="7580008232297437190">"Sesha"</string>
+    <string name="tab_photos" msgid="9110813680630313419">"Izithombe"</string>
+    <string name="tab_albums" msgid="8079449907770685691">"Ama-albhamu"</string>
+  <plurals name="number_of_photos">
+    <item quantity="one" msgid="6949174783125614798">"%1$d isithombe"</item>
+    <item quantity="other" msgid="3813306834113858135">"%1$d izithombe"</item>
+  </plurals>
+</resources>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..5457650
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,489 @@
+<?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.
+ -->
+
+<resources>
+    <!-- Camera Preferences Video Quality entries -->
+    <string-array name="pref_video_quality_entries" translatable="false">
+        <item>@string/pref_video_quality_entry_1080p</item>
+        <item>@string/pref_video_quality_entry_720p</item>
+        <item>@string/pref_video_quality_entry_480p</item>
+        <item>@string/pref_video_quality_entry_high</item>
+        <item>@string/pref_video_quality_entry_low</item>
+   </string-array>
+
+    <string-array name="pref_video_quality_entryvalues" translatable="false">
+        <!-- The integer value of CamcorderProfile.QUALITY_1080P -->
+        <item>6</item>
+        <!-- The integer value of CamcorderProfile.QUALITY_720P -->
+        <item>@string/pref_video_quality_default</item>
+        <!-- The integer value of CamcorderProfile.QUALITY_480P -->
+        <item>4</item>
+        <!-- The integer value of CamcorderProfile.QUALITY_HIGH -->
+        <item>1</item>
+         <!-- The integer value of CamcorderProfile.QUALITY_LOW -->
+        <item>0</item>
+    </string-array>
+
+    <!-- These values correspond to the time interval between frame capture in millseconds
+    for time lapse recording -->
+    <string-array name="pref_video_time_lapse_frame_interval_entryvalues" translatable="false">
+        <item>0</item>
+        <item>500</item>
+        <item>1000</item>
+        <item>1500</item>
+        <item>2000</item>
+        <item>2500</item>
+        <item>3000</item>
+        <item>4000</item>
+        <item>5000</item>
+        <item>6000</item>
+        <item>10000</item>
+        <item>12000</item>
+        <item>15000</item>
+        <item>24000</item>
+        <item>30000</item>
+        <item>60000</item>
+        <item>90000</item>
+        <item>120000</item>
+        <item>150000</item>
+        <item>180000</item>
+        <item>240000</item>
+        <item>300000</item>
+        <item>360000</item>
+        <item>600000</item>
+        <item>720000</item>
+        <item>900000</item>
+        <item>1440000</item>
+        <item>1800000</item>
+        <item>3600000</item>
+        <item>5400000</item>
+        <item>7200000</item>
+        <item>9000000</item>
+        <item>10800000</item>
+        <item>14400000</item>
+        <item>18000000</item>
+        <item>21600000</item>
+        <item>36000000</item>
+        <item>43200000</item>
+        <item>54000000</item>
+        <item>86400000</item>
+    </string-array>
+
+    <!-- These values correspond to the time interval between frame capture in
+    different units (i.e. seconds, minutes, hours) for time lapse recording -->
+    <string-array name="pref_video_time_lapse_frame_interval_entries">
+        <item>@string/pref_video_time_lapse_frame_interval_off</item>
+        <item>@string/pref_video_time_lapse_frame_interval_500</item>
+        <item>@string/pref_video_time_lapse_frame_interval_1000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_1500</item>
+        <item>@string/pref_video_time_lapse_frame_interval_2000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_2500</item>
+        <item>@string/pref_video_time_lapse_frame_interval_3000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_4000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_5000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_6000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_10000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_12000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_15000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_24000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_30000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_60000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_90000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_120000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_150000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_180000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_240000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_300000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_360000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_600000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_720000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_900000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_1440000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_1800000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_3600000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_5400000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_7200000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_9000000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_10800000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_14400000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_18000000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_21600000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_36000000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_43200000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_54000000</item>
+        <item>@string/pref_video_time_lapse_frame_interval_86400000</item>
+    </string-array>
+
+    <!-- These values correspond to the time interval between frame capture
+    for time lapse recording -->
+    <string-array name="pref_video_time_lapse_frame_interval_duration_values" translatable="false">
+        <item>0.5</item>
+        <item>1</item>
+        <item>1.5</item>
+        <item>2</item>
+        <item>2.5</item>
+        <item>3</item>
+        <item>4</item>
+        <item>5</item>
+        <item>6</item>
+        <item>10</item>
+        <item>12</item>
+        <item>15</item>
+        <item>24</item>
+    </string-array>
+
+    <string-array name="pref_video_time_lapse_frame_interval_units">
+        <item>@string/time_lapse_seconds</item>
+        <item>@string/time_lapse_minutes</item>
+        <item>@string/time_lapse_hours</item>
+    </string-array>
+
+    <!-- Camera Preferences Picture size dialog box entries -->
+    <string-array name="pref_camera_picturesize_entries" translatable="false">
+        <item>@string/pref_camera_picturesize_entry_13mp</item>
+        <item>@string/pref_camera_picturesize_entry_8mp</item>
+        <item>@string/pref_camera_picturesize_entry_5mp</item>
+        <item>@string/pref_camera_picturesize_entry_5mp</item>
+        <item>@string/pref_camera_picturesize_entry_5mp</item>
+        <item>@string/pref_camera_picturesize_entry_4mp</item>
+        <item>@string/pref_camera_picturesize_entry_3mp</item>
+        <item>@string/pref_camera_picturesize_entry_2mp</item>
+        <item>@string/pref_camera_picturesize_entry_2mp_wide</item>
+        <item>@string/pref_camera_picturesize_entry_1_3mp</item>
+        <item>@string/pref_camera_picturesize_entry_1mp</item>
+        <item>@string/pref_camera_picturesize_entry_vga</item>
+        <item>@string/pref_camera_picturesize_entry_qvga</item>
+    </string-array>
+
+    <!-- When launching the camera app first time, we will set the picture
+         size to the first one in the list that is also supported by the
+         driver -->
+    <string-array name="pref_camera_picturesize_entryvalues" translatable="false">
+        <item>4128x3096</item>
+        <item>3264x2448</item>
+        <item>2592x1944</item>
+        <item>2592x1936</item>
+        <item>2560x1920</item>
+        <item>2688x1520</item>
+        <item>2048x1536</item>
+        <item>1600x1200</item>
+        <item>1920x1088</item>
+        <item>1280x960</item>
+        <item>1024x768</item>
+        <item>640x480</item>
+        <item>320x240</item>
+    </string-array>
+
+    <!-- Camera Preferences focus mode dialog box entries -->
+    <string-array name="pref_camera_focusmode_entries" translatable="false">
+        <item>@string/pref_camera_focusmode_entry_auto</item>
+        <item>@string/pref_camera_focusmode_entry_infinity</item>
+        <item>@string/pref_camera_focusmode_entry_macro</item>
+    </string-array>
+
+    <string-array name="pref_camera_focusmode_entryvalues" translatable="false">
+        <item>auto</item>
+        <item>infinity</item>
+        <item>macro</item>
+    </string-array>
+
+    <string-array name="pref_camera_focusmode_labels" translatable="false">
+        <item>@string/pref_camera_focusmode_label_auto</item>
+        <item>@string/pref_camera_focusmode_label_infinity</item>
+        <item>@string/pref_camera_focusmode_label_macro</item>
+    </string-array>
+
+    <!-- Camera Preferences flash mode dialog box entries -->
+    <string-array name="pref_camera_flashmode_entries" translatable="false">
+        <item>@string/pref_camera_flashmode_entry_off</item>
+        <item>@string/pref_camera_flashmode_entry_auto</item>
+        <item>@string/pref_camera_flashmode_entry_on</item>
+    </string-array>
+
+    <string-array name="pref_camera_flashmode_labels" translatable="false">
+        <item>@string/pref_camera_flashmode_label_off</item>
+        <item>@string/pref_camera_flashmode_label_auto</item>
+        <item>@string/pref_camera_flashmode_label_on</item>
+    </string-array>
+
+    <string-array name="pref_camera_flashmode_entryvalues" translatable="false">
+        <item>off</item>
+        <item>auto</item>
+        <item>on</item>
+    </string-array>
+
+    <array name="camera_flashmode_icons" translatable="false">
+        <item>@drawable/ic_flash_off_holo_light</item>
+        <item>@drawable/ic_flash_auto_holo_light</item>
+        <item>@drawable/ic_flash_on_holo_light</item>
+    </array>
+
+    <array name="camera_flashmode_largeicons" translatable="false">
+        <item>@drawable/ic_flash_off_holo_light</item>
+        <item>@drawable/ic_flash_auto_holo_light</item>
+        <item>@drawable/ic_flash_on_holo_light</item>
+    </array>
+
+    <!-- Videocamera Preferences flash mode dialog box entries -->
+    <string-array name="pref_camera_video_flashmode_entries" translatable="false">
+        <item>@string/pref_camera_flashmode_entry_on</item>
+        <item>@string/pref_camera_flashmode_entry_off</item>
+    </string-array>
+
+    <string-array name="pref_camera_video_flashmode_labels" translatable="false">
+        <item>@string/pref_camera_flashmode_label_on</item>
+        <item>@string/pref_camera_flashmode_label_off</item>
+    </string-array>
+
+    <string-array name="pref_camera_video_flashmode_entryvalues" translatable="false">
+        <item>torch</item>
+        <item>off</item>
+    </string-array>
+
+    <array name="video_flashmode_icons" translatable="false">
+        <item>@drawable/ic_flash_on_holo_light</item>
+        <item>@drawable/ic_flash_off_holo_light</item>
+    </array>
+
+    <array name="video_flashmode_largeicons" translatable="false">
+        <item>@drawable/ic_flash_on_holo_light</item>
+        <item>@drawable/ic_flash_off_holo_light</item>
+    </array>
+
+    <string-array name="pref_camera_recordlocation_entryvalues" translatable="false">
+        <item>off</item>
+        <item>on</item>
+    </string-array>
+
+    <array name="pref_camera_recordlocation_entries" translatable="false">
+        <item>@string/setting_off</item>
+        <item>@string/setting_on</item>
+    </array>
+
+    <array name="pref_camera_recordlocation_labels" translatable="false">
+        <item>@string/pref_camera_location_label</item>
+        <item>@string/pref_camera_location_label</item>
+    </array>
+
+    <array name="camera_recordlocation_icons" translatable="false">
+        <item>@drawable/ic_location_off</item>
+        <item>@drawable/ic_location</item>
+    </array>
+
+    <array name="camera_recordlocation_largeicons" translatable="false">
+        <item>@drawable/ic_location_off</item>
+        <item>@drawable/ic_location</item>
+    </array>
+
+    <!-- Camera Preferences White Balance dialog box entries -->
+    <string-array name="pref_camera_whitebalance_entries" translatable="false">
+        <item>@string/pref_camera_whitebalance_entry_incandescent</item>
+        <item>@string/pref_camera_whitebalance_entry_fluorescent</item>
+        <item>@string/pref_camera_whitebalance_entry_auto</item>
+        <item>@string/pref_camera_whitebalance_entry_daylight</item>
+        <item>@string/pref_camera_whitebalance_entry_cloudy</item>
+    </string-array>
+
+    <string-array name="pref_camera_whitebalance_labels" translatable="false">
+        <item>@string/pref_camera_whitebalance_label_incandescent</item>
+        <item>@string/pref_camera_whitebalance_label_fluorescent</item>
+        <item>@string/pref_camera_whitebalance_label_auto</item>
+        <item>@string/pref_camera_whitebalance_label_daylight</item>
+        <item>@string/pref_camera_whitebalance_label_cloudy</item>
+    </string-array>
+
+    <string-array name="pref_camera_whitebalance_entryvalues" translatable="false">
+        <item>incandescent</item>
+        <item>fluorescent</item>
+        <item>auto</item>
+        <item>daylight</item>
+        <item>cloudy-daylight</item>
+    </string-array>
+
+    <array name="whitebalance_icons" translatable="false">
+        <item>@drawable/ic_wb_incandescent</item>
+        <item>@drawable/ic_wb_fluorescent</item>
+        <item>@drawable/ic_wb_auto</item>
+        <item>@drawable/ic_wb_sunlight</item>
+        <item>@drawable/ic_wb_cloudy</item>
+    </array>
+
+    <array name="whitebalance_largeicons" translatable="false">
+        <item>@drawable/ic_wb_incandescent</item>
+        <item>@drawable/ic_wb_fluorescent</item>
+        <item>@drawable/ic_wb_auto</item>
+        <item>@drawable/ic_wb_sunlight</item>
+        <item>@drawable/ic_wb_cloudy</item>
+    </array>
+
+    <array name="camera_wb_indicators" translatable="false">
+        <item>@drawable/ic_indicator_wb_tungsten</item>
+        <item>@drawable/ic_indicator_wb_fluorescent</item>
+        <item>@drawable/ic_indicator_wb_off</item>
+        <item>@drawable/ic_indicator_wb_daylight</item>
+        <item>@drawable/ic_indicator_wb_cloudy</item>
+    </array>
+
+    <!-- Camera Preferences Scene Mode dialog box entries -->
+    <string-array name="pref_camera_scenemode_entries" translatable="false">
+        <item>@string/pref_camera_scenemode_entry_action</item>
+        <item>@string/pref_camera_scenemode_entry_night</item>
+        <item>@string/pref_camera_scenemode_entry_auto</item>
+        <item>@string/pref_camera_scenemode_entry_sunset</item>
+        <item>@string/pref_camera_scenemode_entry_party</item>
+    </string-array>
+
+    <string-array name="pref_camera_scenemode_labels">
+        <item>@string/pref_camera_scenemode_label_action</item>
+        <item>@string/pref_camera_scenemode_label_night</item>
+        <item>@string/pref_camera_scenemode_label_auto</item>
+        <item>@string/pref_camera_scenemode_label_sunset</item>
+        <item>@string/pref_camera_scenemode_label_party</item>
+    </string-array>
+
+    <array name="pref_camera_scenemode_icons">
+        <item>@drawable/ic_sce_action</item>
+        <item>@drawable/ic_sce_night</item>
+        <item>@drawable/ic_sce_off</item>
+        <item>@drawable/ic_sce_sunset</item>
+        <item>@drawable/ic_sce_party</item>
+    </array>
+
+    <string-array name="pref_camera_scenemode_entryvalues" translatable="false">
+        <item>action</item>
+        <item>night</item>
+        <item>auto</item>
+        <item>sunset</item>
+        <item>party</item>
+    </string-array>
+
+    <array name="camera_id_entries" translatable="false">
+        <item>@string/pref_camera_id_entry_back</item>
+        <item>@string/pref_camera_id_entry_front</item>
+    </array>
+
+    <array name="camera_id_labels" translatable="false">
+        <item>@string/pref_camera_id_label_back</item>
+        <item>@string/pref_camera_id_label_front</item>
+    </array>
+
+    <array name="camera_id_icons" translatable="false">
+        <item>@drawable/ic_switch_back</item>
+        <item>@drawable/ic_switch_front</item>
+    </array>
+
+    <array name="camera_id_largeicons" translatable="false">
+        <item>@drawable/ic_switch_back</item>
+        <item>@drawable/ic_switch_front</item>
+    </array>
+
+    <string-array name="pref_video_effect_entries" translatable="false">
+        <item>@string/effect_none</item>
+        <item>@string/effect_goofy_face_squeeze</item>
+        <item>@string/effect_goofy_face_big_eyes</item>
+        <item>@string/effect_goofy_face_big_mouth</item>
+        <item>@string/effect_goofy_face_small_mouth</item>
+        <item>@string/effect_goofy_face_big_nose</item>
+        <item>@string/effect_goofy_face_small_eyes</item>
+        <item>@string/effect_backdropper_space</item>
+        <item>@string/effect_backdropper_sunset</item>
+        <item>@string/effect_backdropper_gallery</item>
+    </string-array>
+
+    <string-array name="pref_video_effect_entryvalues" translatable="false">
+        <item>@string/pref_video_effect_default</item>
+        <item>goofy_face/squeeze</item>
+        <item>goofy_face/big_eyes</item>
+        <item>goofy_face/big_mouth</item>
+        <item>goofy_face/small_mouth</item>
+        <item>goofy_face/big_nose</item>
+        <item>goofy_face/small_eyes</item>
+        <item>backdropper/file:///system/media/video/AndroidInSpace.480p.mp4</item>
+        <item>backdropper/file:///system/media/video/Sunset.480p.mp4</item>
+        <item>backdropper/gallery</item>
+    </string-array>
+
+    <array name="video_effect_icons" translatable="false">
+        <item>@drawable/ic_effects_holo_light</item>
+        <item>@drawable/ic_video_effects_faces_squeeze_holo_dark</item>
+        <item>@drawable/ic_video_effects_faces_big_eyes_holo_dark</item>
+        <item>@drawable/ic_video_effects_faces_big_mouth_holo_dark</item>
+        <item>@drawable/ic_video_effects_faces_small_mouth_holo_dark</item>
+        <item>@drawable/ic_video_effects_faces_big_nose_holo_dark</item>
+        <item>@drawable/ic_video_effects_faces_small_eyes_holo_dark</item>
+        <item>@drawable/ic_video_effects_background_intergalactic_holo</item>
+        <item>@drawable/ic_video_effects_background_fields_of_wheat_holo</item>
+        <item>@drawable/ic_video_effects_background_normal_holo_dark</item>
+    </array>
+
+    <string-array name="pref_camera_hdr_entries" translatable="false">
+        <item>@string/setting_off</item>
+        <item>@string/setting_on</item>
+    </string-array>
+
+    <string-array name="pref_camera_hdr_labels" translatable="false">
+        <item>@string/pref_camera_hdr_label</item>
+        <item>@string/pref_camera_hdr_label</item>
+    </string-array>
+
+    <string-array name="pref_camera_hdr_icons" translatable="false">
+        <item>@drawable/ic_hdr_off</item>
+        <item>@drawable/ic_hdr</item>
+    </string-array>
+
+    <string-array name="pref_camera_hdr_entryvalues" translatable="false">
+        <item>@string/setting_off_value</item>
+        <item>@string/setting_on_value</item>
+    </string-array>
+
+    <string-array name="pref_camera_timer_sound_entries" translatable="false">
+        <item>@string/setting_off</item>
+        <item>@string/setting_on</item>
+    </string-array>
+
+    <string-array name="pref_camera_timer_sound_entryvalues" translatable="false">
+        <item>@string/setting_off_value</item>
+        <item>@string/setting_on_value</item>
+    </string-array>
+
+    <!-- Default focus mode setting.-->
+    <string-array name="pref_camera_focusmode_default_array" translatable="false">
+        <item>continuous-picture</item>
+        <item>auto</item>
+    </string-array>
+
+    <!-- Icons for exposure compensation -->
+    <array name="pref_camera_exposure_icons" translatable="false">
+        <item>@drawable/ic_exposure_n3</item>
+        <item>@drawable/ic_exposure_n2</item>
+        <item>@drawable/ic_exposure_n1</item>
+        <item>@drawable/ic_exposure_0</item>
+        <item>@drawable/ic_exposure_p1</item>
+        <item>@drawable/ic_exposure_p2</item>
+        <item>@drawable/ic_exposure_p3</item>
+    </array>
+
+    <!--  Labels for Countdown timer -->
+    <string-array name="pref_camera_countdown_labels">
+        <item>@string/pref_camera_countdown_label_off</item>
+        <item>@string/pref_camera_countdown_label_one</item>
+        <item>@string/pref_camera_countdown_label_three</item>
+        <item>@string/pref_camera_countdown_label_ten</item>
+        <item>@string/pref_camera_countdown_label_fifteen</item>
+    </string-array>
+
+</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
new file mode 100644
index 0000000..5a00a69
--- /dev/null
+++ b/res/values/attrs.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <declare-styleable name="Theme.GalleryBase">
+        <attr name="listPreferredItemHeightSmall" format="dimension" />
+        <attr name="switchStyle" format="reference" />
+    </declare-styleable>
+
+    <!-- Camera resources below -->
+
+    <declare-styleable name="CameraPreference">
+        <attr name="title" format="string" />
+    </declare-styleable>
+    <declare-styleable name="ListPreference">
+        <attr name="key" format="string" />
+        <attr name="defaultValue" format="string|reference" />
+        <attr name="entryValues" format="reference" />
+        <attr name="entries" format="reference" />
+        <attr name="labelList" format="reference" />
+    </declare-styleable>
+    <declare-styleable name="IconIndicator">
+        <attr name="icons" format="reference" />
+        <attr name="modes" format="reference" />
+    </declare-styleable>
+    <declare-styleable name="IconListPreference">
+        <!-- If a preference does not have individual icons for each entry, it can has a single icon to represent it. -->
+        <attr name="singleIcon" format="reference" />
+        <attr name="icons" />
+        <attr name="largeIcons" format="reference" />
+        <attr name="images" format="reference" />
+    </declare-styleable>
+
+</resources>
diff --git a/res/values/bool.xml b/res/values/bool.xml
new file mode 100644
index 0000000..464842a
--- /dev/null
+++ b/res/values/bool.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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>
+    <bool name="show_action_bar_title">false</bool>
+</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..4fe9180
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <color name="default_background">#000</color>
+
+    <!-- configuration for album set page -->
+    <color name="albumset_background">#1A1A1A</color>
+    <color name="albumset_placeholder">#333</color>
+    <color name="albumset_label_background">#EE414143</color>
+    <color name="albumset_label_title">#FBFBFB</color>
+    <color name="albumset_label_count">#A9ABAD</color>
+
+    <!-- configuration for album page -->
+    <color name="album_background">#1A1A1A</color>
+    <color name="album_placeholder">#333</color>
+
+    <!-- configuration for photo page -->
+    <color name="photo_background">#1A1A1A</color>
+    <color name="photo_placeholder">#333</color>
+
+    <!-- configuration for manage cache page -->
+    <color name="cache_background">#1A1A1A</color>
+    <color name="cache_placeholder">#333</color>
+
+    <color name="bitmap_screennail_placeholder">#333</color>
+
+    <color name="slideshow_background">#1A1A1A</color>
+
+    <color name="button_dark_transparent_background">#6000</color>
+
+    <color name="ingest_highlight_semitransparent">#8833b5e5</color>
+    <color name="ingest_date_tile_text">#33b5e5</color>
+
+    <!-- Camera resources below -->
+
+    <color name="recording_time_elapsed_text">#FFFFFFFF</color>
+    <color name="recording_time_remaining_text">#FFFF0033</color>
+    <color name="on_viewfinder_label_background_color">#77333333</color>
+    <color name="review_control_pressed_color">#FF33B5E5</color>
+    <color name="review_control_pressed_fan_color">#3F33B5E5</color>
+    <color name="review_background">#FF000000</color>
+    <color name="icon_disabled_color">#DD777777</color>
+    <color name="time_lapse_arc">#FFC5C5C5</color>
+    <color name="indicator_background">#40000000</color>
+    <color name="popup_title_color">#ff33b5e5</color>
+    <color name="popup_background">#ff282828</color>
+    <color name="pano_progress_empty">#FF2E2E2E</color>
+    <color name="pano_progress_done">#FF33525E</color>
+    <color name="pano_progress_indication">#FF0099CC</color>
+    <color name="pano_progress_indication_fast">#FFFF2222</color>
+    <color name="mode_selection_border">#33B5E5</color>
+    <color name="holo_blue_light">#ff33b5e5</color>
+    <color name="bright_foreground_disabled_holo_dark">#ff4c4c4c</color>
+    <color name="bright_foreground_holo_dark">#fff3f3f3</color>
+    <color name="face_detect_start">#80ffffff</color>
+    <color name="face_detect_success">#8050d060</color>
+    <color name="face_detect_fail">#80d05060</color>
+    <color name="gray">#FFAAAAAA</color>
+
+</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..33e1e14
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<!-- Camera app resources that may need to be customized
+     for different hardware or product builds. -->
+<resources>
+    <!-- Maximum recording length in milliseconds. 0 means unlimited. -->
+    <integer name="max_video_recording_length">0</integer>
+</resources>
diff --git a/res/values/crop_colors.xml b/res/values/crop_colors.xml
new file mode 100644
index 0000000..3f64c50
--- /dev/null
+++ b/res/values/crop_colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <color name="crop_shadow_color">#CF000000</color>
+    <color name="crop_shadow_wp_color">#4F000000</color>
+    <color name="crop_wp_markers">#7FFFFFFF</color>
+</resources>
diff --git a/res/values/crop_dimens.xml b/res/values/crop_dimens.xml
new file mode 100644
index 0000000..fc91dbf
--- /dev/null
+++ b/res/values/crop_dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <dimen name="preview_margin">2dp</dimen>
+    <dimen name="shadow_margin">5dp</dimen>
+    <dimen name="crop_min_side">45dp</dimen>
+    <dimen name="crop_touch_tolerance">20dp</dimen>
+    <dimen name="wp_selector_dash_length">4dp</dimen>
+    <dimen name="wp_selector_off_length">4dp</dimen>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..692a87f
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2013, 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>
+    <dimen name="hint_y_offset">64dp</dimen>
+    <dimen name="pano_mosaic_surface_height">240dp</dimen>
+    <dimen name="pano_review_button_width">70dp</dimen>
+    <dimen name="pano_review_button_height">45dp</dimen>
+    <dimen name="setting_popup_right_margin">5dp</dimen>
+    <dimen name="setting_row_height">50dp</dimen>
+    <dimen name="setting_item_text_size">18sp</dimen>
+    <dimen name="setting_knob_width">20dp</dimen>
+    <dimen name="setting_knob_text_size">20dp</dimen>
+    <dimen name="setting_item_text_width">95dp</dimen>
+    <dimen name="setting_popup_window_width">240dp</dimen>
+    <dimen name="setting_item_list_margin">14dp</dimen>
+    <dimen name="indicator_bar_width">48dp</dimen>
+    <dimen name="popup_title_text_size">22dp</dimen>
+    <dimen name="popup_title_frame_min_height">49dp</dimen>
+    <dimen name="big_setting_popup_window_width">320dp</dimen>
+    <dimen name="setting_item_icon_width">28dp</dimen>
+    <dimen name="effect_setting_item_icon_width">40dp</dimen>
+    <dimen name="effect_setting_item_text_size">12sp</dimen>
+    <dimen name="effect_setting_type_text_size">12sp</dimen>
+    <dimen name="effect_setting_type_text_min_height">36dp</dimen>
+    <dimen name="effect_setting_clear_text_size">20dp</dimen>
+    <dimen name="effect_setting_clear_text_min_height">45dp</dimen>
+    <dimen name="effect_setting_type_text_left_padding">16dp</dimen>
+    <dimen name="onscreen_indicators_height">28dp</dimen>
+    <dimen name="onscreen_exposure_indicator_text_size">15dp</dimen>
+    <dimen name="switch_padding">16dp</dimen>
+    <dimen name="switch_min_width">96dp</dimen>
+    <dimen name="switch_text_max_width">44dp</dimen>
+    <dimen name="thumb_text_padding">12dp</dimen>
+    <dimen name="thumb_text_size">14sp</dimen>
+    <dimen name="setting_popup_right_margin_large">8dp</dimen>
+    <dimen name="setting_row_height_large">54dp</dimen>
+    <dimen name="setting_popup_window_width_large">260dp</dimen>
+    <dimen name="indicator_bar_width_large">72dp</dimen>
+    <dimen name="setting_item_icon_width_large">48dp</dimen>
+    <dimen name="onscreen_indicators_height_large">36dp</dimen>
+    <dimen name="pano_mosaic_surface_height_xlarge">480dp</dimen>
+    <dimen name="pano_review_button_width_xlarge">180dp</dimen>
+    <dimen name="pano_review_button_height_xlarge">115dp</dimen>
+    <dimen name="setting_row_height_xlarge">50dp</dimen>
+    <dimen name="setting_item_text_size_xlarge">21dp</dimen>
+    <dimen name="setting_knob_width_xlarge">50dp</dimen>
+    <dimen name="setting_item_text_width_xlarge">130dp</dimen>
+    <dimen name="setting_popup_window_width_xlarge">410dp</dimen>
+    <dimen name="setting_item_list_margin_xlarge">24dp</dimen>
+    <dimen name="indicator_bar_width_xlarge">13dp</dimen>
+    <dimen name="popup_title_text_size_xlarge">22dp</dimen>
+    <dimen name="popup_title_frame_min_height_xlarge">60dp</dimen>
+    <dimen name="big_setting_popup_window_width_xlarge">590dp</dimen>
+    <dimen name="setting_item_icon_width_xlarge">35dp</dimen>
+    <dimen name="effect_setting_item_icon_width_xlarge">54dp</dimen>
+    <dimen name="effect_setting_item_text_size_xlarge">21dp</dimen>
+    <dimen name="effect_setting_type_text_size_xlarge">21dp</dimen>
+    <dimen name="effect_setting_type_text_min_height_xlarge">34dp</dimen>
+    <dimen name="effect_setting_clear_text_size_xlarge">23dp</dimen>
+    <dimen name="effect_setting_clear_text_min_height_xlarge">44dp</dimen>
+    <dimen name="effect_setting_type_text_left_padding_xlarge">26dp</dimen>
+    <dimen name="onscreen_indicators_height_xlarge">36dp</dimen>
+    <dimen name="onscreen_exposure_indicator_text_size_xlarge">18dp</dimen>
+    <dimen name="pie_radius_start">80dp</dimen>
+    <dimen name="pie_radius_increment">48dp</dimen>
+    <dimen name="pie_touch_slop">12dp</dimen>
+    <dimen name="pie_touch_offset">32dp</dimen>
+    <dimen name="pie_view_size">48dp</dimen>
+    <dimen name="pie_arc_offset">48dp</dimen>
+    <dimen name="pie_item_radius">370dp</dimen>
+    <dimen name="pie_arc_radius">214dp</dimen>
+    <dimen name="pie_deadzone_width">36dp</dimen>
+    <dimen name="pie_anglezone_width">92dp</dimen>
+    <dimen name="focus_radius_offset">8dp</dimen>
+    <dimen name="focus_inner_offset">24dp</dimen>
+    <dimen name="focus_outer_stroke">3dp</dimen>
+    <dimen name="focus_inner_stroke">2dp</dimen>
+    <dimen name="zoom_ring_min">48dp</dimen>
+    <dimen name="switcher_size">72dp</dimen>
+    <dimen name="face_circle_stroke">2dip</dimen>
+    <dimen name="zoom_font_size">14pt</dimen>
+    <dimen name="shutter_offset">-22dp</dimen>
+    <dimen name="size_thumbnail">200dip</dimen>
+    <dimen name="size_preview">400dip</dimen>
+    <dimen name="navigation_bar_height">48dip</dimen>
+    <dimen name="navigation_bar_width">42dip</dimen>
+    <dimen name="capture_size">48dip</dimen>
+    <dimen name="capture_border">8dip</dimen>
+    <dimen name="capture_margin_right">16dip</dimen>
+    <dimen name="capture_margin_top">16dip</dimen>
+    <dimen name="camera_controls_size">0dip</dimen>
+    <dimen name="camera_film_strip_gap">32dip</dimen>
+
+    <dimen name="appwidget_width">180dp</dimen>
+    <dimen name="appwidget_height">180dp</dimen>
+    <dimen name="stack_photo_width">160dp</dimen>
+    <dimen name="stack_photo_height">120dp</dimen>
+
+    <!-- configuration for legacy album set page -->
+    <integer name="albumset_rows_land">2</integer>
+    <integer name="albumset_rows_port">3</integer>
+    <dimen name="albumset_padding_top">7dp</dimen>
+    <dimen name="albumset_padding_bottom">7dp</dimen>
+    <dimen name="albumset_slot_gap">7dp</dimen>
+
+    <dimen name="albumset_label_background_height">30dp</dimen>
+    <dimen name="albumset_title_offset">10dp</dimen>
+    <dimen name="albumset_count_offset">10dp</dimen>
+    <dimen name="albumset_title_font_size">12sp</dimen>
+    <dimen name="albumset_count_font_size">9sp</dimen>
+    <dimen name="albumset_left_margin">2dp</dimen>
+    <dimen name="albumset_title_right_margin">20dp</dimen>
+    <dimen name="albumset_icon_size">25dp</dimen>
+
+    <!-- configuration for album page -->
+    <integer name="album_rows_land">2</integer>
+    <integer name="album_rows_port">4</integer>
+    <dimen name="album_slot_gap">5dp</dimen>
+
+    <!-- configuration for manage page -->
+    <dimen name="cache_pin_size">24dp</dimen>
+    <dimen name="cache_pin_margin">8dp</dimen>
+
+    <!-- for manage cache bar -->
+    <dimen name="manage_cache_bottom_height">48dp</dimen>
+
+    <!--  configuration for filtershow UI -->
+    <dimen name="thumbnail_size">96dip</dimen>
+    <dimen name="thumbnail_margin">3dip</dimen>
+    <dimen name="action_item_height">175dip</dimen>
+
+    <!-- configuration for album set page -->
+    <dimen name="album_set_item_image_height">120dp</dimen>
+    <dimen name="album_set_item_width">140dp</dimen>
+
+    <!-- configuration for preview in editor -->
+    <dimen name="photoeditor_text_size">12dp</dimen>
+    <dimen name="photoeditor_text_padding">10dp</dimen>
+    <dimen name="photoeditor_original_text_size">18dp</dimen>
+    <dimen name="photoeditor_original_text_margin">4dp</dimen>
+</resources>
diff --git a/res/values/filtershow_color.xml b/res/values/filtershow_color.xml
new file mode 100644
index 0000000..d7cf79d
--- /dev/null
+++ b/res/values/filtershow_color.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <color name="yellow">#FFFF00</color>
+    <color name="green">#00FF00</color>
+    <color name="red">#FF0000</color>
+    <color name="blue">#0000FF</color>
+    <color name="text_toolbar">#FFFFFF</color>
+    <color name="background_screen">#101010</color>
+    <color name="background_toolbar">#363949</color>
+    <color name="background_main_toolbar">#232323</color>
+    <color name="toolbar_separation_line">#333333</color>
+    <color name="slider_dot_color">#6464FF</color>
+    <color name="slider_line_color">#33B5E5</color>
+    <color name="state_panel_separation_line">#232323</color>
+    <color name="filtershow_background">#333333</color>
+    <color name="filtershow_graphic">#717171</color>
+    <color name="filtershow_stateview_end_background">#232323</color>
+    <color name="filtershow_stateview_end_text">#a7a7a7</color>
+    <color name="filtershow_stateview_background">#464646</color>
+    <color name="filtershow_stateview_text">#FFFFFF</color>
+    <color name="filtershow_stateview_selected_background">#c8c8c8</color>
+    <color name="filtershow_stateview_selected_text">#000000</color>
+    <color name="filtershow_categoryview_background">#1a1a1a</color>
+    <color name="filtershow_categoryview_text">#a7a7a7</color>
+    <color name="filtershow_category_selection">#ffffffff</color>
+    <color name="gradcontrol_point_center">#ffffffff</color>
+    <color name="gradcontrol_point_edge">#ffffffff</color>
+    <color name="gradcontrol_graypoint_center">#888888</color>
+    <color name="gradcontrol_graypoint_edge">#BBBBBB</color>
+    <color name="gradcontrol_point_shadow_start">#66000000</color>
+    <color name="gradcontrol_point_shadow_end">#00000000</color>
+    <color name="gradcontrol_line_color">#FFFFFF</color>
+    <color name="gradcontrol_line_shadow">#000000</color>
+
+
+</resources>
\ No newline at end of file
diff --git a/res/values/filtershow_ids.xml b/res/values/filtershow_ids.xml
new file mode 100644
index 0000000..b315d12
--- /dev/null
+++ b/res/values/filtershow_ids.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2013 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>
+    <!-- Buttons ids for the filters -->
+    <item type="id" name="tinyplanetButton" />
+    <item type="id" name="vignetteButton" />
+    <item type="id" name="vibranceButton" />
+    <item type="id" name="contrastButton" />
+    <item type="id" name="saturationButton" />
+    <item type="id" name="bwfilterButton" />
+    <item type="id" name="wbalanceButton" />
+    <item type="id" name="hueButton" />
+    <item type="id" name="exposureButton" />
+    <item type="id" name="shadowRecoveryButton" />
+    <item type="id" name="highlightRecoveryButton" />
+    <item type="id" name="sharpenButton" />
+    <item type="id" name="curvesButtonRGB" />
+    <item type="id" name="negativeButton" />
+    <item type="id" name="edgeButton" />
+    <item type="id" name="kmeansButton" />
+    <item type="id" name="downsampleButton" />
+    <item type="id" name="drawOnImageButton" />
+    <item type="id" name="imageCurves" />
+    <item type="id" name="imageZoom" />
+    <item type="id" name="editorDraw" />
+    <item type="id" name="editorRedEye" />
+    <item type="id" name="imageOnlyEditor" />
+    <item type="id" name="vignetteEditor" />
+    <item type="id" name="editorCrop" />
+    <item type="id" name="editorFlip" />
+    <item type="id" name="editorRotate" />
+    <item type="id" name="editorStraighten" />
+    <item type="id" name="editorParametric" />
+    <item type="id" name="editorGrad" />
+    <item type="id" name="editorChanSat" />
+</resources>
diff --git a/res/values/filtershow_strings.xml b/res/values/filtershow_strings.xml
new file mode 100644
index 0000000..c4546c8
--- /dev/null
+++ b/res/values/filtershow_strings.xml
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <!--  Title for the image editor activity [CHAR LIMIT=NONE]-->
+    <string name="title_activity_filter_show">Photo Editor</string>
+
+    <!--  String shown when we cannot load the image when starting the activity [CHAR LIMIT=NONE] -->
+    <string name="cannot_load_image">Cannot load the image!</string>
+    <!--  String displayed when showing the original image [CHAR LIMIT=NONE] -->
+    <string name="original_picture_text">@string/original</string>
+    <!--  String displayed when setting the homepage wallpaper in the background [CHAR LIMIT=NONE] -->
+    <string name="setting_wallpaper">Setting wallpaper</string>
+
+    <!--  generic strings -->
+
+
+    <!--  Text for to display on a download failure [CHAR LIMIT=NONE] -->
+    <string name="download_failure">Could not download photo. Network unavailable.</string>
+    <!--  Text for original image [CHAR LIMIT=20] -->
+    <string name="original">Original</string>
+    <!--  Text for original image [CHAR LIMIT=20] -->
+    <string name="saved">Saved</string>
+    <!--  Text for filters that apply a border to a picture [CHAR LIMIT=20] -->
+    <string name="borders" msgid="4461692156695893616">Borders</string>
+
+    <!--  actionbar menu -->
+
+    <!--  Text for the undo menu item [CHAR LIMIT=20] -->
+    <string name="filtershow_undo">Undo</string>
+    <!--  Text for redo menu item [CHAR LIMIT=20] -->
+    <string name="filtershow_redo">Redo</string>
+    <!--  Text for the image menu item showing the filters that have been applied [CHAR LIMIT=30] -->
+    <string name="show_imagestate_panel">Show Applied Effects</string>
+    <!--  Text for the image state panel menu item [CHAR LIMIT=30] -->
+    <string name="hide_imagestate_panel">Hide Applied Effects</string>
+    <!--  Text for the menu item to export a flattened photo[CHAR LIMIT=30] -->
+    <string name="export_flattened">Export Flattened Image</string>
+    <!--  Text for selecting export image quality [CHAR LIMIT=100] -->
+    <string name="select_compression">Select output quality.</string>
+    <!--  Text for quality value tag [CHAR LIMIT=30] -->
+    <string name="quality">Quality</string>
+
+    <!--  Name for the overflow menu item for settings [CHAR LIMIT=20] -->
+    <string name="menu_settings">Settings</string>
+
+    <!--  Exit Dialog -->
+
+    <!--  String displayed when exiting with unsaved changes [CHAR LIMIT=NONE] -->
+    <string name="unsaved">There are unsaved changes to this image.</string>
+    <!--  String displayed when exiting with unsaved changes [CHAR LIMIT=NONE] -->
+    <string name="save_before_exit">Do you want to save before exiting?</string>
+    <!--  String displayed when saving and exiting editor [CHAR LIMIT=NONE] -->
+    <string name="save_and_exit">Save and Exit</string>
+    <!--  String displayed when exiting editor[CHAR LIMIT=NONE] -->
+    <string name="exit">Exit</string>
+
+    <!--  History Panel -->
+
+    <!--  Text for the history panel title [CHAR LIMIT=50] -->
+    <string name="history">History</string>
+    <!--  Text for the history panel reset button [CHAR LIMIT=20]-->
+    <string name="reset">Reset</string>
+    <!--  Text for the original image[CHAR LIMIT=20]-->
+    <string name="history_original">@string/original</string>
+
+    <!--  Image state panel -->
+
+    <!--  Text for the image state panel title [CHAR LIMIT=50] -->
+    <string name="imageState">Applied Effects</string>
+
+    <!--  Additional filters buttons  -->
+
+    <!--  Label for the compare original image filter button [CHAR LIMIT=15] -->
+    <string name="compare_original">Compare</string>
+    <!--  Label for the apply effect button [CHAR LIMIT=15] -->
+    <string name="apply_effect">Apply</string>
+    <!--  Label for the reset effect button [CHAR LIMIT=15] -->
+    <string name="reset_effect">Reset</string>
+    <!--  Label for aspect [CHAR LIMIT=15] -->
+    <string name="aspect">Aspect</string>
+    <!--  Label for the aspect 1:1 effect [CHAR LIMIT=15] -->
+    <string name="aspect1to1_effect">1:1</string>
+    <!--  Label for the aspect 4:3 effect [CHAR LIMIT=15] -->
+    <string name="aspect4to3_effect">4:3</string>
+    <!--  Label for the aspect 3:4 effect [CHAR LIMIT=15] -->
+    <string name="aspect3to4_effect">3:4</string>
+    <!--  Label for the aspect 4:7 effect [CHAR LIMIT=15] -->
+    <string name="aspect4to6_effect">4:6</string>
+    <!--  Label for the aspect 5:7 effect [CHAR LIMIT=15] -->
+    <string name="aspect5to7_effect">5:7</string>
+    <!--  Label for the aspect 7:5 effect [CHAR LIMIT=15] -->
+    <string name="aspect7to5_effect">7:5</string>
+    <!--  Label for the aspect 1:1 effect [CHAR LIMIT=15] -->
+    <string name="aspect9to16_effect">16:9</string>
+    <!--  Label for the aspect None effect [CHAR LIMIT=15] -->
+    <string name="aspectNone_effect">None</string>
+    <!--  Label for the aspect None effect [CHAR LIMIT=15] -->
+    <string name="aspectOriginal_effect">@string/original</string>
+    <!-- Label for when the aspect ratio is fixed to a value [CHAR LIMIT=15] -->
+    <string name="Fixed">Fixed</string>
+    <!--  Label for the tuny planet effect [CHAR LIMIT=10] -->
+    <string name="tinyplanet">Tiny Planet</string>
+
+    <!--  Filters buttons -->
+
+    <!--  Label for the image exposure (brightness) filter button [CHAR LIMIT=10] -->
+    <string name="exposure" msgid="1229093066434614811">Exposure</string>
+    <!--  Label for the image sharpness (clarity, distinctness) filter button [CHAR LIMIT=10] -->
+    <string name="sharpness">Sharpness</string>
+    <!--  Label for the image contrast (color difference) filter button [CHAR LIMIT=10] -->
+    <string name="contrast">Contrast</string>
+    <!--  Label for the image vibrance (strengthens colors) filter button [CHAR LIMIT=10] -->
+    <string name="vibrance">Vibrance</string>
+    <!--  Label for the image saturation (brightens colors) filter button [CHAR LIMIT=10] -->
+    <string name="saturation">Saturation</string>
+    <!--  Label for the image BW filter (makes black & white) button [CHAR LIMIT=10] -->
+    <string name="bwfilter">BW Filter</string>
+    <!--  Label for the image Autocolor filter (makes off-white colors whiter) button [CHAR LIMIT=10] -->
+    <string name="wbalance">Autocolor</string>
+    <!--  Label for the image Hue filter (color, shade, tinge, tone) button [CHAR LIMIT=10] -->
+    <string name="hue">Hue</string>
+    <!--  Label for the image shadow recovery (lightens/darkens shadows) filter button [CHAR LIMIT=10] -->
+    <string name="shadow_recovery">Shadows</string>
+    <!--  Label for the image highlights recovery (lightens/darkens bright regions) filter button [CHAR LIMIT=15] -->
+    <string name="highlight_recovery">Highlights</string>
+    <!--  Label for the image curves filter button [CHAR LIMIT=10] -->
+    <string name="curvesRGB">Curves</string>
+    <!--  Label for the image vignette filter (darkens photo around edges) button [CHAR LIMIT=10] -->
+    <string name="vignette">Vignette</string>
+    <!--  Label for the image effect that removes redeye. [CHAR LIMIT=10] -->
+    <string name="redeye">Red Eye</string>
+    <!--  Label for the that allows drawing on Image [CHAR LIMIT=10] -->
+    <string name="imageDraw">Draw</string>
+    <!--  Label for the image straighten effect [CHAR LIMIT=15] -->
+    <string name="straighten" msgid="5217801513491493491">Straighten</string>
+    <!--  Label for the image crop effect [CHAR LIMIT=15] -->
+    <string name="crop" msgid="5584000454518174632">Crop</string>
+    <!--  Label for the image rotate effect [CHAR LIMIT=15] -->
+    <string name="rotate" msgid="460017689320955494">Rotate</string>
+    <!--  Label for the image flip effect [CHAR LIMIT=15] -->
+    <string name="mirror">Mirror</string>
+    <!-- Name for the photo effect that inverts photo to negative images. [CHAR LIMIT=10] -->
+    <string name="negative">Negative</string>
+    <!--  Label for having no filters applied to the image [CHAR LIMIT=10] -->
+    <string name="none" msgid="3601545724573307541">None</string>
+    <!--  Label for the image edges effect (highlights edges in image) [CHAR LIMIT=10] -->
+    <string name="edge">Edges</string>
+    <!--  Label for an image effect that replicates the "pop art" style of segmenting
+          images into solid colors, as popularized by Andy Warhol [CHAR LIMIT=10] -->
+    <string name="kmeans">Warhol</string>
+    <!--  Label for the image downsampling effect (makes image smaller) [CHAR LIMIT=15] -->
+    <string name="downsample">Downsample</string>
+    <!--  Label for the image graduated filter effect  [CHAR LIMIT=15] -->
+    <string name="grad">Graduated</string>
+    <!--  Label for the Brightness effect  [CHAR LIMIT=20] -->
+    <string name="editor_grad_brightness">Brightness</string>
+    <!--  Label for the Contrast filter effect  [CHAR LIMIT=20] -->
+    <string name="editor_grad_contrast">Contrast</string>
+    <!--  Label for the saturation effect  [CHAR LIMIT=20] -->
+    <string name="editor_grad_saturation">Saturation</string>
+    <!--  Label for the Main or Master control for per channel saturation effect [CHAR LIMIT=20] -->
+    <string name="editor_chan_sat_main">Main</string>
+    <!--  Label for the red control for per channel saturation effect [CHAR LIMIT=20] -->
+    <string name="editor_chan_sat_red">Red</string>
+    <!--  Label for the yellow control for per channel saturation effect [CHAR LIMIT=20] -->
+    <string name="editor_chan_sat_yellow">Yellow</string>
+    <!--  Label for the green control for per channel saturation effect [CHAR LIMIT=20] -->
+    <string name="editor_chan_sat_green">Green</string>
+    <!--  Label for the cyan control for per channel saturation effect [CHAR LIMIT=20] -->
+    <string name="editor_chan_sat_cyan">Cyan</string>
+    <!--  Label for the blue control for per channel saturation effect [CHAR LIMIT=20] -->
+    <string name="editor_chan_sat_blue">Blue</string>
+    <!--  Label for the Magenta control for per channel saturation effect [CHAR LIMIT=20] -->
+    <string name="editor_chan_sat_magenta">Magenta</string>
+    <!--  Label for the image graduated filter effect  [CHAR LIMIT=20] -->
+    <string name="editor_grad_style">Style</string>
+    <!--  Label for the image new grad layer  [CHAR LIMIT=20] -->
+    <string name="editor_grad_new">new</string>
+
+
+    <!--  Labels for the curves tool -->
+
+    <!--  Label for the curves tool, all channels (RGB) [CHAR LIMIT=3] -->
+    <string name="curves_channel_rgb">RGB</string>
+    <!--  Label for the curves tool, Red color channel [CHAR LIMIT=14] -->
+    <string name="curves_channel_red">Red</string>
+    <!--  Label for the curves tool, Green color channel [CHAR LIMIT=14] -->
+    <string name="curves_channel_green">Green</string>
+    <!--  Label for the curves tool, Blue color channel [CHAR LIMIT=14] -->
+    <string name="curves_channel_blue">Blue</string>
+
+    <!--  Label for the The style to draw in [CHAR LIMIT=14] -->
+    <string name="draw_style">Style</string>
+    <!--  Label for the size to draw in in [CHAR LIMIT=14] -->
+    <string name="draw_size">Size</string>
+    <!--  Label for the color to draw in [CHAR LIMIT=14] -->
+    <string name="draw_color">Color</string>
+    <!--  Label for the line style of drawing in [CHAR LIMIT=14] -->
+    <string name="draw_style_line">Lines</string>
+    <!--  Label for the Marker brush style of drawing in [CHAR LIMIT=14] -->
+    <string name="draw_style_brush_spatter">Marker</string>
+    <!--  Label for the Spatter brush style of drawing in [CHAR LIMIT=14] -->
+    <string name="draw_style_brush_marker">Spatter</string>
+    <!--  Label for the removing drawing from screen [CHAR LIMIT=14] -->
+    <string name="draw_clear">Clear</string>
+
+    <!--  Label for the select the color [CHAR LIMIT=35] -->
+    <string name="color_pick_select">Choose custom color</string>
+    <!--  The title for the color pick dialog [CHAR LIMIT=20] -->
+    <string name="color_pick_title">Select Color</string>
+    <!--  The title for draw size [CHAR LIMIT=50] -->
+    <string name="draw_size_title">Select Size</string>
+    <!--  The accept the draw size [CHAR LIMIT=20] -->
+    <string name="draw_size_accept">OK</string>
+
+    <!--  Name used to indicate the original image in the state panel [CHAR LIMIT=20] -->
+    <string name="state_panel_original">Original</string>
+
+    <!--  Name used to indicate the final image in the state panel [CHAR LIMIT=20] -->
+    <string name="state_panel_result">Result</string>
+
+    <!-- Label for the notification [CHAR LIMIT=50] -->
+    <string name="filtershow_notification_label">Saving Image</string>
+    <!-- Label for the notification message [CHAR LIMIT=50] -->
+    <string name="filtershow_notification_message">Processing...</string>
+
+    <!-- Label for the save preset menu [CHAR LIMIT=30] -->
+    <string name="filtershow_save_preset">Save current preset</string>
+    <!-- Label for the manage preset menu [CHAR LIMIT=30] -->
+    <string name="filtershow_manage_preset">Manage user presets</string>
+    <!-- Label for newly created user preset [CHAR LIMIT=30] -->
+    <string name="filtershow_new_preset">New Preset</string>
+
+</resources>
diff --git a/res/values/filtershow_styles.xml b/res/values/filtershow_styles.xml
new file mode 100644
index 0000000..4162ccd
--- /dev/null
+++ b/res/values/filtershow_styles.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012 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>
+
+    <style name="FilterShowHistoryButton">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">48dip</item>
+        <item name="android:layout_weight">1</item>
+        <item name="android:background">@drawable/filtershow_button_background</item>
+        <item name="android:gravity">center</item>
+        <item name="android:padding">2dip</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:textSize">18dip</item>
+        <item name="android:textStyle">bold</item>
+    </style>
+
+    <style name="FilterShowTopButton">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:background">@drawable/filtershow_button_background</item>
+        <item name="android:gravity">center</item>
+        <item name="android:padding">8dip</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:textSize">18dip</item>
+        <item name="android:textStyle">bold</item>
+        <item name="android:scaleType">centerInside</item>
+    </style>
+
+    <style name="FilterShowBottomButton">
+        <item name="android:layout_width">96dip</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:background">@drawable/filtershow_button_background</item>
+        <item name="android:gravity">center</item>
+        <item name="android:paddingBottom">16dip</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:textSize">18dip</item>
+        <item name="android:textStyle">bold</item>
+        <item name="android:scaleType">centerInside</item>
+    </style>
+
+    <style name="FilterShowSlider">
+        <item name="android:indeterminateOnly">false</item>
+        <item name="android:progressDrawable">@drawable/filtershow_slider</item>
+        <item name="android:indeterminateDrawable">@drawable/filtershow_slider</item>
+        <item name="android:minHeight">13dip</item>
+        <item name="android:maxHeight">13dip</item>
+        <item name="android:thumb">@drawable/filtershow_scrubber</item>
+        <item name="android:thumbOffset">16dip</item>
+        <item name="android:focusable">true</item>
+        <item name="android:paddingStart">16dip</item>
+        <item name="android:paddingEnd">16dip</item>
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/res/values/filtershow_values.xml b/res/values/filtershow_values.xml
new file mode 100644
index 0000000..0bb59c0
--- /dev/null
+++ b/res/values/filtershow_values.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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>
+    <!-- Specify the screen orientation -->
+    <bool name="only_use_portrait">true</bool>
+
+    <!-- Text size for the state panel -->
+    <dimen name="state_panel_text_size">16dip</dimen>
+
+    <!-- Category Panel Height -->
+    <dimen name="category_panel_height">86dip</dimen>
+
+    <!-- Category Panel Icon Size -->
+    <dimen name="category_panel_icon_size">64dip</dimen>
+
+    <!-- Category Panel Text Size -->
+    <dimen name="category_panel_text_size">13dip</dimen>
+
+    <!-- Category Panel Text Size -->
+    <dimen name="category_panel_margin">4dip</dimen>
+
+    <!-- Grad filter dot size -->
+    <dimen name="gradcontrol_dot_size">20dip</dimen>
+
+    <!-- Grad filter minimum touch distance -->
+    <dimen name="gradcontrol_min_touch_dist">80dip</dimen>
+
+
+</resources>
\ No newline at end of file
diff --git a/res/values/filtershow_values_attrs.xml b/res/values/filtershow_values_attrs.xml
new file mode 100644
index 0000000..32a3a87
--- /dev/null
+++ b/res/values/filtershow_values_attrs.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+    <declare-styleable name="ImageButtonTitle">
+        <attr name="android:text"/>
+        <attr name="android:textColor"/>
+    </declare-styleable>
+    <declare-styleable name="CenteredLinearLayout">
+        <attr name="max_width" format="dimension" />
+    </declare-styleable>
+    <declare-styleable name="StatePanelTrack">
+        <attr name="elemSize" format="dimension" />
+        <attr name="elemEndSize" format="dimension" />
+    </declare-styleable>
+    <declare-styleable name="CategoryTrack">
+        <attr name="iconSize" format="dimension" />
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/res/values/iconbutton_styles.xml b/res/values/iconbutton_styles.xml
new file mode 100644
index 0000000..e33460a
--- /dev/null
+++ b/res/values/iconbutton_styles.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012 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>
+    <style name="IconButton">
+        <item name="android:layout_width">96dp</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:background">@drawable/filtershow_button_background</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:textSize">14dp</item>
+        <item name="android:scaleType">centerInside</item>
+        <item name="android:gravity">center</item>
+        <item name="android:padding">6dp</item>
+        <item name="android:drawablePadding">6dp</item>
+        <item name="android:ellipsize">marquee</item>
+        <item name="android:marqueeRepeatLimit">marquee_forever</item>
+        <item name="android:singleLine">true</item>
+    </style>
+
+    <style name="FilterIconButton">
+        <item name="android:layout_width">70dp</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:background">@drawable/filtershow_button_background</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:textSize">13dp</item>
+        <item name="android:scaleType">centerInside</item>
+        <item name="android:gravity">center</item>
+        <item name="android:paddingLeft">3dp</item>
+        <item name="android:paddingRight">3dp</item>
+        <item name="android:paddingTop">6dp</item>
+        <item name="android:paddingBottom">6dp</item>
+        <item name="android:ellipsize">marquee</item>
+        <item name="android:marqueeRepeatLimit">marquee_forever</item>
+        <item name="android:singleLine">true</item>
+    </style>
+</resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
new file mode 100644
index 0000000..fefd5f0
--- /dev/null
+++ b/res/values/ids.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2010 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="action_toggle_full_caching" />
+    <item type="id" name="action_select_all" />
+    <item type="id" name="viewpager" />
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..f2d69c8
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,1102 @@
+<?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">
+    <string name="app_name">Gallery</string>
+    <!-- Title for picture frame gadget to show in list of all available gadgets -->
+    <string name="gadget_title">Picture frame</string>
+
+    <!-- Used to format short video duration in Details dialog. minutes:seconds e.g. 00:30 -->
+    <string name="details_ms">%1$02d:%2$02d</string>
+    <!-- Used to format video duration in Details dialog. hours:minutes:seconds e.g. 0:21:30 -->
+    <string name="details_hms">%1$d:%2$02d:%3$02d</string>
+    <!-- Activity label. This might show up in the activity-picker -->
+    <string name="movie_view_label">Video player</string>
+    <!-- shown in the video player view while the video is being loaded, before it starts playing -->
+    <string name="loading_video">Loading video\u2026</string>
+    <string name="loading_image">Loading image\u2026</string>
+
+    <!-- Message shown on the progress dialog to indicate we're loading the
+            account info [CHAR LIMIT=30] -->
+    <string name="loading_account">Loading account\u2026</string>
+
+    <!-- Movie View Resume Playing dialog title -->
+    <string name="resume_playing_title">Resume video</string>
+
+    <!-- Movie View Start Playing dialog title -->
+    <string name="resume_playing_message">Resume playing from %s ?</string>
+    <!-- Movie View Start Playing button "Resume from bookmark" -->
+    <string name="resume_playing_resume">Resume playing</string>
+
+    <!-- Displayed in the title of those albums that are being loaded -->
+    <string name="loading">Loading\u2026</string>
+
+    <!-- Displayed in the title of those pictures that fails to be loaded
+         [CHAR LIMIT=50]-->
+    <string name="fail_to_load">Couldn\'t load</string>
+
+    <!-- Used in a toast message when an image fails to be loaded
+         [CHAR LIMIT=50]-->
+    <string name="fail_to_load_image">Couldn\'t load the image</string>
+
+    <!-- Displayed in place of the picture when we fail to get the thumbnail of it.
+         [CHAR LIMIT=50]-->
+    <string name="no_thumbnail">No thumbnail</string>
+
+    <!-- Movie View Start Playing button "Beginning" -->
+    <string name="resume_playing_restart">Start over</string>
+
+    <!-- Title of a menu item to indicate performing the image crop operation
+         [CHAR LIMIT=20] -->
+    <string name="crop_save_text">OK</string>
+    <!-- Title of a menu item to indicate performing the image crop operation
+         [CHAR LIMIT=20] -->
+    <string name="ok">OK</string>
+    <!-- Button indicating that the cropped image should be reverted back to the original -->
+    <!-- Hint that appears when cropping an image with more than one face -->
+    <string name="multiface_crop_help">Touch a face to begin.</string>
+    <!-- Toast/alert that the image is being saved to the SD card -->
+    <string name="saving_image">Saving picture\u2026</string>
+
+    <!-- Toast/alert that the image is being saved after editing in filtershow [CHAR LIMIT=40]-->
+    <string name="filtershow_saving_image">Saving picture to <xliff:g id="album_name">%1$s</xliff:g> \u2026</string>
+
+    <!-- Eorror toast message that the image cannot be saved [CHAR LIMIT=40]-->
+    <string name="save_error">Couldn\'t save cropped image.</string>
+
+    <!-- menu pick: crop the currently selected image [CHAR LIMIT=30]-->
+    <string name="crop_label">Crop picture</string>
+    <!-- menu pick: trim the currently selected video [CHAR LIMIT=30]-->
+    <string name="trim_label">Trim video</string>
+
+    <!-- Toast/alert that the face detection is being run -->
+
+    <!-- Title prompted for user to choose a photo item [CHAR LIMIT=20] -->
+    <string name="select_image">Select photo</string>
+    <!-- Title prompted for user to choose a video item [CHAR LIMIT=20] -->
+    <string name="select_video">Select video</string>
+    <!-- Title prompted for user to choose a media object [CHAR LIMIT=20] -->
+    <string name="select_item">Select item</string>
+    <!-- Title prompted for user to choose an album [CHAR LIMIT=20] -->
+    <string name="select_album">Select album</string>
+    <!-- Title prompted for user to choose a group [CHAR LIMIT=20] -->
+    <string name="select_group">Select group</string>
+
+    <!-- Displayed in the title of the dialog for things to do with a picture
+             that is to be "set as" (e.g. set as contact photo or set as wallpaper) -->
+    <string name="set_image">Set picture as</string>
+    <!-- Activity title for cropping picture and setting it as wallpaper [CHAR LIMIT=20] -->
+    <string name="set_wallpaper">Set wallpaper</string>
+    <!-- Toast/alert after saving wallpaper -->
+    <string name="wallpaper">Setting wallpaper\u2026</string>
+    <string name="camera_setas_wallpaper">Wallpaper</string>
+
+    <!-- Details dialog "OK" button. Dismisses dialog. -->
+    <string name="delete">Delete</string>
+    <!-- String Delete the selected media item(s) [CHAR LIMIT=50] -->
+    <plurals name="delete_selection">
+        <item quantity="one">Delete selected item?</item>
+        <item quantity="other">Delete selected items?</item>
+    </plurals>
+    <string name="confirm">Confirm</string>
+    <string name="cancel">Cancel</string>
+    <string name="share">Share</string>
+    <string name="share_panorama">Share panorama</string>
+    <string name="share_as_photo">Share as photo</string>
+
+    <!-- The label shown after an image is deleted [CHAR LIMIT=16] -->
+    <string name="deleted">Deleted</string>
+
+    <!-- The label on the button which when clicked will undo a deletion of image [CHAR LIMIT=16]-->
+    <string name="undo">UNDO</string>
+
+    <!-- String indicating more actions are available -->
+    <string name="select_all">Select all</string>
+    <string name="deselect_all">Deselect all</string>
+    <string name="slideshow">Slideshow</string>
+
+    <string name="details">Details</string>
+    <string name="details_title">%1$d of %2$d items:</string>
+    <string name="close">Close</string>
+
+    <!-- Title of a menu item to switch from Gallery to Camera app [CHAR LIMIT=30] -->
+    <string name="switch_to_camera">Switch to Camera</string>
+
+    <!-- String indicating how many media item(s) is(are) selected
+            eg. 1 selected [CHAR LIMIT=30] -->
+    <plurals name="number_of_items_selected">
+        <item quantity="zero">%1$d selected</item>
+        <item quantity="one">%1$d selected</item>
+        <item quantity="other">%1$d selected</item>
+    </plurals>
+
+    <!-- String indicating how many media album(s) is(are) selected
+            eg. 1 selected [CHAR LIMIT=30] -->
+    <plurals name="number_of_albums_selected">
+        <item quantity="zero">%1$d selected</item>
+        <item quantity="one">%1$d selected</item>
+        <item quantity="other">%1$d selected</item>
+    </plurals>
+
+    <!-- String indicating how many media group(s) is(are) selected
+            eg. 1 selected [CHAR LIMIT=30] -->
+    <plurals name="number_of_groups_selected">
+        <item quantity="zero">%1$d selected</item>
+        <item quantity="one">%1$d selected</item>
+        <item quantity="other">%1$d selected</item>
+    </plurals>
+
+    <!-- String indicating timestamp of photo or video -->
+    <string name="show_on_map">Show on map</string>
+    <string name="rotate_left">Rotate left</string>
+    <string name="rotate_right">Rotate right</string>
+
+    <!-- Toast message prompted when the specified item is not found [CHAR LIMIT=40]-->
+    <string name="no_such_item">Couldn\'t find item.</string>
+
+    <!-- String used as a menu label. The user can choose to edit the image
+         [CHAR_LIMIT=20]-->
+    <string name="edit">Edit</string>
+
+    <!-- String used as a menu label. The user can choose to edit the image
+         [CHAR_LIMIT=20]-->
+    <string name="simple_edit">Simple Edit</string>
+
+    <!-- String used as a title of a progress dialog. The user can
+         choose to cache some Picasa picture albums on device, so it can
+         be viewed offline. This string is shown when the request is being
+         processed. [CHAR LIMIT=50] -->
+    <string name="process_caching_requests">Processing caching requests</string>
+
+    <!-- String used as a small notification label above a Picasa album.
+         It means the pictures of the Picasa album is currently being
+         transferred to local storage, so the pictures can later be viewed
+         offline. [CHAR LIMIT=15] -->
+    <string name="caching_label">Caching\u2026</string>
+
+    <!-- The title of the menu item to let user crop the image. [CHAR LIMIT=15] -->
+    <string name="crop_action">Crop</string>
+    <!-- The title of the menu item to let user trim the video. [CHAR LIMIT=15] -->
+    <string name="trim_action">Trim</string>
+    <!-- The title of the menu item to let user mute the video. [CHAR LIMIT=15] -->
+    <string name="mute_action">Mute</string>
+    <!-- The title of the menu item to let user set the image as background etc. [CHAR LIMIT=15] -->
+    <string name="set_as">Set as</string>
+
+    <!-- String indicating an error when muting the video. [CHAR LIMIT=30] -->
+    <string name="video_mute_err">Can\'t mute video.</string>
+    <!-- String indicating an error when playing the video. [CHAR LIMIT=30] -->
+    <string name="video_err">Can\'t play video.</string>
+
+    <!-- Strings for grouping operations in the menu. The photos can be grouped
+         by their location, taken time, or tags. -->
+    <!-- The title of the menu item to let user choose the grouping rule, when
+         pressed, a submenu will shown and user can choose one grouping rule
+         from the submenu. -->
+
+    <!-- Title of a menu item to group photo by location [CHAR LIMIT=30] -->
+    <string name="group_by_location">By location</string>
+
+    <!-- Title of a menu tiem to group photo by taken date [CHAR LIMIT=30]-->
+    <string name="group_by_time">By time</string>
+
+    <!-- Title of a menu item to group photo by tags [CHAR LIMIT=30]-->
+    <string name="group_by_tags">By tags</string>
+
+    <!-- Title of a menu item to group photo by faces [CHAR LIMIT=30]-->
+    <string name="group_by_faces">By people</string>
+
+    <!-- Title of a menu item to group photo by albums [CHAR LIMIT=30]-->
+    <string name="group_by_album">By album</string>
+
+    <!-- Title of a menu item to group photo by size [CHAR LIMIT=30]-->
+    <string name="group_by_size">By size</string>
+
+    <!-- When grouping photos by tags, the label used for photos without tags
+         [CHAR LIMIT=20]-->
+    <string name="untagged">Untagged</string>
+
+    <!-- When grouping photos by locations, the label used for photos that don't
+         have location information in them [CHAR LIMIT=20]-->
+    <string name="no_location">No location</string>
+
+    <!-- This toast message is shown when network connection is lost while doing clustering -->
+    <string name="no_connectivity">Some locations couldn\'t be identified due to network problems.</string>
+
+    <!-- This toast message is shown when failed to load the album data. [CHAR LIMIT=NONE] -->
+    <string name="sync_album_error">Couldn\'t download the photos in this album. Retry later.</string>
+
+    <!-- The title of the menu item to let user choose the which portion of
+         the media items the user wants to see. When pressed, a submenu will
+         appear and user can choose one of "show images only",
+         "show videos only", or "show all" from the submenu. -->
+
+    <!-- Title of a menu item to show images only [CHAR LIMIT=30]-->
+    <string name="show_images_only">Images only</string>
+
+    <!-- Title of a menu item to show videos only [CHAR LIMIT=30]-->
+    <string name="show_videos_only">Videos only</string>
+
+    <!-- Title of a menu item to show all (both images and videos) [CHAR LIMIT=30]-->
+    <string name="show_all">Images &amp; videos</string>
+
+    <!-- Title of the StackView AppWidget -->
+    <string name="appwidget_title">Photo Gallery</string>
+
+    <!-- Text for the empty state of the StackView AppWidget [CHAR LIMIT=30] -->
+    <string name="appwidget_empty_text">No photos.</string>
+
+    <!-- Toast message shown when the cropped image has been saved in the
+         %s folder (string: folder_download) [CHAR LIMIT=50]-->
+    <string name="crop_saved">
+        Cropped image saved to <xliff:g id="folder_name">%s</xliff:g>.</string>
+
+    <!-- Toast message shown when there is no albums available [CHAR LIMIT=50]-->
+    <string name="no_albums_alert">No albums available.</string>
+
+    <!-- Toast message shown when we close the AlbumPage because it is empty
+            [CHAR LIMIT=50] -->
+    <string name="empty_album">O images/videos available.</string>
+
+    <!-- Album label used to indicate the collection of PWA Buzz/Post photos -->
+    <string name="picasa_posts">Posts</string>
+
+    <!-- A label describing that the current screen is for the user to pick
+         some albums to be viewable offline [CHAR LIMIT=30] -->
+    <string name="make_available_offline">Make available offline</string>
+
+    <!-- A label of a menu item for user to sync the content [CHAR LIMIT=30] -->
+    <string name="sync_picasa_albums">Refresh</string>
+
+    <!-- A label on a button. The user clicks this button after he has
+         finished selection. [CHAR LIMIT=15] -->
+    <string name="done">Done</string>
+
+    <!-- String indicating the sequence of currently selected item in the
+            media set eg. 3 of 5 items [CHAR LIMIT=30] -->
+    <string name="sequence_in_set">%1$d of %2$d items:</string>
+    <!-- Text indicating the title of a media item in details window [CHAR LIMIT=14] -->
+    <string name="title">Title</string>
+    <!-- Text indicating the description of a media item in details window [CHAR LIMIT=14] -->
+    <string name="description">Description</string>
+    <!-- Text indicating the creation time of a media item in details window [CHAR LIMIT=14] -->
+    <string name="time">Time</string>
+    <!-- Text indicating the location of a media item in details window [CHAR LIMIT=14] -->
+    <string name="location">Location</string>
+    <!-- Text indicating the path of a media item in details window [CHAR LIMIT=14] -->
+    <string name="path">Path</string>
+    <!-- Text indicating the width of a media item in details window [CHAR LIMIT=14] -->
+    <string name="width">Width</string>
+    <!-- Text indicating the height of a media item in details window [CHAR LIMIT=14] -->
+    <string name="height">Height</string>
+    <!-- Text indicating the orientation of a media item in details window [CHAR LIMIT=14] -->
+    <string name="orientation">Orientation</string>
+    <!-- Text indicating the duration of a video item in details window [CHAR LIMIT=14] -->
+    <string name="duration">Duration</string>
+    <!-- Text indicating the mime type of a media item in details window [CHAR LIMIT=14] -->
+    <string name="mimetype">MIME type</string>
+    <!-- Text indicating the file size of a media item in details window [CHAR LIMIT=14] -->
+    <string name="file_size">File size</string>
+    <!-- Text indicating the maker of a media item in details window [CHAR LIMIT=14] -->
+    <string name="maker">Maker</string>
+    <!-- Text indicating the model of a media item in details window [CHAR LIMIT=14] -->
+    <string name="model">Model</string>
+    <!-- Text indicating flash info of a media item in details window [CHAR LIMIT=14] -->
+    <string name="flash">Flash</string>
+    <!-- Text indicating aperture of a media item in details window [CHAR LIMIT=14] -->
+    <string name="aperture">Aperture</string>
+    <!-- Text indicating the focal length of a media item in details window [CHAR LIMIT=14] -->
+    <string name="focal_length">Focal Length</string>
+    <!-- Text indicating the white balance of a media item in details window [CHAR LIMIT=14] -->
+    <string name="white_balance">White balance</string>
+    <!-- Text indicating the exposure time of a media item in details window [CHAR LIMIT=14] -->
+    <string name="exposure_time">Exposure time</string>
+    <!-- Text indicating the ISO speed rating of a media item in details window [CHAR LIMIT=14] -->
+    <string name="iso">ISO</string>
+    <!-- String indicating the time units in seconds. [CHAR LIMIT=8] -->
+    <!-- String indicating the length units in milli-meters. [CHAR LIMIT=8] -->
+    <string name="unit_mm">mm</string>
+    <!-- String indicating how camera shooting feature is used. [CHAR LIMIT=8] -->
+    <string name="manual">Manual</string>
+    <!-- String indicating how camera shooting feature is used. [CHAR LIMIT=8] -->
+    <string name="auto">Auto</string>
+    <!-- String indicating camera flash is fired. [CHAR LIMIT=14] -->
+    <string name="flash_on">Flash fired</string>
+    <!-- String indicating camera flash is not used. [CHAR LIMIT=14] -->
+    <string name="flash_off">No flash</string>
+
+    <!-- String indicating image width or height is unknown. [CHAR LIMIT=14] -->
+    <string name="unknown">Unknown</string>
+
+    <!-- String for the empty not filtered image [CHAR LIMIT=10] -->
+    <string name="ffx_original">Original</string>
+    <!-- String for brown-colored old-fashion looking filter (filtershow_fx_0000_vintage) [CHAR LIMIT=10] -->
+    <string name="ffx_vintage">Vintage</string>
+    <!-- String for filter that brightens colors (filtershow_fx_0001_instant) [CHAR LIMIT=10] -->
+    <string name="ffx_instant">Instant</string>
+    <!-- String for filter that washes out colors (filtershow_fx_0002_bleach) [CHAR LIMIT=10] -->
+    <string name="ffx_bleach">Bleach</string>
+    <!-- String for filter that makes colors a bluish (filtershow_fx_0003_blue_crush) [CHAR LIMIT=10] -->
+    <string name="ffx_blue_crush">Blue</string>
+    <!-- String for filter that makes image black & white (filtershow_fx_0004_bw_contrast) [CHAR LIMIT=10] -->
+    <string name="ffx_bw_contrast">B/W</string>
+    <!-- String for filter that makes colors a yellowish (filtershow_fx_0005_punch) [CHAR LIMIT=10] -->
+    <string name="ffx_punch">Punch</string>
+    <!-- String for filter that mimics the cross-process technique in
+         photography (makes colors bluish) (filtershow_fx_0006_x_process) [CHAR LIMIT=10] -->
+    <string name="ffx_x_process">X Process</string>
+    <!-- String for filter that makes image coffee-colored (filtershow_fx_0007_washout) [CHAR LIMIT=10] -->
+    <string name="ffx_washout">Latte</string>
+    <!-- String for filter that makes colors washed out and brownish
+         (filtershow_fx_0008_washout_color) [CHAR LIMIT=10] -->
+    <string name="ffx_washout_color">Litho</string>
+
+    <!-- Toast message shown after we make some album(s) available offline [CHAR LIMIT=50] -->
+    <plurals name="make_albums_available_offline">
+        <item quantity="one">Making album available offline.</item>
+        <item quantity="other">Making albums available offline.</item>
+    </plurals>
+
+    <!-- Toast message shown after we try to make a local album available offline
+         [CHAR LIMIT=150] -->
+    <string name="try_to_set_local_album_available_offline">
+        This item is stored locally and available offline.</string>
+
+    <!-- A label shown on the action bar. It indicates that the user is
+         viewing all available albums [CHAR LIMIT=20] -->
+    <string name="set_label_all_albums">All albums</string>
+
+    <!-- A label shown on the action bar. It indicates that the user is
+         viewing albums stored locally on the device [CHAR LIMIT=20] -->
+    <string name="set_label_local_albums">Local albums</string>
+
+    <!-- A label shown on the action bar. It indicates that the user is
+         viewing MTP devices connected (like other digital cameras).
+         [CHAR LIMIT=20] -->
+    <string name="set_label_mtp_devices">MTP devices</string>
+
+    <!-- A label shown on the action bar. It indicates that the user is
+         viewing Picasa albums [CHAR LIMIT=20] -->
+    <string name="set_label_picasa_albums">Picasa albums</string>
+
+    <!-- Label indicating the amount on free space on the device. The parameter
+         is a string representation of the amount of free space, eg. "20MB".
+         [CHAR LIMIT=20]
+    -->
+    <string name="free_space_format"><xliff:g id="bytes">%s</xliff:g> free</string>
+
+    <!-- Label of a group of pictures. The size of each picture in this group is
+         less than a certain amount. The parameter is a string representation
+         of that amount, eg. "10MB".
+         [CHAR LIMIT=20]
+    -->
+    <string name="size_below"><xliff:g id="size">%1$s</xliff:g> or below</string>
+
+    <!-- Label of a group of pictures. The size of each picture in this group is
+         more than a certain amount. The parameter is a string representation
+         of that amount, eg. "10MB".
+         [CHAR LIMIT=20]
+    -->
+    <string name="size_above"><xliff:g id="size">%1$s</xliff:g> or above</string>
+
+    <!-- Label of a group of pictures. The size of each picture in this group is
+         between two amounts. The parameters are string representations of the two
+         amounts, eg. "10MB", "100MB".
+         [CHAR LIMIT=20]
+    -->
+    <string name="size_between"><xliff:g id="min_size">%1$s</xliff:g> to <xliff:g id="max_size">%2$s</xliff:g></string>
+
+    <!-- A label shown on the action bar. It indicates that the operation
+         to import media item(s) [CHAR LIMIT=20] -->
+    <string name="Import">Import</string>
+
+    <!-- A label shown on the action bar. It indicates whether the import
+         operation succeeds or fails. [CHAR LIMIT=20] -->
+    <string name="import_complete">Import complete</string>
+    <string name="import_fail">Import unsuccessful</string>
+
+    <!-- A toast indicating a camera is connected to the device [CHAR LIMIT=30]-->
+    <string name="camera_connected">Camera connected.</string>
+    <!-- A toast indicating a camera is disconnected [CHAR LIMIT=30] -->
+    <string name="camera_disconnected">Camera disconnected.</string>
+    <!-- A label shown on MTP albums thumbnail to instruct users to import
+        [CHAR LIMIT=40] -->
+    <string name="click_import">Touch here to import</string>
+
+    <!-- The label on the radio button for the widget type that shows the images randomly. [CHAR LIMIT=30]-->
+    <string name="widget_type_album">Choose an album</string>
+    <!-- The label on the radio button for the widget type that shows the images in an album. [CHAR LIMIT=30]-->
+    <string name="widget_type_shuffle">Shuffle all images</string>
+    <!-- The label on the radio button for the widget type that shows only one image. [CHAR LIMIT=30]-->
+    <string name="widget_type_photo">Choose an image</string>
+
+    <!-- The title of the dialog for choosing the type of widget. [CHAR LIMIT=20] -->
+    <string name="widget_type">Choose images</string>
+
+    <!-- Title of the Android Dreams slideshow screensaver. [CHAR LIMIT=20] -->
+    <string name="slideshow_dream_name">Slideshow</string>
+
+    <!-- Group by Albums tab on Action Bar. [CHAR LIMIT=12] -->
+    <string name="albums">Albums</string>
+
+    <!-- Group by Times tab on Action Bar. [CHAR LIMIT=12] -->
+    <string name="times">Times</string>
+
+    <!-- Group by Locations tab on Action Bar. [CHAR LIMIT=12] -->
+    <string name="locations">Locations</string>
+
+    <!-- Group by People tab on Action Bar. [CHAR LIMIT=12] -->
+    <string name="people">People</string>
+
+    <!-- Group by Tags tab on Action Bar. [CHAR LIMIT=12] -->
+    <string name="tags">Tags</string>
+
+    <!-- Group by menu item. [CHAR LIMIT=20] -->
+    <string name="group_by">Group by</string>
+
+    <!-- The title of the menu item which enable the settings [CHAR LIMIT=20] -->
+    <string name="settings">Settings</string>
+
+    <!-- The title of menu item where user can add a new account -->
+    <string name="add_account">Add account</string>
+
+    <!-- The label for the folder contains pictures taken by the camera. [CHAR LIMIT=20]-->
+    <string name="folder_camera">Camera</string>
+
+    <!-- The label for the folder contains downloaded pictures. [CHAR LIMIT=20]-->
+    <string name="folder_download">Download</string>
+
+    <!-- The label for the folder contains edited online pictures. [CHAR LIMIT=40]-->
+    <string name="folder_edited_online_photos">Edited Online Photos</string>
+
+    <!-- The label for the folder contains pictures that was imported from an
+         external camera. [CHAR LIMIT=20]-->
+    <string name="folder_imported">Imported</string>
+
+    <!-- The label for the folder contains screenshot images. [CHAR LIMIT=20]-->
+    <string name="folder_screenshot">Screenshot</string>
+
+    <!-- The title of the menu item which display online help in browser. [CHAR LIMIT=20]-->
+    <string name="help">Help</string>
+
+    <!-- The tilte of a dialog showing there is no external storage. [CHAR LIMIT=20] -->
+    <string name="no_external_storage_title">No Storage</string>
+
+    <!-- The message of a dialog showing there is no external storage. [CHAR LIMIT=none] -->
+    <string name="no_external_storage">No external storage available</string>
+
+    <!-- Label for album filmstrip button -->
+    <string name="switch_photo_filmstrip">Filmstrip view</string>
+
+    <!-- Label for album grid button -->
+    <string name="switch_photo_grid">Grid view</string>
+
+    <!-- Label for fullscreen button. [CHAR LIMIT=20] -->
+    <string name="switch_photo_fullscreen">Fullscreen view</string>
+
+    <!-- The tilte of a dialog showing trimming in progress. [CHAR LIMIT=20] -->
+    <string name="trimming">Trimming</string>
+
+    <!-- The tilte of a dialog showing muting in progress. [CHAR LIMIT=20] -->
+    <string name="muting">Muting</string>
+
+    <!-- The content of a dialog showing trimming in progress. [CHAR LIMIT=30] -->
+    <string name="please_wait">Please wait</string>
+
+    <!-- Toast after the trimming / muting is done. [CHAR LIMIT=50] -->
+    <string name="save_into">Saving video to <xliff:g id="album_name">%1$s</xliff:g> \u2026</string>
+
+    <!-- Toast if the trimmed video is too short to trim. [CHAR LIMIT=80] -->
+    <string name="trim_too_short">Can not trim : target video is too short</string>
+
+    <!-- Text to show with progress bar while stitching in Gallery -->
+    <string name="pano_progress_text">Rendering panorama</string>
+
+    <!-- The label on the button that will save an edited image -->
+    <string name="save" msgid="8140440041190264400">Save</string>
+
+    <!--  Text of notification message which is shown when user attaches camera -->
+    <string name="ingest_scanning" msgid="2048262851775139720">Scanning content...</string>
+
+    <!-- String indicating how many media items from the camera have been scanned -->
+    <plurals name="ingest_number_of_items_scanned">
+        <item quantity="zero">%1$d items scanned</item>
+        <item quantity="one">%1$d item scanned</item>
+        <item quantity="other">%1$d items scanned</item>
+    </plurals>
+
+    <!--  Status message shown when content from the camera is being sorted -->
+    <string name="ingest_sorting" msgid="624687230903648118">Sorting...</string>
+
+    <!--  Status message shown when scanning the content from the camera has completed -->
+    <string name="ingest_scanning_done">Scanning done</string>
+
+    <!--  Status message shown when content from an external camera is being imported -->
+    <string name="ingest_importing">Importing...</string>
+
+    <!--  Status message shown when there is no content available to be imported -->
+    <string name="ingest_empty_device">There is no content available for importing on this device.</string>
+
+    <!--  Status message shown when there is no MTP device connected  -->
+    <string name="ingest_no_device">There is no MTP device connected</string>
+
+    <!-- Camera resources below -->
+
+    <!-- General strings -->
+
+    <!-- title for the dialog showing the error of camera hardware -->
+    <string name="camera_error_title">Camera error</string>
+
+    <!-- message for the dialog showing the error of camera hardware -->
+    <string name="cannot_connect_camera">Can\'t connect to the camera.</string>
+
+    <!-- message for the dialog showing the camera is disabled because of security policies. Camera cannot be used. -->
+    <string name="camera_disabled">Camera has been disabled because of security policies.</string>
+
+    <!-- label for the icon meaning 'show me all the images that were taken with the camera' -->
+    <string name="camera_label">Camera</string>
+
+    <!-- label for the 'video recording application shown in the top level 'all applications' -->
+    <string name="video_camera_label">Camcorder</string>
+
+    <!-- alert to the user to wait for some operation to complete -->
+    <string name="wait">Please wait\u2026</string>
+
+    <!-- alert to the user that USB storage must be available before using the camera [CHAR LIMIT=NONE] -->
+    <string name="no_storage" product="nosdcard">Mount USB storage before using the camera.</string>
+    <!-- alert to the user that an SD card must be installed before using the camera -->
+    <string name="no_storage" product="default">Insert an SD card before using the camera.</string>
+
+    <!-- alert to the user that the USB storage is being disk-checked [CHAR LIMIT=30] -->
+    <string name="preparing_sd" product="nosdcard">Preparing USB storage\u2026</string>
+    <!-- alert to the user that the SD card is being disk-checked -->
+    <string name="preparing_sd" product="default">Preparing SD card\u2026</string>
+
+    <!-- alert to the user that the camera fails to read or write the USB storage. [CHAR LIMIT=NONE] -->
+    <string name="access_sd_fail" product="nosdcard">Couldn\'t access USB storage.</string>
+    <!-- alert to the user that the camera fails to read or write the SD card. -->
+    <string name="access_sd_fail" product="default">Couldn\'t access SD card.</string>
+
+    <!-- button in review mode indicating that the photo taking, video recording, and panorama saving session should be canceled [CHAR LIMIT=10] -->
+    <string name="review_cancel">CANCEL</string>
+
+    <!-- button in review mode indicating that the taken photo/video is OK to be attached/uploaded [CHAR LIMIT=10] -->
+    <string name="review_ok">DONE</string>
+
+    <!-- A label that overlays on top of the preview frame to indicate the camcorder is in time lapse mode [CHAR LIMIT=35] -->
+    <string name="time_lapse_title">Time lapse recording</string>
+
+    <!-- Settings screen, camera selection dialog title. Users can select a camera from the phone (front-facing or back-facing). [CHAR LIMIT=20] -->
+    <string name="pref_camera_id_title">Choose camera</string>
+
+    <string name="pref_camera_id_default" translatable="false">0</string>
+
+    <!-- In select camera setting, back facing camera. [CHAR LIMIT=14] -->
+    <string name="pref_camera_id_entry_back">Back</string>
+    <!-- In select camera setting, front-facing camera. [CHAR LIMIT=14] -->
+    <string name="pref_camera_id_entry_front">Front</string>
+
+    <!-- Settings screen, setting title text -->
+    <string name="pref_camera_recordlocation_title">Store location</string>
+
+    <string name="pref_camera_recordlocation_default" translatable="false">none</string>
+
+    <!--  Label for record location preference [CHAR LIMIT=50] -->
+    <string name="pref_camera_location_label">LOCATION</string>
+
+    <!-- Title for countdown timer on camera settings screen [CHAR LIMIT=30]-->
+    <string name="pref_camera_timer_title">Countdown timer</string>
+
+    <string name="pref_camera_timer_default" translatable="false">0</string>
+    <!-- Entry for countdown timer setting. e.g. 1 second, 10 seconds, etc. [CHAR LIMIT=30]-->
+    <plurals name="pref_camera_timer_entry">
+        <item quantity="one">1 second</item>
+        <item quantity="other">%d seconds</item>
+    </plurals>
+    <string name="pref_camera_timer_sound_default">@string/setting_on_value</string>
+    <!-- Text followed by a checkbox to turn on/off sound effects during the countdown. [CHAR LIMIT = 16]-->
+    <string name="pref_camera_timer_sound_title">Beep during countdown</string>
+
+    <!-- Entry of a on/off setting. The setting is turned off. [CHAR LIMIT=15] -->
+    <string name="setting_off">Off</string>
+    <!-- Entry of a on/off setting. The setting is turned on. [CHAR LIMIT=15] -->
+    <string name="setting_on">On</string>
+
+    <!-- The value of a camera preference indicating the setting is off. -->
+    <string name="setting_off_value" translatable="false">off</string>
+    <!-- The value of a camera preference indicating the setting is on. -->
+    <string name="setting_on_value" translatable="false">on</string>
+
+    <!-- The Video quality settings in preference [CHAR LIMIT=21] -->
+    <string name="pref_video_quality_title">Video quality</string>
+    <!-- The default quality value is 5 (720p) -->
+    <string name="pref_video_quality_default" translatable="false">5</string>
+    <!-- Video quality setting entry. Videos will be recorded in 1080p quality. [CHAR LIMIT=24] -->
+    <string name="pref_video_quality_entry_1080p" translatable="false">HD 1080p</string>
+    <!-- Video quality setting entry. Videos will be recorded in 720p quality. [CHAR LIMIT=24] -->
+    <string name="pref_video_quality_entry_720p" translatable="false">HD 720p</string>
+    <!-- Video quality setting entry. Videos will be recorded in 480p quality. [CHAR LIMIT=24] -->
+    <string name="pref_video_quality_entry_480p" translatable="false">SD 480p</string>
+    <!-- Video quality setting entry. Videos will be recorded in the highest quality available on the device. [CHAR LIMIT=24] -->
+    <string name="pref_video_quality_entry_high">High</string>
+    <!-- Video quality setting entry. Videos will be recorded in the lowest quality available on the device. [CHAR LIMIT=24] -->
+    <string name="pref_video_quality_entry_low">Low</string>
+
+    <!-- Describes the preference dialog for choosing interval between frame capture for
+    time lapse recording. Appears at top of the dialog. [CHAR LIMIT=30] -->
+    <string name="pref_video_time_lapse_frame_interval_title">Time lapse</string>
+    <string name="pref_video_time_lapse_frame_interval_default" translatable="false">0</string>
+
+    <!-- Settings screen, Camera setting category title -->
+    <string name="pref_camera_settings_category">Camera settings</string>
+
+    <!-- Settings screen, Camcorder setting category title -->
+    <string name="pref_camcorder_settings_category">Camcorder settings</string>
+
+    <!-- Settings screen, Picture size title -->
+    <string name="pref_camera_picturesize_title">Picture size</string>
+
+    <!-- Settings screen, dialog choice for 13 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_13mp">13M pixels</string>
+    <!-- Settings screen, dialog choice for 8 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_8mp">8M pixels</string>
+    <!-- Settings screen, dialog choice for 5 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_5mp">5M pixels</string>
+    <!-- Settings screen, dialog choice for 4 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_4mp">4M pixels</string>
+    <!-- Settings screen, dialog choice for 3 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_3mp">3M pixels</string>
+    <!-- Settings screen, dialog choice for 2 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_2mp">2M pixels</string>
+    <!-- Settings screen, dialog choice for 2 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_2mp_wide">2M pixels (16:9)</string>
+    <!-- Settings screen, dialog choice for 1.3 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_1_3mp">1.3M pixels</string>
+    <!-- Settings screen, dialog choice for 1 megapixels picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_1mp">1M pixels</string>
+    <!-- Settings screen, dialog choice for VGA picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_vga">VGA</string>
+    <!-- Settings screen, dialog choice for QVGA picture size [CHAR LIMIT=15] -->
+    <string name="pref_camera_picturesize_entry_qvga">QVGA</string>
+
+    <!-- Settings screen, Focus mode title -->
+    <string name="pref_camera_focusmode_title">Focus mode</string>
+
+    <!-- Settings screen, Focus mode dialog radio button choices -->
+    <string name="pref_camera_focusmode_entry_auto">Auto</string>
+    <string name="pref_camera_focusmode_entry_infinity">Infinity</string>
+    <string name="pref_camera_focusmode_entry_macro">Macro</string>
+
+    <!-- Menu, focus mode labels [CHAR LIMIT=50] -->
+    <string name="pref_camera_focusmode_label_auto">AUTO</string>
+    <string name="pref_camera_focusmode_label_infinity">INFINITY</string>
+    <string name="pref_camera_focusmode_label_macro">MACRO</string>
+
+    <!-- Default flash mode setting.-->
+    <string name="pref_camera_flashmode_default" translatable="false">auto</string>
+
+    <!-- Value for flash off setting-->
+    <string name="pref_camera_flashmode_no_flash" translatable="false">no_flash</string>
+
+    <!-- Settings screen, Flash mode title -->
+    <string name="pref_camera_flashmode_title">Flash mode</string>
+        <!-- flash label [CHAR LIMIT=50] -->
+    <string name="pref_camera_flashmode_label">FLASH MODE</string>
+
+    <!-- Settings screen, Flash mode dialog radio button choices -->
+    <string name="pref_camera_flashmode_entry_auto">Auto</string>
+    <string name="pref_camera_flashmode_entry_on">On</string>
+    <string name="pref_camera_flashmode_entry_off">Off</string>
+
+    <!-- Menu, flash mode labels [CHAR LIMIT=50] -->
+    <string name="pref_camera_flashmode_label_auto">FLASH AUTO</string>
+    <string name="pref_camera_flashmode_label_on">FLASH ON</string>
+    <string name="pref_camera_flashmode_label_off">FLASH OFF</string>
+
+    <!-- Default videocamera flash mode setting.-->
+    <string name="pref_camera_video_flashmode_default" translatable="false">off</string>
+
+    <!-- Default white balance setting. -->
+    <string name="pref_camera_whitebalance_default" translatable="false">auto</string>
+
+    <!-- Settings screen, white balance title -->
+    <string name="pref_camera_whitebalance_title">White balance</string>
+    <!-- Menu, white balance label -->
+    <string name="pref_camera_whitebalance_label">WHITE BALANCE</string>
+
+    <!-- Settings screen, White balance dialog radio button choices -->
+    <string name="pref_camera_whitebalance_entry_auto">Auto</string>
+    <string name="pref_camera_whitebalance_entry_incandescent">Incandescent</string>
+    <string name="pref_camera_whitebalance_entry_daylight">Daylight</string>
+    <string name="pref_camera_whitebalance_entry_fluorescent">Fluorescent</string>
+    <string name="pref_camera_whitebalance_entry_cloudy">Cloudy</string>
+
+    <!-- Menu, White balance labels [CHAR LIMIT=50] -->
+    <string name="pref_camera_whitebalance_label_auto">AUTO</string>
+    <string name="pref_camera_whitebalance_label_incandescent">INCANDESCENT</string>
+    <string name="pref_camera_whitebalance_label_daylight">DAYLIGHT</string>
+    <string name="pref_camera_whitebalance_label_fluorescent">FLUORESCENT</string>
+    <string name="pref_camera_whitebalance_label_cloudy">CLOUDY</string>
+
+    <!-- Default scene mode setting. -->
+    <string name="pref_camera_scenemode_default" translatable="false">auto</string>
+
+    <!-- Settings screen, Select Scene mode -->
+    <string name="pref_camera_scenemode_title">Scene mode</string>
+
+    <!-- Settings menu, scene mode choices [CHAR LIMIT=16] -->
+    <string name="pref_camera_scenemode_entry_auto">Auto</string>
+    <!-- Scene mode that uses HDR (high dynamic range) [CHAR LIMIT=16] -->
+    <string name="pref_camera_scenemode_entry_hdr">HDR</string>
+    <!-- Scene mode that takes an image quickly with little motion blur. [CHAR LIMIT=16] -->
+    <string name="pref_camera_scenemode_entry_action">Action</string>
+    <!-- Scene mode that takes long exposures to capture night scenes without flash. [CHAR LIMIT=16] -->
+    <string name="pref_camera_scenemode_entry_night">Night</string>
+    <!-- Scene mode optimized for taking images in the sunset. [CHAR LIMIT=16] -->
+    <string name="pref_camera_scenemode_entry_sunset">Sunset</string>
+    <!-- Scene mode optimized for taking indoor low-lights pictures. [CHAR LIMIT=16] -->
+    <string name="pref_camera_scenemode_entry_party">Party</string>
+
+    <!-- Settings menu, scene mode labels [CHAR LIMIT=50] -->
+    <string name="pref_camera_scenemode_label_auto">NONE</string>
+    <!-- Scene mode that takes an image quickly with little motion blur. [CHAR LIMIT=50] -->
+    <string name="pref_camera_scenemode_label_action">ACTION</string>
+    <!-- Scene mode that takes long exposures to capture night scenes without flash. [CHAR LIMIT=50] -->
+    <string name="pref_camera_scenemode_label_night">NIGHT</string>
+    <!-- Scene mode optimized for taking images in the sunset. [CHAR LIMIT=50] -->
+    <string name="pref_camera_scenemode_label_sunset">SUNSET</string>
+    <!-- Scene mode optimized for taking indoor low-lights pictures. [CHAR LIMIT=50] -->
+    <string name="pref_camera_scenemode_label_party">PARTY</string>
+
+    <!-- Settings menu countdown timer labels [CHAR LIMIT=50] -->
+    <string name="pref_camera_countdown_label">COUNTDOWN TIMER</string>
+    <!-- Settings menu countdown timer off [CHAR LIMIT=50] -->
+    <string name="pref_camera_countdown_label_off">TIMER OFF</string>
+    <!-- Settings menu countdown timer 1 second [CHAR LIMIT=50] -->
+    <string name="pref_camera_countdown_label_one">1 SECOND</string>
+    <!-- Settings menu countdown timer 3 seconds [CHAR LIMIT=50] -->
+    <string name="pref_camera_countdown_label_three">3 SECONDS</string>
+    <!-- Settings menu countdown timer 10 seconds [CHAR LIMIT=50] -->
+    <string name="pref_camera_countdown_label_ten">10 SECONDS</string>
+    <!-- Settings menu countdown timer 15 seconds [CHAR LIMIT=50] -->
+    <string name="pref_camera_countdown_label_fifteen">15 SECONDS</string>
+
+    <!-- Toast after trying to select a setting that is not allowed to change in scene mode [CHAR LIMIT=NONE] -->
+    <string name="not_selectable_in_scene_mode">Not selectable in scene mode.</string>
+
+    <!-- Exposure settings in preference -->
+    <string name="pref_exposure_title">Exposure</string>
+    <string name="pref_exposure_default" translatable="false">0</string>
+    <!--  menu label exposure compensation [CHAR LIMIT=50] -->
+    <string name="pref_exposure_label">EXPOSURE</string>
+
+    <!-- Default HDR entry value -->
+    <string name="pref_camera_hdr_default">@string/setting_off_value</string>
+
+    <!-- HDR label ON [CHAR LIMIT=60] -->
+    <string name="pref_camera_hdr_label">HDR</string>
+
+    <!-- switch camera label back [CHAR LIMIT=60] -->
+    <string name="pref_camera_id_label_back">FRONT CAMERA</string>
+    <!-- switch camera label front [CHAR LIMIT=60] -->
+    <string name="pref_camera_id_label_front">BACK CAMERA</string>
+
+    <!-- Dialog "OK" button. Dismisses dialog. -->
+    <string name="dialog_ok">OK</string>
+
+    <!-- Low-memory dialog message [CHAR LIMT=NONE] -->
+    <string name="spaceIsLow_content" product="nosdcard">Your USB storage is running out of space. Change the quality setting or delete some images or other files.</string>
+    <!-- Low-memory dialog message [CHAR LIMIT=NONE] -->
+    <string name="spaceIsLow_content" product="default">Your SD card is running out of space. Change the quality setting or delete some images or other files.</string>
+
+    <!-- Camera format string for new image files. Passed to java.text.SimpleDateFormat. -->
+    <string name="image_file_name_format" translatable="false">"'IMG'_yyyyMMdd_HHmmss"</string>
+
+    <!-- Video Camera format string for new video files. Passed to java.text.SimpleDateFormat. -->
+    <string name="video_file_name_format" translatable="false">"'VID'_yyyyMMdd_HHmmss"</string>
+
+    <!-- Filename prefix for panorama output. -->
+    <string name="pano_file_name_format" translatable="false">"'PANO'_yyyyMMdd_HHmmss"</string>
+
+    <!-- The message shown when video record reaches size limit. -->
+    <string name="video_reach_size_limit">Size limit reached.</string>
+
+    <!-- The text shown when the panorama panning speed is to fast [CHAR LIMIT=12] -->
+    <string name="pano_too_fast_prompt">Too fast</string>
+
+    <!-- The text shown in the progress dialog when panorama preview is generating in the background [CHAR LIMIT=30] -->
+    <string name="pano_dialog_prepare_preview">Preparing panorama</string>
+
+    <!-- The text shown in the dialog when panorama saving failed [CHAR LIMIT=40] -->
+    <string name="pano_dialog_panorama_failed">Couldn\'t save panorama.</string>
+
+    <!-- The text shown on the dialog title in the dialogs for Panorama [CHAR LIMIT=12] -->
+    <string name="pano_dialog_title">Panorama</string>
+
+    <!-- The text shown on the top-left corner of the screen to indicate the capturing is on going [CHAR LIMIT=27] -->
+    <string name="pano_capture_indication">Capturing panorama</string>
+
+    <!-- The text shown in the progress dialog when waiting for previous panorama finishing [CHAR LIMIT=40] -->
+    <string name="pano_dialog_waiting_previous">Waiting for previous panorama</string>
+
+    <!-- The text shown on the bottom-left corner of the screen to indicate that the saving is in process [CHAR LIMIT=13] -->
+    <string name="pano_review_saving_indication_str">Saving\u2026</string>
+
+    <!-- The text shown on the screen to indicate that the panorama is rendering [CHAR LIMIT=27] -->
+    <string name="pano_review_rendering">Rendering panorama</string>
+
+    <!-- Toast telling users tapping on the viewfinder will trigger autofocus [CHAR LIMIT=28] -->
+    <string name="tap_to_focus">Touch to focus.</string>
+
+    <!-- Default effect setting that clears the effect. -->
+    <string name="pref_video_effect_default" translatable="false">none</string>
+
+    <!-- Title of video effect setting popup window -->
+    <string name="pref_video_effect_title">Effects</string>
+
+    <!-- Effect setting item that clear the effect. [CHAR LIMIT=14] -->
+    <string name="effect_none">None</string>
+    <!-- Effect setting item that squeezes the face. [CHAR LIMIT=14] -->
+    <string name="effect_goofy_face_squeeze">Squeeze</string>
+    <!-- Effect setting item that makes eyes big. [CHAR LIMIT=14] -->
+    <string name="effect_goofy_face_big_eyes">Big eyes</string>
+    <!-- Effect setting item that makes mouth big. [CHAR LIMIT=14] -->
+    <string name="effect_goofy_face_big_mouth">Big mouth</string>
+    <!-- Effect setting item that makes mouth small. [CHAR LIMIT=14] -->
+    <string name="effect_goofy_face_small_mouth">Small mouth</string>
+    <!-- Effect setting item that makes nose big. [CHAR LIMIT=14] -->
+    <string name="effect_goofy_face_big_nose">Big nose</string>
+    <!-- Effect setting item that makes eyes small. [CHAR LIMIT=14] -->
+    <string name="effect_goofy_face_small_eyes">Small eyes</string>
+    <!-- Effect setting item that replaces background with Android in Space. [CHAR LIMIT=14] -->
+    <string name="effect_backdropper_space">In space</string>
+    <!-- Effect setting item that replaces background with a sunset. [CHAR LIMIT=14] -->
+    <string name="effect_backdropper_sunset">Sunset</string>
+    <!-- Effect setting item that replaces background with video from gallery. [CHAR LIMIT=14] -->
+    <string name="effect_backdropper_gallery">Your video</string>
+
+    <!-- Message displayed in overlay during background replacement training [CHAR LIMIT=180]-->
+    <string name="bg_replacement_message">Set your device down.\nStep out of view for a moment.</string>
+
+
+    <!-- Toast telling users tapping on the viewfinder will take a picture [CHAR LIMIT=54] -->
+    <string name="video_snapshot_hint">Touch to take photo while recording.</string>
+
+    <!-- Announcement telling users video recording has just started [CHAR LIMIT=NONE] -->
+    <string name="video_recording_started">Video recording has started.</string>
+    <!-- Announcement telling users video recording has just stopped [CHAR LIMIT=NONE] -->
+    <string name="video_recording_stopped">Video recording has stopped.</string>
+
+    <!-- Toast telling users video snapshot is disabled when the effects are on and a user tries to tap on the viewfinder [CHAR LIMIT=65] -->
+    <string name="disable_video_snapshot_hint">Video snapshot is disabled when special effects are on.</string>
+
+    <!-- A button in effect setting popup to clear the effect. [CHAR LIMIT=26] -->
+    <string name="clear_effects">Clear effects</string>
+
+    <!-- Title of category for silly face effects. [CHAR LIMIT=26] -->
+    <string name="effect_silly_faces">SILLY FACES</string>
+
+    <!-- Title of category for background replacement effects. [CHAR LIMIT=26] -->
+    <string name="effect_background">BACKGROUND</string>
+
+    <!-- The shutter button. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_shutter_button">Shutter button</string>
+    <!-- The menu button. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_menu_button">Menu button</string>
+    <!-- The button to review the thumbnail. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_review_thumbnail">Most recent photo</string>
+    <!-- The front/back camera switch. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_camera_picker">Front and back camera switch</string>
+    <!-- The mode picker to switch between camera, video and panorama. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_mode_picker">Camera, video, or panorama selector</string>
+    <!-- The button to switch to the second-level indicators of the camera settings. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_second_level_indicators">More setting controls</string>
+    <!-- The button to back to the first-level indicators of the camera settings. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_back_to_first_level">Close setting controls</string>
+    <!-- The zoom control button. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_zoom_control">Zoom control</string>
+    <!-- The decrement button in camera preference such as exposure, picture size. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_decrement">Decrease %1$s</string>
+    <!-- The increment button in camera preference such as exposure, picture size. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_increment">Increase %1$s</string>
+    <!-- The check box in camera settings, such as store location. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_check_box">%1$s check box</string>
+    <!-- The button to switch to Camera mode. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_switch_to_camera">Switch to photo</string>
+    <!-- The button to switch to Video mode. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_switch_to_video">Switch to video</string>
+    <!-- The button to switch to Panorama mode. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_switch_to_panorama">Switch to panorama</string>
+    <!-- The button to switch to new Panorama mode. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_switch_to_new_panorama">Switch to new panorama</string>
+    <!-- The button to switch to the Re-Focus mode. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_switch_to_refocus">Switch to Refocus</string>
+    <!-- The button in review mode indicating that the photo taking, video recording, and panorama saving session should be canceled [CHAR LIMIT = NONE] -->
+    <string name="accessibility_review_cancel">Review cancel</string>
+    <!-- The button in review mode indicating that the taken photo/video is OK to be attached/uploaded [CHAR LIMIT = NONE] -->
+    <string name="accessibility_review_ok">Review done</string>
+    <!-- button in review mode indicate the user want to retake another photo/video for attachment [
+CHAR LIMIT = NONE] -->
+    <string name="accessibility_review_retake">Review retake</string>
+    <!-- The button to play the video. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_play_video">Play video</string>
+    <!-- The button to pause the video. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_pause_video">Pause video</string>
+    <!-- The button to reload the video. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_reload_video">Reload video</string>
+    <!-- The time bar of the media player. [CHAR LIMIT = NONE] -->
+    <string name="accessibility_time_bar">Video player time bar</string>
+
+    <!-- TODO: remove the string as it is a work-around solution to bypass the default speak of the element type. -->
+    <string name="empty" translatable="false">" "</string>
+
+    <!-- Default text for a button that can be toggled on and off. -->
+    <string name="capital_on">ON</string>
+    <!-- Default text for a button that can be toggled on and off. -->
+    <string name="capital_off">OFF</string>
+
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_off">Off</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_500">0.5 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_1000">1 second</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_1500">1.5 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_2000">2 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_2500">2.5 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_3000">3 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_4000">4 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_5000">5 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_6000">6 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_10000">10 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_12000">12 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_15000">15 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_24000">24 seconds</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_30000">0.5 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_60000">1 minute</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_90000">1.5 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_120000">2 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_150000">2.5 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_180000">3 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_240000">4 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_300000">5 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_360000">6 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_600000">10 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_720000">12 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_900000">15 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_1440000">24 minutes</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_1800000">0.5 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_3600000">1 hour</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_5400000">1.5 hour</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_7200000">2 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_9000000">2.5 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_10800000">3 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_14400000">4 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_18000000">5 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_21600000">6 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_36000000">10 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_43200000">12 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_54000000">15 hours</string>
+    <!-- Text to indicate time lapse recording frame interval [CHAR LIMIT = 30] -->
+    <string name="pref_video_time_lapse_frame_interval_86400000">24 hours</string>
+
+    <!-- Seconds: a unit of time for time lapse intervals. [CHAR LIMIT = 20] -->
+    <string name="time_lapse_seconds">seconds</string>
+    <!-- Minutes: a unit of time for time lapse intervals. [CHAR LIMIT = 20] -->
+    <string name="time_lapse_minutes">minutes</string>
+    <!-- Hours: a unit of time for time lapse intervals. [CHAR LIMIT = 20] -->
+    <string name="time_lapse_hours">hours</string>
+
+    <!-- The button to confirm time-lapse setting changes. [CHAR LIMIT = 20] -->
+    <string name="time_lapse_interval_set">Done</string>
+    <!-- Title in time interval picker for setting time interval. [CHAR LIMIT = 30]-->
+    <string name="set_time_interval">Set Time Interval</string>
+    <!-- Help text that is shown when the time lapse feature is turned off. [CHAR LIMIT = 180]-->
+    <string name="set_time_interval_help">Time lapse feature is off. Turn it on to set time interval.</string>
+    <!-- Help text that is shown when the countdown timer is turned off. [CHAR LIMIT = 180]-->
+    <string name="set_timer_help">Countdown timer is off. Turn it on to count down before taking a picture.</string>
+    <!-- Title in timer setting for setting the duration for the countdown timer. [CHAR LIMIT = 50]-->
+    <string name="set_duration">Set duration in seconds</string>
+    <!-- On-screen hint during timer countdown for taking a photo. [CHAR LIMIT = 60]-->
+    <string name="count_down_title_text">Counting down to take a photo</string>
+
+    <!-- Title for first run dialog asking if the user wants to remember photo locations [CHAR LIMIT = 50] -->
+    <string name="remember_location_title">Remember photo locations?</string>
+    <!-- Message for first run dialog asking if the user wants to remember photo locations [CHAR LIMIT = None] -->
+    <string name="remember_location_prompt">Tag your photos and videos with the locations where they are taken.\n\nOther apps can access this information along with your saved images.</string>
+    <!-- Negative answer for first run dialog asking if the user wants to remember photo locations [CHAR LIMIT = 20] -->
+    <string name="remember_location_no">No thanks</string>
+    <!-- Positive answer for first run dialog asking if the user wants to remember photo locations [CHAR LIMIT = 20] -->
+    <string name="remember_location_yes">Yes</string>
+
+    <!-- Menu item to launch the camera app [CHAR LIMIT=25] -->
+    <string name="menu_camera">Camera</string>
+    <!-- Menu item to search for photos [CHAR LIMIT=25] -->
+    <string name="menu_search">Search</string>
+    <!-- Title for the all photos tab [CHAR LIMIT=25] -->
+    <string name="tab_photos">Photos</string>
+    <!-- Title for the albums tab [CHAR LIMIT=25] -->
+    <string name="tab_albums">Albums</string>
+
+    <!-- Camera menu labels -->
+
+    <!-- more options label [CHAR LIMIT=50] -->
+    <string name="camera_menu_more_label">MORE OPTIONS</string>
+    <!-- settings label [CHAR LIMIT=50] -->
+    <string name="camera_menu_settings_label">SETTINGS</string>
+
+    <!-- String indicating how many photos are in an album [CHAR LIMIT=15] -->
+    <plurals name="number_of_photos">
+        <item quantity="one">%1$d photo</item>
+        <item quantity="other">%1$d photos</item>
+    </plurals>
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 0000000..67c53f8
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,292 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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">
+    <style name="Theme.GalleryBase" parent="android:Theme.Holo">
+        <item name="listPreferredItemHeightSmall">48dp</item>
+        <item name="switchStyle">@android:style/Widget.CompoundButton</item>
+    </style>
+    <style name="Theme.Gallery.Dialog" parent="android:Theme.Holo.Dialog"/>
+    <style name="Theme.Gallery" parent="Theme.GalleryBase">
+        <item name="android:displayOptions"></item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:actionBarStyle">@style/Holo.ActionBar</item>
+        <item name="android:windowBackground">@android:color/black</item>
+        <item name="android:colorBackground">@null</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+    </style>
+    <style name="Theme.FilterShow" parent="Theme.Gallery">
+        <item name="android:windowBackground">@color/background_screen</item>
+    </style>
+    <style name="Holo.ActionBar" parent="android:Widget.Holo.ActionBar">
+        <item name="android:displayOptions">useLogo|showHome</item>
+        <item name="android:background">@drawable/actionbar_translucent</item>
+        <item name="android:backgroundStacked">@null</item>
+    </style>
+    <style name="MediaButton.Play" parent="@android:style/MediaButton.Play">
+        <item name="android:background">@null</item>
+        <item name="android:src">@drawable/icn_media_play</item>
+    </style>
+    <style name="DialogPickerTheme" parent="Theme.Gallery">
+    </style>
+    <style name="Theme.ProxyLauncher" parent="@android:Theme.Translucent.NoTitleBar">
+    </style>
+    <bool name="picker_is_dialog">false</bool>
+    <color name="darker_transparent">#C1000000</color>
+    <style name="ActionBarTwoLinePrimary" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title"></style>
+    <style name="ActionBarTwoLineSecondary" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Subtitle"></style>
+    <style name="ActionBarTwoLineItem">
+        <item name="android:background">@drawable/action_bar_two_line_background</item>
+    </style>
+
+    <!-- Camera resources below -->
+
+    <style name="Theme.Camera" parent="Theme.CameraBase">
+        <item name="android:windowBackground">@android:color/black</item>
+        <item name="android:colorBackground">@android:color/black</item>
+        <item name="android:colorBackgroundCacheHint">@android:color/black</item>
+    </style>
+    <style name="Theme.CameraBase" parent="android:Theme.Black.NoTitleBar.Fullscreen"/>
+    <style name="OnScreenHintTextAppearance">
+        <item name="android:textColor">@android:color/primary_text_dark</item>
+        <item name="android:textColorHighlight">#FFFF9200</item>
+        <item name="android:textColorHint">#808080</item>
+        <item name="android:textColorLink">#5C5CFF</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textStyle">normal</item>
+    </style>
+    <style name="OnScreenHintTextAppearance.Small">
+        <item name="android:textSize">14sp</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textColor">@android:color/secondary_text_dark</item>
+    </style>
+    <style name="Animation_OnScreenHint">
+        <item name="android:windowEnterAnimation">@anim/on_screen_hint_enter</item>
+        <item name="android:windowExitAnimation">@anim/on_screen_hint_exit</item>
+    </style>
+    <style name="ReviewPlayIcon">
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_centerInParent">true</item>
+        <item name="android:visibility">gone</item>
+        <item name="android:src">@drawable/ic_gallery_play_big</item>
+    </style>
+    <style name="PopupTitleSeparator">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">2dp</item>
+        <item name="android:background">@color/popup_title_color</item>
+    </style>
+    <style name="SettingItemList">
+        <item name="android:orientation">vertical</item>
+        <item name="android:paddingBottom">3dp</item>
+        <item name="android:layout_gravity">center</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:listSelector">@drawable/bg_pressed</item>
+    </style>
+    <style name="SettingItemTitle">
+        <item name="android:textSize">@dimen/setting_item_text_size</item>
+        <item name="android:gravity">left|center_vertical</item>
+        <item name="android:textColor">@color/primary_text</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:layout_weight">1</item>
+        <item name="android:layout_width">0dp</item>
+        <item name="android:layout_height">match_parent</item>
+    </style>
+    <style name="SettingItemText">
+        <item name="android:layout_width">@dimen/setting_item_text_width</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_gravity">center_vertical</item>
+        <item name="android:gravity">center</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textColor">@color/primary_text</item>
+        <item name="android:textSize">@dimen/setting_item_text_size</item>
+    </style>
+    <style name="SettingRow">
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:orientation">horizontal</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">@dimen/setting_row_height</item>
+        <item name="android:paddingLeft">@dimen/setting_item_list_margin</item>
+        <item name="android:paddingRight">@dimen/setting_item_list_margin</item>
+        <item name="android:background">@drawable/setting_picker</item>
+    </style>
+    <style name="OnViewfinderLabel">
+        <item name="android:gravity">center</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:layout_margin">10dp</item>
+        <item name="android:paddingLeft">15dp</item>
+        <item name="android:paddingRight">15dp</item>
+        <item name="android:paddingTop">3dp</item>
+        <item name="android:paddingBottom">3dp</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:textSize">16dp</item>
+        <item name="android:background">@drawable/bg_text_on_preview</item>
+    </style>
+    <style name="PanoCustomDialogText">
+        <item name="android:textAppearance">@android:style/TextAppearance.Medium</item>
+    </style>
+    <style name="EffectSettingGrid">
+        <item name="android:layout_marginLeft">@dimen/setting_item_list_margin</item>
+        <item name="android:layout_marginRight">@dimen/setting_item_list_margin</item>
+        <item name="android:paddingBottom">3dp</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:numColumns">3</item>
+        <item name="android:verticalSpacing">3dp</item>
+        <item name="android:horizontalSpacing">3dp</item>
+        <item name="android:choiceMode">singleChoice</item>
+    </style>
+    <style name="EffectSettingItem">
+        <item name="android:orientation">vertical</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:paddingTop">9dp</item>
+        <item name="android:paddingBottom">9dp</item>
+        <item name="android:paddingLeft">2dp</item>
+        <item name="android:paddingRight">2dp</item>
+        <item name="android:background">@drawable/setting_picker</item>
+    </style>
+    <style name="EffectSettingItemTitle">
+        <item name="android:textSize">@dimen/effect_setting_item_text_size</item>
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:paddingTop">1dp</item>
+    </style>
+    <style name="EffectSettingTypeTitle">
+        <item name="android:textSize">@dimen/effect_setting_type_text_size</item>
+        <item name="android:gravity">left|center_vertical</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:alpha">0.7</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:minHeight">@dimen/effect_setting_type_text_min_height</item>
+        <item name="android:paddingLeft">@dimen/effect_setting_type_text_left_padding</item>
+    </style>
+    <style name="EffectTypeSeparator">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_marginLeft">8dp</item>
+        <item name="android:layout_marginRight">8dp</item>
+        <item name="android:layout_marginBottom">14dp</item>
+        <item name="android:layout_height">2dp</item>
+        <item name="android:background">#2c2c2c</item>
+    </style>
+    <style name="EffectTitleSeparator">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">2dp</item>
+        <item name="android:paddingBottom">4dp</item>
+        <item name="android:background">@android:drawable/divider_horizontal_dark</item>
+    </style>
+    <style name="TextAppearance.DialogWindowTitle" parent="">
+        <item name="android:textSize">22sp</item>
+        <item name="android:textColor">@color/holo_blue_light</item>
+    </style>
+    <style name="TextAppearance.Medium" parent="@android:style/TextAppearance.Medium"/>
+    <style name="Widget.Button.Borderless" parent="android:Widget.Button">
+        <item name="android:background">@drawable/bg_pressed</item>
+        <item name="android:textAppearance">@style/TextAppearance.Medium</item>
+        <item name="android:textColor">@color/primary_text</item>
+        <item name="android:minHeight">48dip</item>
+        <item name="android:minWidth">64dip</item>
+        <item name="android:paddingLeft">4dip</item>
+        <item name="android:paddingRight">4dip</item>
+    </style>
+
+    <style name="ReviewControlText_xlarge">
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:background">@drawable/bg_pressed_exit_fading</item>
+        <item name="android:gravity">center</item>
+        <item name="android:paddingLeft">2dp</item>
+        <item name="android:paddingRight">10dp</item>
+        <item name="android:paddingTop">10dp</item>
+        <item name="android:paddingBottom">10dp</item>
+        <item name="android:textSize">18sp</item>
+        <item name="android:textStyle">bold</item>
+        <item name="android:clickable">true</item>
+        <item name="android:focusable">true</item>
+    </style>
+    <style name="PopupTitleText_xlarge">
+        <item name="android:textSize">@dimen/popup_title_text_size</item>
+        <item name="android:layout_gravity">left|center_vertical</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textColor">@color/popup_title_color</item>
+        <item name="android:layout_marginLeft">10dp</item>
+    </style>
+    <style name="PanoCustomDialogText_xlarge">
+        <item name="android:textAppearance">@android:style/TextAppearance.Large</item>
+    </style>
+    <style name="ViewfinderLabelLayout_xlarge">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:layout_margin">13dp</item>
+    </style>
+    <style name="SwitcherButton">
+        <item name="android:layout_width">@dimen/switcher_size</item>
+        <item name="android:layout_height">@dimen/switcher_size</item>
+        <item name="android:background">@drawable/bg_pressed_exit_fading</item>
+    </style>
+    <style name="MenuIndicator">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:enabled">false</item>
+        <item name="android:scaleType">center</item>
+    </style>
+    <style name="CameraControls">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+    </style>
+    <style name="UndoBar">
+        <item name="android:layout_marginLeft">4dp</item>
+        <item name="android:layout_marginRight">4dp</item>
+        <item name="android:paddingLeft">16dp</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">48dp</item>
+        <item name="android:layout_gravity">bottom</item>
+        <item name="android:background">@drawable/panel_undo_holo</item>
+    </style>
+    <style name="UndoBarTextAppearance">
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">@android:color/white</item>
+    </style>
+    <style name="UndoBarSeparator">
+        <item name="android:background">@color/gray</item>
+        <item name="android:layout_width">1dp</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:layout_marginTop">10dp</item>
+        <item name="android:layout_marginBottom">10dp</item>
+        <item name="android:paddingRight">12dp</item>
+    </style>
+    <style name="UndoButton">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:paddingRight">16dp</item>
+        <item name="android:textSize">12sp</item>
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:textStyle">bold</item>
+        <item name="android:textColor">@color/gray</item>
+        <item name="android:drawablePadding">8dp</item>
+        <item name="android:background">@drawable/bg_pressed</item>
+    </style>
+</resources>
diff --git a/res/xml/camera_preferences.xml b/res/xml/camera_preferences.xml
new file mode 100644
index 0000000..8c13a34
--- /dev/null
+++ b/res/xml/camera_preferences.xml
@@ -0,0 +1,101 @@
+<?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.
+-->
+
+<PreferenceGroup
+        xmlns:camera="http://schemas.android.com/apk/res/com.android.gallery3d"
+        camera:title="@string/pref_camera_settings_category">
+    <IconListPreference
+            camera:key="pref_camera_flashmode_key"
+            camera:defaultValue="@string/pref_camera_flashmode_default"
+            camera:title="@string/pref_camera_flashmode_title"
+            camera:icons="@array/camera_flashmode_icons"
+            camera:largeIcons="@array/camera_flashmode_largeicons"
+            camera:entries="@array/pref_camera_flashmode_entries"
+            camera:entryValues="@array/pref_camera_flashmode_entryvalues"
+            camera:labelList="@array/pref_camera_flashmode_labels" />
+    <IconListPreference
+            camera:key="pref_camera_exposure_key"
+            camera:defaultValue="@string/pref_exposure_default"
+            camera:title="@string/pref_exposure_title"
+            camera:singleIcon="@drawable/ic_exposure_holo_light" />
+    <IconListPreference
+            camera:key="pref_camera_scenemode_key"
+            camera:defaultValue="@string/pref_camera_scenemode_default"
+            camera:title="@string/pref_camera_scenemode_title"
+            camera:singleIcon="@drawable/ic_sce"
+            camera:entries="@array/pref_camera_scenemode_entries"
+            camera:labelList="@array/pref_camera_scenemode_labels"
+            camera:icons="@array/pref_camera_scenemode_icons"
+            camera:largeIcons="@array/pref_camera_scenemode_icons"
+            camera:entryValues="@array/pref_camera_scenemode_entryvalues" />
+    <IconListPreference
+            camera:key="pref_camera_whitebalance_key"
+            camera:defaultValue="@string/pref_camera_whitebalance_default"
+            camera:title="@string/pref_camera_whitebalance_title"
+            camera:icons="@array/whitebalance_icons"
+            camera:largeIcons="@array/whitebalance_largeicons"
+            camera:entries="@array/pref_camera_whitebalance_entries"
+            camera:entryValues="@array/pref_camera_whitebalance_entryvalues"
+            camera:labelList="@array/pref_camera_whitebalance_labels" />
+    <RecordLocationPreference
+            camera:key="pref_camera_recordlocation_key"
+            camera:defaultValue="@string/pref_camera_recordlocation_default"
+            camera:title="@string/pref_camera_recordlocation_title"
+            camera:icons="@array/camera_recordlocation_icons"
+            camera:largeIcons="@array/camera_recordlocation_largeicons"
+            camera:entries="@array/pref_camera_recordlocation_entries"
+            camera:labelList="@array/pref_camera_recordlocation_labels"
+            camera:entryValues="@array/pref_camera_recordlocation_entryvalues" />
+    <ListPreference
+            camera:key="pref_camera_picturesize_key"
+            camera:title="@string/pref_camera_picturesize_title"
+            camera:entries="@array/pref_camera_picturesize_entries"
+            camera:entryValues="@array/pref_camera_picturesize_entryvalues" />
+    <ListPreference
+            camera:key="pref_camera_focusmode_key"
+            camera:defaultValue="@array/pref_camera_focusmode_default_array"
+            camera:title="@string/pref_camera_focusmode_title"
+            camera:entries="@array/pref_camera_focusmode_entries"
+            camera:labelList="@array/pref_camera_focusmode_labels"
+            camera:entryValues="@array/pref_camera_focusmode_entryvalues" />
+    <IconListPreference
+            camera:key="pref_camera_id_key"
+            camera:defaultValue="@string/pref_camera_id_default"
+            camera:title="@string/pref_camera_id_title"
+            camera:icons="@array/camera_id_icons"
+            camera:entries="@array/camera_id_entries"
+            camera:labelList="@array/camera_id_labels"
+            camera:largeIcons="@array/camera_id_largeicons" />
+    <IconListPreference
+            camera:key="pref_camera_hdr_key"
+            camera:defaultValue="@string/pref_camera_hdr_default"
+            camera:title="@string/pref_camera_scenemode_entry_hdr"
+            camera:entries="@array/pref_camera_hdr_entries"
+            camera:icons="@array/pref_camera_hdr_icons"
+            camera:largeIcons="@array/pref_camera_hdr_icons"
+            camera:labelList="@array/pref_camera_hdr_labels"
+            camera:entryValues="@array/pref_camera_hdr_entryvalues" />
+    <CountDownTimerPreference
+            camera:key="pref_camera_timer_key"
+            camera:defaultValue="@string/pref_camera_timer_default"
+            camera:title="@string/pref_camera_timer_title" />
+    <ListPreference
+            camera:key="pref_camera_timer_sound_key"
+            camera:defaultValue="@string/pref_camera_timer_sound_default"
+            camera:title="@string/pref_camera_timer_sound_title"
+            camera:entries="@array/pref_camera_timer_sound_entries"
+            camera:entryValues="@array/pref_camera_timer_sound_entryvalues" />
+</PreferenceGroup>
diff --git a/res/xml/device_filter.xml b/res/xml/device_filter.xml
new file mode 100644
index 0000000..36cd13d
--- /dev/null
+++ b/res/xml/device_filter.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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>
+    <!-- filter for PTP devices -->
+    <usb-device class="6" subclass="1" protocol="1" />
+</resources>
diff --git a/res/xml/video_preferences.xml b/res/xml/video_preferences.xml
new file mode 100644
index 0000000..79154e6
--- /dev/null
+++ b/res/xml/video_preferences.xml
@@ -0,0 +1,74 @@
+<?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.
+-->
+
+<PreferenceGroup
+        xmlns:camera="http://schemas.android.com/apk/res/com.android.gallery3d"
+        camera:title="@string/pref_camcorder_settings_category">
+    <ListPreference
+            camera:key="pref_video_quality_key"
+            camera:title="@string/pref_video_quality_title"
+            camera:entries="@array/pref_video_quality_entries"
+            camera:entryValues="@array/pref_video_quality_entryvalues"/>
+    <IconListPreference
+            camera:key="pref_video_time_lapse_frame_interval_key"
+            camera:defaultValue="@string/pref_video_time_lapse_frame_interval_default"
+            camera:title="@string/pref_video_time_lapse_frame_interval_title"
+            camera:singleIcon="@drawable/ic_timelapse_none"
+            camera:entries="@array/pref_video_time_lapse_frame_interval_entries"
+            camera:entryValues="@array/pref_video_time_lapse_frame_interval_entryvalues"/>
+    <IconListPreference
+            camera:key="pref_camera_video_flashmode_key"
+            camera:defaultValue="@string/pref_camera_video_flashmode_default"
+            camera:title="@string/pref_camera_flashmode_title"
+            camera:icons="@array/video_flashmode_icons"
+            camera:largeIcons="@array/video_flashmode_largeicons"
+            camera:entries="@array/pref_camera_video_flashmode_entries"
+            camera:labelList="@array/pref_camera_video_flashmode_labels"
+            camera:entryValues="@array/pref_camera_video_flashmode_entryvalues"/>
+    <IconListPreference
+            camera:key="pref_camera_whitebalance_key"
+            camera:defaultValue="@string/pref_camera_whitebalance_default"
+            camera:title="@string/pref_camera_whitebalance_title"
+            camera:icons="@array/whitebalance_icons"
+            camera:largeIcons="@array/whitebalance_largeicons"
+            camera:entries="@array/pref_camera_whitebalance_entries"
+            camera:labelList="@array/pref_camera_whitebalance_labels"
+            camera:entryValues="@array/pref_camera_whitebalance_entryvalues"/>
+    <IconListPreference
+            camera:key="pref_camera_id_key"
+            camera:defaultValue="@string/pref_camera_id_default"
+            camera:title="@string/pref_camera_id_title"
+            camera:icons="@array/camera_id_icons"
+            camera:entries="@array/camera_id_entries"
+            camera:labelList="@array/camera_id_labels"
+            camera:largeIcons="@array/camera_id_largeicons"/>
+    <IconListPreference
+            camera:key="pref_video_effect_key"
+            camera:defaultValue="@string/pref_video_effect_default"
+            camera:title="@string/pref_video_effect_title"
+            camera:icons="@array/video_effect_icons"
+            camera:largeIcons="@array/video_effect_icons"
+            camera:entries="@array/pref_video_effect_entries"
+            camera:entryValues="@array/pref_video_effect_entryvalues" />
+    <RecordLocationPreference
+            camera:key="pref_camera_recordlocation_key"
+            camera:defaultValue="@string/pref_camera_recordlocation_default"
+            camera:title="@string/pref_camera_recordlocation_title"
+            camera:icons="@array/camera_recordlocation_icons"
+            camera:largeIcons="@array/camera_recordlocation_largeicons"
+            camera:entries="@array/pref_camera_recordlocation_entries"
+            camera:entryValues="@array/pref_camera_recordlocation_entryvalues" />
+</PreferenceGroup>
diff --git a/res/xml/wallpaper_picker_preview.xml b/res/xml/wallpaper_picker_preview.xml
new file mode 100644
index 0000000..759ff6f
--- /dev/null
+++ b/res/xml/wallpaper_picker_preview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<wallpaper-preview xmlns:android="http://schemas.android.com/apk/res/android"
+    android:staticWallpaperPreview="@drawable/wallpaper_picker_preview">
+</wallpaper-preview>
diff --git a/res/xml/widget_info.xml b/res/xml/widget_info.xml
new file mode 100644
index 0000000..4aa460f
--- /dev/null
+++ b/res/xml/widget_info.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+        android:minWidth="@dimen/appwidget_width"
+        android:minHeight="@dimen/appwidget_height"
+        android:updatePeriodMillis="86400000"
+        android:previewImage="@drawable/preview"
+        android:initialLayout="@layout/appwidget_main"
+        android:configure="com.android.gallery3d.gadget.WidgetConfigure"/>
diff --git a/src/android/util/Pools.java b/src/android/util/Pools.java
new file mode 100644
index 0000000..40bab1e
--- /dev/null
+++ b/src/android/util/Pools.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Helper class for crating pools of objects. An example use looks like this:
+ * <pre>
+ * public class MyPooledClass {
+ *
+ *     private static final SynchronizedPool<MyPooledClass> sPool =
+ *             new SynchronizedPool<MyPooledClass>(10);
+ *
+ *     public static MyPooledClass obtain() {
+ *         MyPooledClass instance = sPool.acquire();
+ *         return (instance != null) ? instance : new MyPooledClass();
+ *     }
+ *
+ *     public void recycle() {
+ *          // Clear state if needed.
+ *          sPool.release(this);
+ *     }
+ *
+ *     . . .
+ * }
+ * </pre>
+ *
+ * @hide
+ */
+public final class Pools {
+
+    /**
+     * Interface for managing a pool of objects.
+     *
+     * @param <T> The pooled type.
+     */
+    public static interface Pool<T> {
+
+        /**
+         * @return An instance from the pool if such, null otherwise.
+         */
+        public T acquire();
+
+        /**
+         * Release an instance to the pool.
+         *
+         * @param instance The instance to release.
+         * @return Whether the instance was put in the pool.
+         *
+         * @throws IllegalStateException If the instance is already in the pool.
+         */
+        public boolean release(T instance);
+    }
+
+    private Pools() {
+        /* do nothing - hiding constructor */
+    }
+
+    /**
+     * Simple (non-synchronized) pool of objects.
+     *
+     * @param <T> The pooled type.
+     */
+    public static class SimplePool<T> implements Pool<T> {
+        private final Object[] mPool;
+
+        private int mPoolSize;
+
+        /**
+         * Creates a new instance.
+         *
+         * @param maxPoolSize The max pool size.
+         *
+         * @throws IllegalArgumentException If the max pool size is less than zero.
+         */
+        public SimplePool(int maxPoolSize) {
+            if (maxPoolSize <= 0) {
+                throw new IllegalArgumentException("The max pool size must be > 0");
+            }
+            mPool = new Object[maxPoolSize];
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public T acquire() {
+            if (mPoolSize > 0) {
+                final int lastPooledIndex = mPoolSize - 1;
+                T instance = (T) mPool[lastPooledIndex];
+                mPool[lastPooledIndex] = null;
+                mPoolSize--;
+                return instance;
+            }
+            return null;
+        }
+
+        @Override
+        public boolean release(T instance) {
+            if (isInPool(instance)) {
+                throw new IllegalStateException("Already in the pool!");
+            }
+            if (mPoolSize < mPool.length) {
+                mPool[mPoolSize] = instance;
+                mPoolSize++;
+                return true;
+            }
+            return false;
+        }
+
+        private boolean isInPool(T instance) {
+            for (int i = 0; i < mPoolSize; i++) {
+                if (mPool[i] == instance) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Synchronized) pool of objects.
+     *
+     * @param <T> The pooled type.
+     */
+    public static class SynchronizedPool<T> extends SimplePool<T> {
+        private final Object mLock = new Object();
+
+        /**
+         * Creates a new instance.
+         *
+         * @param maxPoolSize The max pool size.
+         *
+         * @throws IllegalArgumentException If the max pool size is less than zero.
+         */
+        public SynchronizedPool(int maxPoolSize) {
+            super(maxPoolSize);
+        }
+
+        @Override
+        public T acquire() {
+            synchronized (mLock) {
+                return super.acquire();
+            }
+        }
+
+        @Override
+        public boolean release(T element) {
+            synchronized (mLock) {
+                return super.release(element);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/camera/AndroidCameraManagerImpl.java b/src/com/android/camera/AndroidCameraManagerImpl.java
new file mode 100644
index 0000000..897aa92
--- /dev/null
+++ b/src/com/android/camera/AndroidCameraManagerImpl.java
@@ -0,0 +1,779 @@
+/*
+ * Copyright (C) 2013 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 com.android.camera.Util.Assert;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.AutoFocusCallback;
+import android.hardware.Camera.AutoFocusMoveCallback;
+import android.hardware.Camera.ErrorCallback;
+import android.hardware.Camera.FaceDetectionListener;
+import android.hardware.Camera.OnZoomChangeListener;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.PictureCallback;
+import android.hardware.Camera.PreviewCallback;
+import android.hardware.Camera.ShutterCallback;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.IOException;
+
+/**
+ * A class to implement {@link CameraManager} of the Android camera framework.
+ */
+class AndroidCameraManagerImpl implements CameraManager {
+    private static final String TAG = "CAM_" +
+            AndroidCameraManagerImpl.class.getSimpleName();
+
+    private Parameters mParameters;
+    private boolean mParametersIsDirty;
+    private IOException mReconnectIOException;
+
+    /* Messages used in CameraHandler. */
+    // Camera initialization/finalization
+    private static final int OPEN_CAMERA = 1;
+    private static final int RELEASE =     2;
+    private static final int RECONNECT =   3;
+    private static final int UNLOCK =      4;
+    private static final int LOCK =        5;
+    // Preview
+    private static final int SET_PREVIEW_TEXTURE_ASYNC =        101;
+    private static final int START_PREVIEW_ASYNC =              102;
+    private static final int STOP_PREVIEW =                     103;
+    private static final int SET_PREVIEW_CALLBACK_WITH_BUFFER = 104;
+    private static final int ADD_CALLBACK_BUFFER =              105;
+    private static final int SET_PREVIEW_DISPLAY_ASYNC =        106;
+    private static final int SET_PREVIEW_CALLBACK =             107;
+    // Parameters
+    private static final int SET_PARAMETERS =     201;
+    private static final int GET_PARAMETERS =     202;
+    private static final int REFRESH_PARAMETERS = 203;
+    // Focus, Zoom
+    private static final int AUTO_FOCUS =                   301;
+    private static final int CANCEL_AUTO_FOCUS =            302;
+    private static final int SET_AUTO_FOCUS_MOVE_CALLBACK = 303;
+    private static final int SET_ZOOM_CHANGE_LISTENER =     304;
+    // Face detection
+    private static final int SET_FACE_DETECTION_LISTENER = 461;
+    private static final int START_FACE_DETECTION =        462;
+    private static final int STOP_FACE_DETECTION =         463;
+    private static final int SET_ERROR_CALLBACK =          464;
+    // Presentation
+    private static final int ENABLE_SHUTTER_SOUND =    501;
+    private static final int SET_DISPLAY_ORIENTATION = 502;
+
+    private CameraHandler mCameraHandler;
+    private android.hardware.Camera mCamera;
+
+    // Used to retain a copy of Parameters for setting parameters.
+    private Parameters mParamsToSet;
+
+    AndroidCameraManagerImpl() {
+        HandlerThread ht = new HandlerThread("Camera Handler Thread");
+        ht.start();
+        mCameraHandler = new CameraHandler(ht.getLooper());
+    }
+
+    private class CameraHandler extends Handler {
+        CameraHandler(Looper looper) {
+            super(looper);
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        private void startFaceDetection() {
+            mCamera.startFaceDetection();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        private void stopFaceDetection() {
+            mCamera.stopFaceDetection();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        private void setFaceDetectionListener(FaceDetectionListener listener) {
+            mCamera.setFaceDetectionListener(listener);
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+        private void setPreviewTexture(Object surfaceTexture) {
+            try {
+                mCamera.setPreviewTexture((SurfaceTexture) surfaceTexture);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN_MR1)
+        private void enableShutterSound(boolean enable) {
+            mCamera.enableShutterSound(enable);
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+        private void setAutoFocusMoveCallback(
+                android.hardware.Camera camera, Object cb) {
+            camera.setAutoFocusMoveCallback((AutoFocusMoveCallback) cb);
+        }
+
+        public void requestTakePicture(
+                final ShutterCallback shutter,
+                final PictureCallback raw,
+                final PictureCallback postView,
+                final PictureCallback jpeg) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        mCamera.takePicture(shutter, raw, postView, jpeg);
+                    } catch (RuntimeException e) {
+                        // TODO: output camera state and focus state for debugging.
+                        Log.e(TAG, "take picture failed.");
+                        throw e;
+                    }
+                }
+            });
+        }
+
+        /**
+         * Waits for all the {@code Message} and {@code Runnable} currently in the queue
+         * are processed.
+         *
+         * @return {@code false} if the wait was interrupted, {@code true} otherwise.
+         */
+        public boolean waitDone() {
+            final Object waitDoneLock = new Object();
+            final Runnable unlockRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (waitDoneLock) {
+                        waitDoneLock.notifyAll();
+                    }
+                }
+            };
+
+            synchronized (waitDoneLock) {
+                mCameraHandler.post(unlockRunnable);
+                try {
+                    waitDoneLock.wait();
+                } catch (InterruptedException ex) {
+                    Log.v(TAG, "waitDone interrupted");
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * This method does not deal with the API level check.  Everyone should
+         * check first for supported operations before sending message to this handler.
+         */
+        @Override
+        public void handleMessage(final Message msg) {
+            try {
+                switch (msg.what) {
+                    case OPEN_CAMERA:
+                        mCamera = android.hardware.Camera.open(msg.arg1);
+                        if (mCamera != null) {
+                            mParametersIsDirty = true;
+
+                            // Get a instance of Camera.Parameters for later use.
+                            if (mParamsToSet == null) {
+                                mParamsToSet = mCamera.getParameters();
+                            }
+                        }
+                        return;
+
+                    case RELEASE:
+                        mCamera.release();
+                        mCamera = null;
+                        return;
+
+                    case RECONNECT:
+                        mReconnectIOException = null;
+                        try {
+                            mCamera.reconnect();
+                        } catch (IOException ex) {
+                            mReconnectIOException = ex;
+                        }
+                        return;
+
+                    case UNLOCK:
+                        mCamera.unlock();
+                        return;
+
+                    case LOCK:
+                        mCamera.lock();
+                        return;
+
+                    case SET_PREVIEW_TEXTURE_ASYNC:
+                        setPreviewTexture(msg.obj);
+                        return;
+
+                    case SET_PREVIEW_DISPLAY_ASYNC:
+                        try {
+                            mCamera.setPreviewDisplay((SurfaceHolder) msg.obj);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                        return;
+
+                    case START_PREVIEW_ASYNC:
+                        mCamera.startPreview();
+                        return;
+
+                    case STOP_PREVIEW:
+                        mCamera.stopPreview();
+                        return;
+
+                    case SET_PREVIEW_CALLBACK_WITH_BUFFER:
+                        mCamera.setPreviewCallbackWithBuffer(
+                            (PreviewCallback) msg.obj);
+                        return;
+
+                    case ADD_CALLBACK_BUFFER:
+                        mCamera.addCallbackBuffer((byte[]) msg.obj);
+                        return;
+
+                    case AUTO_FOCUS:
+                        mCamera.autoFocus((AutoFocusCallback) msg.obj);
+                        return;
+
+                    case CANCEL_AUTO_FOCUS:
+                        mCamera.cancelAutoFocus();
+                        return;
+
+                    case SET_AUTO_FOCUS_MOVE_CALLBACK:
+                        setAutoFocusMoveCallback(mCamera, msg.obj);
+                        return;
+
+                    case SET_DISPLAY_ORIENTATION:
+                        mCamera.setDisplayOrientation(msg.arg1);
+                        return;
+
+                    case SET_ZOOM_CHANGE_LISTENER:
+                        mCamera.setZoomChangeListener(
+                            (OnZoomChangeListener) msg.obj);
+                        return;
+
+                    case SET_FACE_DETECTION_LISTENER:
+                        setFaceDetectionListener((FaceDetectionListener) msg.obj);
+                        return;
+
+                    case START_FACE_DETECTION:
+                        startFaceDetection();
+                        return;
+
+                    case STOP_FACE_DETECTION:
+                        stopFaceDetection();
+                        return;
+
+                    case SET_ERROR_CALLBACK:
+                        mCamera.setErrorCallback((ErrorCallback) msg.obj);
+                        return;
+
+                    case SET_PARAMETERS:
+                        mParametersIsDirty = true;
+                        mParamsToSet.unflatten((String) msg.obj);
+                        mCamera.setParameters(mParamsToSet);
+                        return;
+
+                    case GET_PARAMETERS:
+                        if (mParametersIsDirty) {
+                            mParameters = mCamera.getParameters();
+                            mParametersIsDirty = false;
+                        }
+                        return;
+
+                    case SET_PREVIEW_CALLBACK:
+                        mCamera.setPreviewCallback((PreviewCallback) msg.obj);
+                        return;
+
+                    case ENABLE_SHUTTER_SOUND:
+                        enableShutterSound((msg.arg1 == 1) ? true : false);
+                        return;
+
+                    case REFRESH_PARAMETERS:
+                        mParametersIsDirty = true;
+                        return;
+
+                    default:
+                        throw new RuntimeException("Invalid CameraProxy message=" + msg.what);
+                }
+            } catch (RuntimeException e) {
+                if (msg.what != RELEASE && mCamera != null) {
+                    try {
+                        mCamera.release();
+                    } catch (Exception ex) {
+                        Log.e(TAG, "Fail to release the camera.");
+                    }
+                    mCamera = null;
+                }
+                throw e;
+            }
+        }
+    }
+
+    @Override
+    public CameraManager.CameraProxy cameraOpen(int cameraId) {
+        mCameraHandler.obtainMessage(OPEN_CAMERA, cameraId, 0).sendToTarget();
+        mCameraHandler.waitDone();
+        if (mCamera != null) {
+            return new AndroidCameraProxyImpl();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * A class which implements {@link CameraManager.CameraProxy} and 
+     * camera handler thread.
+     */
+    public class AndroidCameraProxyImpl implements CameraManager.CameraProxy {
+
+        private AndroidCameraProxyImpl() {
+            Assert(mCamera != null);
+        }
+
+        @Override
+        public android.hardware.Camera getCamera() {
+            return mCamera;
+        }
+
+        @Override
+        public void release() {
+            // release() must be synchronous so we know exactly when the camera
+            // is released and can continue on.
+            mCameraHandler.sendEmptyMessage(RELEASE);
+            mCameraHandler.waitDone();
+        }
+
+        @Override
+        public void reconnect() throws IOException {
+            mCameraHandler.sendEmptyMessage(RECONNECT);
+            mCameraHandler.waitDone();
+            if (mReconnectIOException != null) {
+                throw mReconnectIOException;
+            }
+        }
+
+        @Override
+        public void unlock() {
+            mCameraHandler.sendEmptyMessage(UNLOCK);
+            mCameraHandler.waitDone();
+        }
+
+        @Override
+        public void lock() {
+            mCameraHandler.sendEmptyMessage(LOCK);
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+        @Override
+        public void setPreviewTexture(SurfaceTexture surfaceTexture) {
+            mCameraHandler.obtainMessage(SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture).sendToTarget();
+        }
+
+        @Override
+        public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
+            mCameraHandler.obtainMessage(SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder).sendToTarget();
+        }
+
+        @Override
+        public void startPreview() {
+            mCameraHandler.sendEmptyMessage(START_PREVIEW_ASYNC);
+        }
+
+        @Override
+        public void stopPreview() {
+            mCameraHandler.sendEmptyMessage(STOP_PREVIEW);
+            mCameraHandler.waitDone();
+        }
+
+        @Override
+        public void setPreviewDataCallback(
+                Handler handler, CameraPreviewDataCallback cb) {
+            mCameraHandler.obtainMessage(
+                    SET_PREVIEW_CALLBACK,
+                    PreviewCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+        }
+
+        @Override
+        public void setPreviewDataCallbackWithBuffer(
+                Handler handler, CameraPreviewDataCallback cb) {
+            mCameraHandler.obtainMessage(
+                    SET_PREVIEW_CALLBACK_WITH_BUFFER,
+                    PreviewCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+        }
+
+        @Override
+        public void addCallbackBuffer(byte[] callbackBuffer) {
+            mCameraHandler.obtainMessage(ADD_CALLBACK_BUFFER, callbackBuffer).sendToTarget();
+        }
+
+        @Override
+        public void autoFocus(Handler handler, CameraAFCallback cb) {
+            mCameraHandler.obtainMessage(
+                    AUTO_FOCUS,
+                    AFCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+        }
+
+        @Override
+        public void cancelAutoFocus() {
+            mCameraHandler.removeMessages(AUTO_FOCUS);
+            mCameraHandler.sendEmptyMessage(CANCEL_AUTO_FOCUS);
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+        @Override
+        public void setAutoFocusMoveCallback(
+                Handler handler, CameraAFMoveCallback cb) {
+            mCameraHandler.obtainMessage(
+                    SET_AUTO_FOCUS_MOVE_CALLBACK,
+                    AFMoveCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+        }
+
+        @Override
+        public void takePicture(
+                Handler handler,
+                CameraShutterCallback shutter,
+                CameraPictureCallback raw,
+                CameraPictureCallback post,
+                CameraPictureCallback jpeg) {
+            mCameraHandler.requestTakePicture(
+                    ShutterCallbackForward.getNewInstance(handler, this, shutter),
+                    PictureCallbackForward.getNewInstance(handler, this, raw),
+                    PictureCallbackForward.getNewInstance(handler, this, post),
+                    PictureCallbackForward.getNewInstance(handler, this, jpeg));
+        }
+
+        @Override
+        public void setDisplayOrientation(int degrees) {
+            mCameraHandler.obtainMessage(SET_DISPLAY_ORIENTATION, degrees, 0)
+                    .sendToTarget();
+        }
+
+        @Override
+        public void setZoomChangeListener(OnZoomChangeListener listener) {
+            mCameraHandler.obtainMessage(SET_ZOOM_CHANGE_LISTENER, listener).sendToTarget();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        public void setFaceDetectionCallback(
+                Handler handler, CameraFaceDetectionCallback cb) {
+            mCameraHandler.obtainMessage(
+                    SET_FACE_DETECTION_LISTENER,
+                    FaceDetectionCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+        }
+
+        @Override
+        public void startFaceDetection() {
+            mCameraHandler.sendEmptyMessage(START_FACE_DETECTION);
+        }
+
+        @Override
+        public void stopFaceDetection() {
+            mCameraHandler.sendEmptyMessage(STOP_FACE_DETECTION);
+        }
+
+        @Override
+        public void setErrorCallback(ErrorCallback cb) {
+            mCameraHandler.obtainMessage(SET_ERROR_CALLBACK, cb).sendToTarget();
+        }
+
+        @Override
+        public void setParameters(Parameters params) {
+            if (params == null) {
+                Log.v(TAG, "null parameters in setParameters()");
+                return;
+            }
+            mCameraHandler.obtainMessage(SET_PARAMETERS, params.flatten())
+                    .sendToTarget();
+        }
+
+        @Override
+        public Parameters getParameters() {
+            mCameraHandler.sendEmptyMessage(GET_PARAMETERS);
+            mCameraHandler.waitDone();
+            return mParameters;
+        }
+
+        @Override
+        public void refreshParameters() {
+            mCameraHandler.sendEmptyMessage(REFRESH_PARAMETERS);
+        }
+
+        @Override
+        public void enableShutterSound(boolean enable) {
+            mCameraHandler.obtainMessage(
+                    ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget();
+        }
+    }
+
+    /**
+     * A helper class to forward AutoFocusCallback to another thread.
+     */
+    private static class AFCallbackForward implements AutoFocusCallback {
+        private final Handler mHandler;
+        private final CameraProxy mCamera;
+        private final CameraAFCallback mCallback;
+
+        /**
+         * Returns a new instance of {@link AFCallbackForward}.
+         *
+         * @param handler The handler in which the callback will be invoked in.
+         * @param camera  The {@link CameraProxy} which the callback is from.
+         * @param cb      The callback to be invoked.
+         * @return        The instance of the {@link AFCallbackForward},
+         *                or null if any parameter is null.
+         */
+        public static AFCallbackForward getNewInstance(
+                Handler handler, CameraProxy camera, CameraAFCallback cb) {
+            if (handler == null || camera == null || cb == null) return null;
+            return new AFCallbackForward(handler, camera, cb);
+        }
+
+        private AFCallbackForward(
+                Handler h, CameraProxy camera, CameraAFCallback cb) {
+            mHandler = h;
+            mCamera = camera;
+            mCallback = cb;
+        }
+
+        @Override
+        public void onAutoFocus(final boolean b, Camera camera) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onAutoFocus(b, mCamera);
+                }
+            });
+        }
+    }
+
+    /** A helper class to forward AutoFocusMoveCallback to another thread. */
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private static class AFMoveCallbackForward implements AutoFocusMoveCallback {
+        private final Handler mHandler;
+        private final CameraAFMoveCallback mCallback;
+        private final CameraProxy mCamera;
+
+        /**
+         * Returns a new instance of {@link AFMoveCallbackForward}.
+         *
+         * @param handler The handler in which the callback will be invoked in.
+         * @param camera  The {@link CameraProxy} which the callback is from.
+         * @param cb      The callback to be invoked.
+         * @return        The instance of the {@link AFMoveCallbackForward},
+         *                or null if any parameter is null.
+         */
+        public static AFMoveCallbackForward getNewInstance(
+                Handler handler, CameraProxy camera, CameraAFMoveCallback cb) {
+            if (handler == null || camera == null || cb == null) return null;
+            return new AFMoveCallbackForward(handler, camera, cb);
+        }
+
+        private AFMoveCallbackForward(
+                Handler h, CameraProxy camera, CameraAFMoveCallback cb) {
+            mHandler = h;
+            mCamera = camera;
+            mCallback = cb;
+        }
+
+        @Override
+        public void onAutoFocusMoving(
+                final boolean moving, android.hardware.Camera camera) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onAutoFocusMoving(moving, mCamera);
+                }
+            });
+        }
+    }
+
+    /**
+     * A helper class to forward ShutterCallback to to another thread.
+     */
+    private static class ShutterCallbackForward implements ShutterCallback {
+        private final Handler mHandler;
+        private final CameraShutterCallback mCallback;
+        private final CameraProxy mCamera;
+
+        /**
+         * Returns a new instance of {@link ShutterCallbackForward}.
+         *
+         * @param handler The handler in which the callback will be invoked in.
+         * @param camera  The {@link CameraProxy} which the callback is from.
+         * @param cb      The callback to be invoked.
+         * @return        The instance of the {@link ShutterCallbackForward},
+         *                or null if any parameter is null.
+         */
+        public static ShutterCallbackForward getNewInstance(
+                Handler handler, CameraProxy camera, CameraShutterCallback cb) {
+            if (handler == null || camera == null || cb == null) return null;
+            return new ShutterCallbackForward(handler, camera, cb);
+        }
+
+        private ShutterCallbackForward(
+                Handler h, CameraProxy camera, CameraShutterCallback cb) {
+            mHandler = h;
+            mCamera = camera;
+            mCallback = cb;
+        }
+
+        @Override
+        public void onShutter() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onShutter(mCamera);
+                }
+            });
+        }
+    }
+
+    /**
+     * A helper class to forward PictureCallback to another thread.
+     */
+    private static class PictureCallbackForward implements PictureCallback {
+        private final Handler mHandler;
+        private final CameraPictureCallback mCallback;
+        private final CameraProxy mCamera;
+
+        /**
+         * Returns a new instance of {@link PictureCallbackForward}.
+         *
+         * @param handler The handler in which the callback will be invoked in.
+         * @param camera  The {@link CameraProxy} which the callback is from.
+         * @param cb      The callback to be invoked.
+         * @return        The instance of the {@link PictureCallbackForward},
+         *                or null if any parameters is null.
+         */
+        public static PictureCallbackForward getNewInstance(
+                Handler handler, CameraProxy camera, CameraPictureCallback cb) {
+            if (handler == null || camera == null || cb == null) return null;
+            return new PictureCallbackForward(handler, camera, cb);
+        }
+
+        private PictureCallbackForward(
+                Handler h, CameraProxy camera, CameraPictureCallback cb) {
+            mHandler = h;
+            mCamera = camera;
+            mCallback = cb;
+        }
+
+        @Override
+        public void onPictureTaken(
+                final byte[] data, android.hardware.Camera camera) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onPictureTaken(data, mCamera);
+                }
+            });
+        }
+    }
+
+    /**
+     * A helper class to forward PreviewCallback to another thread.
+     */
+    private static class PreviewCallbackForward implements PreviewCallback {
+        private final Handler mHandler;
+        private final CameraPreviewDataCallback mCallback;
+        private final CameraProxy mCamera;
+
+        /**
+         * Returns a new instance of {@link PreviewCallbackForward}.
+         *
+         * @param handler The handler in which the callback will be invoked in.
+         * @param camera  The {@link CameraProxy} which the callback is from.
+         * @param cb      The callback to be invoked.
+         * @return        The instance of the {@link PreviewCallbackForward},
+         *                or null if any parameters is null.
+         */
+        public static PreviewCallbackForward getNewInstance(
+                Handler handler, CameraProxy camera, CameraPreviewDataCallback cb) {
+            if (handler == null || camera == null || cb == null) return null;
+            return new PreviewCallbackForward(handler, camera, cb);
+        }
+
+        private PreviewCallbackForward(
+                Handler h, CameraProxy camera, CameraPreviewDataCallback cb) {
+            mHandler = h;
+            mCamera = camera;
+            mCallback = cb;
+        }
+
+        @Override
+        public void onPreviewFrame(
+                final byte[] data, android.hardware.Camera camera) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onPreviewFrame(data, mCamera);
+                }
+            });
+        }
+    }
+
+    private static class FaceDetectionCallbackForward implements FaceDetectionListener {
+        private final Handler mHandler;
+        private final CameraFaceDetectionCallback mCallback;
+        private final CameraProxy mCamera;
+
+        /**
+         * Returns a new instance of {@link FaceDetectionCallbackForward}.
+         *
+         * @param handler The handler in which the callback will be invoked in.
+         * @param camera  The {@link CameraProxy} which the callback is from.
+         * @param cb      The callback to be invoked.
+         * @return        The instance of the {@link FaceDetectionCallbackForward},
+         *                or null if any parameter is null.
+         */
+        public static FaceDetectionCallbackForward getNewInstance(
+                Handler handler, CameraProxy camera, CameraFaceDetectionCallback cb) {
+            if (handler == null || camera == null || cb == null) return null;
+            return new FaceDetectionCallbackForward(handler, camera, cb);
+        }
+
+        private FaceDetectionCallbackForward(
+                Handler h, CameraProxy camera, CameraFaceDetectionCallback cb) {
+            mHandler = h;
+            mCamera = camera;
+            mCallback = cb;
+        }
+
+        @Override
+        public void onFaceDetection(
+                final Camera.Face[] faces, Camera camera) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onFaceDetection(faces, mCamera);
+                }
+            });
+        }
+    }
+}
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
new file mode 100644
index 0000000..7f71d5f
--- /dev/null
+++ b/src/com/android/camera/CameraActivity.java
@@ -0,0 +1,571 @@
+/*
+ * Copyright (C) 2012 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.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ImageView;
+
+import com.android.camera.data.CameraDataAdapter;
+import com.android.camera.data.CameraPreviewData;
+import com.android.camera.data.FixedFirstDataAdapter;
+import com.android.camera.data.FixedLastDataAdapter;
+import com.android.camera.data.LocalData;
+import com.android.camera.data.LocalDataAdapter;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.FilmStripView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.RefocusHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+public class CameraActivity extends Activity
+    implements CameraSwitchListener {
+
+    private static final String TAG = "CAM_Activity";
+
+    private static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE =
+            "android.media.action.STILL_IMAGE_CAMERA_SECURE";
+    public static final String ACTION_IMAGE_CAPTURE_SECURE =
+            "android.media.action.IMAGE_CAPTURE_SECURE";
+
+    // The intent extra for camera from secure lock screen. True if the gallery
+    // should only show newly captured pictures. sSecureAlbumId does not
+    // increment. This is used when switching between camera, camcorder, and
+    // panorama. If the extra is not set, it is in the normal camera mode.
+    public static final String SECURE_CAMERA_EXTRA = "secure_camera";
+
+    /** This data adapter is used by FilmStirpView. */
+    private LocalDataAdapter mDataAdapter;
+    /** This data adapter represents the real local camera data. */
+    private LocalDataAdapter mWrappedDataAdapter;
+
+    private PanoramaStitchingManager mPanoramaManager;
+    private int mCurrentModuleIndex;
+    private CameraModule mCurrentModule;
+    private View mRootView;
+    private FilmStripView mFilmStripView;
+    private int mResultCodeForTesting;
+    private Intent mResultDataForTesting;
+    private OnScreenHint mStorageHint;
+    private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD;
+    private boolean mAutoRotateScreen;
+    private boolean mSecureCamera;
+    // This is a hack to speed up the start of SecureCamera.
+    private static boolean sFirstStartAfterScreenOn = true;
+    private boolean mShowCameraPreview;
+    private int mLastRawOrientation;
+    private MyOrientationEventListener mOrientationListener;
+    private Handler mMainHandler;
+    private PanoramaViewHelper mPanoramaViewHelper;
+    private CameraPreviewData mCameraPreviewData;
+
+    private class MyOrientationEventListener
+        extends OrientationEventListener {
+        public MyOrientationEventListener(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onOrientationChanged(int orientation) {
+            // We keep the last known orientation. So if the user first orient
+            // the camera then point the camera to floor or sky, we still have
+            // the correct orientation.
+            if (orientation == ORIENTATION_UNKNOWN) return;
+            mLastRawOrientation = orientation;
+            mCurrentModule.onOrientationChanged(orientation);
+        }
+    }
+
+    private MediaSaveService mMediaSaveService;
+    private ServiceConnection mConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName className, IBinder b) {
+                mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService();
+                mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+            }
+            @Override
+            public void onServiceDisconnected(ComponentName className) {
+                mMediaSaveService = null;
+            }};
+
+    // close activity when screen turns off
+    private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            finish();
+        }
+    };
+
+    private static BroadcastReceiver sScreenOffReceiver;
+    private static class ScreenOffReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            sFirstStartAfterScreenOn = true;
+        }
+    }
+
+    public static boolean isFirstStartAfterScreenOn() {
+        return sFirstStartAfterScreenOn;
+    }
+
+    public static void resetFirstStartAfterScreenOn() {
+        sFirstStartAfterScreenOn = false;
+    }
+
+    private FilmStripView.Listener mFilmStripListener = new FilmStripView.Listener() {
+            @Override
+            public void onDataPromoted(int dataID) {
+                removeData(dataID);
+            }
+
+            @Override
+            public void onDataDemoted(int dataID) {
+                removeData(dataID);
+            }
+
+            @Override
+            public void onDataFullScreenChange(int dataID, boolean full) {
+            }
+
+            @Override
+            public void onSwitchMode(boolean toCamera) {
+                mCurrentModule.onSwitchMode(toCamera);
+            }
+        };
+
+    private Runnable mDeletionRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mDataAdapter.executeDeletion(CameraActivity.this);
+            }
+        };
+
+    private ImageTaskManager.TaskListener mStitchingListener =
+            new ImageTaskManager.TaskListener() {
+                @Override
+                public void onTaskQueued(String filePath, Uri imageUri) {
+                }
+
+                @Override
+                public void onTaskDone(String filePath, Uri imageUri) {
+                }
+
+                @Override
+                public void onTaskProgress(
+                        String filePath, Uri imageUri, int progress) {
+                }
+            };
+
+    public MediaSaveService getMediaSaveService() {
+        return mMediaSaveService;
+    }
+
+    public void notifyNewMedia(Uri uri) {
+        ContentResolver cr = getContentResolver();
+        String mimeType = cr.getType(uri);
+        if (mimeType.startsWith("video/")) {
+            sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO, uri));
+            mDataAdapter.addNewVideo(cr, uri);
+        } else if (mimeType.startsWith("image/")) {
+            Util.broadcastNewPicture(this, uri);
+            mDataAdapter.addNewPhoto(cr, uri);
+        } else {
+            android.util.Log.w(TAG, "Unknown new media with MIME type:"
+                    + mimeType + ", uri:" + uri);
+        }
+    }
+
+    private void removeData(int dataID) {
+        mDataAdapter.removeData(CameraActivity.this, dataID);
+        mMainHandler.removeCallbacks(mDeletionRunnable);
+        mMainHandler.postDelayed(mDeletionRunnable, 3000);
+    }
+
+    private void bindMediaSaveService() {
+        Intent intent = new Intent(this, MediaSaveService.class);
+        startService(intent);  // start service before binding it so the
+                               // service won't be killed if we unbind it.
+        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    private void unbindMediaSaveService() {
+        if (mMediaSaveService != null) {
+            mMediaSaveService.setListener(null);
+        }
+        if (mConnection != null) {
+            unbindService(mConnection);
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle state) {
+        super.onCreate(state);
+        setContentView(R.layout.camera_filmstrip);
+        if (ApiHelper.HAS_ROTATION_ANIMATION) {
+            setRotationAnimation();
+        }
+        // Check if this is in the secure camera mode.
+        Intent intent = getIntent();
+        String action = intent.getAction();
+        if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action)
+                || ACTION_IMAGE_CAPTURE_SECURE.equals(action)) {
+            mSecureCamera = true;
+        } else {
+            mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false);
+        }
+
+        if (mSecureCamera) {
+            // Change the window flags so that secure camera can show when locked
+            Window win = getWindow();
+            WindowManager.LayoutParams params = win.getAttributes();
+            params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+            win.setAttributes(params);
+
+            // Filter for screen off so that we can finish secure camera activity
+            // when screen is off.
+            IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+            registerReceiver(mScreenOffReceiver, filter);
+            // TODO: This static screen off event receiver is a workaround to the
+            // double onResume() invocation (onResume->onPause->onResume). We should
+            // find a better solution to this.
+            if (sScreenOffReceiver == null) {
+                sScreenOffReceiver = new ScreenOffReceiver();
+                registerReceiver(sScreenOffReceiver, filter);
+            }
+        }
+        mPanoramaManager = new PanoramaStitchingManager(CameraActivity.this);
+        mPanoramaManager.addTaskListener(mStitchingListener);
+        LayoutInflater inflater = getLayoutInflater();
+        View rootLayout = inflater.inflate(R.layout.camera, null, false);
+        mRootView = rootLayout.findViewById(R.id.camera_app_root);
+        mCameraPreviewData = new CameraPreviewData(rootLayout,
+                FilmStripView.ImageData.SIZE_FULL,
+                FilmStripView.ImageData.SIZE_FULL);
+        // Put a CameraPreviewData at the first position.
+        mWrappedDataAdapter = new FixedFirstDataAdapter(
+                new CameraDataAdapter(new ColorDrawable(
+                        getResources().getColor(R.color.photo_placeholder))),
+                mCameraPreviewData);
+        mFilmStripView = (FilmStripView) findViewById(R.id.filmstrip_view);
+        mFilmStripView.setViewGap(
+                getResources().getDimensionPixelSize(R.dimen.camera_film_strip_gap));
+        mPanoramaViewHelper = new PanoramaViewHelper(this);
+        mPanoramaViewHelper.onCreate();
+        mFilmStripView.setPanoramaViewHelper(mPanoramaViewHelper);
+        // Set up the camera preview first so the preview shows up ASAP.
+        mFilmStripView.setListener(mFilmStripListener);
+        mCurrentModule = new PhotoModule();
+        mCurrentModule.init(this, mRootView);
+        mOrientationListener = new MyOrientationEventListener(this);
+        mMainHandler = new Handler(getMainLooper());
+        bindMediaSaveService();
+
+        if (!mSecureCamera) {
+            mDataAdapter = mWrappedDataAdapter;
+            mDataAdapter.requestLoad(getContentResolver());
+        } else {
+            // Put a lock placeholder as the last image by setting its date to 0.
+            ImageView v = (ImageView) getLayoutInflater().inflate(
+                    R.layout.secure_album_placeholder, null);
+            mDataAdapter = new FixedLastDataAdapter(
+                    mWrappedDataAdapter,
+                    new LocalData.LocalViewData(
+                            v,
+                            v.getDrawable().getIntrinsicWidth(),
+                            v.getDrawable().getIntrinsicHeight(),
+                            0, 0));
+            // Flush out all the original data.
+            mDataAdapter.flush();
+        }
+        mFilmStripView.setDataAdapter(mDataAdapter);
+    }
+
+    private void setRotationAnimation() {
+        int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
+        rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
+        Window win = getWindow();
+        WindowManager.LayoutParams winParams = win.getAttributes();
+        winParams.rotationAnimation = rotationAnimation;
+        win.setAttributes(winParams);
+    }
+
+    @Override
+    public void onUserInteraction() {
+        super.onUserInteraction();
+        mCurrentModule.onUserInteraction();
+    }
+
+    @Override
+    public void onPause() {
+        mOrientationListener.disable();
+        mCurrentModule.onPauseBeforeSuper();
+        super.onPause();
+        mCurrentModule.onPauseAfterSuper();
+    }
+
+    @Override
+    public void onResume() {
+        if (Settings.System.getInt(getContentResolver(),
+                Settings.System.ACCELEROMETER_ROTATION, 0) == 0) {// auto-rotate off
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+            mAutoRotateScreen = false;
+        } else {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
+            mAutoRotateScreen = true;
+        }
+        mOrientationListener.enable();
+        mCurrentModule.onResumeBeforeSuper();
+        super.onResume();
+        mCurrentModule.onResumeAfterSuper();
+
+        setSwipingEnabled(true);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        mPanoramaViewHelper.onStart();
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        mPanoramaViewHelper.onStop();
+    }
+
+    @Override
+    public void onDestroy() {
+        unbindMediaSaveService();
+        if (mSecureCamera) unregisterReceiver(mScreenOffReceiver);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        mCurrentModule.onConfigurationChanged(config);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mCurrentModule.onKeyDown(keyCode, event)) return true;
+        // Prevent software keyboard or voice search from showing up.
+        if (keyCode == KeyEvent.KEYCODE_SEARCH
+                || keyCode == KeyEvent.KEYCODE_MENU) {
+            if (event.isLongPress()) return true;
+        }
+        if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) {
+            return true;
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (mCurrentModule.onKeyUp(keyCode, event)) return true;
+        if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) {
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    public boolean isAutoRotateScreen() {
+        return mAutoRotateScreen;
+    }
+
+    protected void updateStorageSpace() {
+        mStorageSpace = Storage.getAvailableSpace();
+    }
+
+    protected long getStorageSpace() {
+        return mStorageSpace;
+    }
+
+    protected void updateStorageSpaceAndHint() {
+        updateStorageSpace();
+        updateStorageHint(mStorageSpace);
+    }
+
+    protected void updateStorageHint() {
+        updateStorageHint(mStorageSpace);
+    }
+
+    protected boolean updateStorageHintOnResume() {
+        return true;
+    }
+
+    protected void updateStorageHint(long storageSpace) {
+        String message = null;
+        if (storageSpace == Storage.UNAVAILABLE) {
+            message = getString(R.string.no_storage);
+        } else if (storageSpace == Storage.PREPARING) {
+            message = getString(R.string.preparing_sd);
+        } else if (storageSpace == Storage.UNKNOWN_SIZE) {
+            message = getString(R.string.access_sd_fail);
+        } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD) {
+            message = getString(R.string.spaceIsLow_content);
+        }
+
+        if (message != null) {
+            if (mStorageHint == null) {
+                mStorageHint = OnScreenHint.makeText(this, message);
+            } else {
+                mStorageHint.setText(message);
+            }
+            mStorageHint.show();
+        } else if (mStorageHint != null) {
+            mStorageHint.cancel();
+            mStorageHint = null;
+        }
+    }
+
+    protected void setResultEx(int resultCode) {
+        mResultCodeForTesting = resultCode;
+        setResult(resultCode);
+    }
+
+    protected void setResultEx(int resultCode, Intent data) {
+        mResultCodeForTesting = resultCode;
+        mResultDataForTesting = data;
+        setResult(resultCode, data);
+    }
+
+    public int getResultCode() {
+        return mResultCodeForTesting;
+    }
+
+    public Intent getResultData() {
+        return mResultDataForTesting;
+    }
+
+    public boolean isSecureCamera() {
+        return mSecureCamera;
+    }
+
+    @Override
+    public void onCameraSelected(int i) {
+        if (mCurrentModuleIndex == i) return;
+
+        CameraHolder.instance().keep();
+        closeModule(mCurrentModule);
+        mCurrentModuleIndex = i;
+        switch (i) {
+            case CameraSwitcher.VIDEO_MODULE_INDEX:
+                mCurrentModule = new VideoModule();
+                break;
+            case CameraSwitcher.PHOTO_MODULE_INDEX:
+                mCurrentModule = new PhotoModule();
+                break;
+            case CameraSwitcher.LIGHTCYCLE_MODULE_INDEX:
+                mCurrentModule = LightCycleHelper.createPanoramaModule();
+                break;
+            case CameraSwitcher.REFOCUS_MODULE_INDEX:
+                mCurrentModule = RefocusHelper.createRefocusModule();
+                break;
+           default:
+               break;
+        }
+
+        openModule(mCurrentModule);
+        mCurrentModule.onOrientationChanged(mLastRawOrientation);
+        if (mMediaSaveService != null) {
+            mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+        }
+    }
+
+    private void openModule(CameraModule module) {
+        module.init(this, mRootView);
+        module.onResumeBeforeSuper();
+        module.onResumeAfterSuper();
+    }
+
+    private void closeModule(CameraModule module) {
+        module.onPauseBeforeSuper();
+        module.onPauseAfterSuper();
+        ((ViewGroup) mRootView).removeAllViews();
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+    }
+
+    public void setSwipingEnabled(boolean enable) {
+        mCameraPreviewData.lockPreview(!enable);
+    }
+
+    // Accessor methods for getting latency times used in performance testing
+    public long getAutoFocusTime() {
+        return (mCurrentModule instanceof PhotoModule) ?
+                ((PhotoModule) mCurrentModule).mAutoFocusTime : -1;
+    }
+
+    public long getShutterLag() {
+        return (mCurrentModule instanceof PhotoModule) ?
+                ((PhotoModule) mCurrentModule).mShutterLag : -1;
+    }
+
+    public long getShutterToPictureDisplayedTime() {
+        return (mCurrentModule instanceof PhotoModule) ?
+                ((PhotoModule) mCurrentModule).mShutterToPictureDisplayedTime : -1;
+    }
+
+    public long getPictureDisplayedToJpegCallbackTime() {
+        return (mCurrentModule instanceof PhotoModule) ?
+                ((PhotoModule) mCurrentModule).mPictureDisplayedToJpegCallbackTime : -1;
+    }
+
+    public long getJpegCallbackFinishTime() {
+        return (mCurrentModule instanceof PhotoModule) ?
+                ((PhotoModule) mCurrentModule).mJpegCallbackFinishTime : -1;
+    }
+
+    public long getCaptureStartTime() {
+        return (mCurrentModule instanceof PhotoModule) ?
+                ((PhotoModule) mCurrentModule).mCaptureStartTime : -1;
+    }
+
+    public boolean isRecording() {
+        return (mCurrentModule instanceof VideoModule) ?
+                ((VideoModule) mCurrentModule).isRecording() : false;
+    }
+}
diff --git a/src/com/android/camera/CameraBackupAgent.java b/src/com/android/camera/CameraBackupAgent.java
new file mode 100644
index 0000000..30ba212
--- /dev/null
+++ b/src/com/android/camera/CameraBackupAgent.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 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.backup.BackupAgentHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.Context;
+
+public class CameraBackupAgent extends BackupAgentHelper {
+    private static final String CAMERA_BACKUP_KEY = "camera_prefs";
+
+    public void onCreate () {
+        Context context = getApplicationContext();
+        String prefNames[] = ComboPreferences.getSharedPreferencesNames(context);
+
+        addHelper(CAMERA_BACKUP_KEY, new SharedPreferencesBackupHelper(context, prefNames));
+    }
+}
diff --git a/src/com/android/camera/CameraButtonIntentReceiver.java b/src/com/android/camera/CameraButtonIntentReceiver.java
new file mode 100644
index 0000000..a65942d
--- /dev/null
+++ b/src/com/android/camera/CameraButtonIntentReceiver.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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * {@code CameraButtonIntentReceiver} is invoked when the camera button is
+ * long-pressed.
+ *
+ * It is declared in {@code AndroidManifest.xml} to receive the
+ * {@code android.intent.action.CAMERA_BUTTON} intent.
+ *
+ * After making sure we can use the camera hardware, it starts the Camera
+ * activity.
+ */
+public class CameraButtonIntentReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        // Try to get the camera hardware
+        CameraHolder holder = CameraHolder.instance();
+        ComboPreferences pref = new ComboPreferences(context);
+        int cameraId = CameraSettings.readPreferredCameraId(pref);
+        if (holder.tryOpen(cameraId) == null) return;
+
+        // We are going to launch the camera, so hold the camera for later use
+        holder.keep();
+        holder.release();
+        Intent i = new Intent(Intent.ACTION_MAIN);
+        i.setClass(context, CameraActivity.class);
+        i.addCategory(Intent.CATEGORY_LAUNCHER);
+        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        context.startActivity(i);
+    }
+}
diff --git a/src/com/android/camera/CameraDisabledException.java b/src/com/android/camera/CameraDisabledException.java
new file mode 100644
index 0000000..512809b
--- /dev/null
+++ b/src/com/android/camera/CameraDisabledException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * This class represents the condition that device policy manager has disabled
+ * the camera.
+ */
+public class CameraDisabledException extends Exception {
+}
diff --git a/src/com/android/camera/CameraErrorCallback.java b/src/com/android/camera/CameraErrorCallback.java
new file mode 100644
index 0000000..22f800e
--- /dev/null
+++ b/src/com/android/camera/CameraErrorCallback.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2010 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.util.Log;
+
+public class CameraErrorCallback
+        implements android.hardware.Camera.ErrorCallback {
+    private static final String TAG = "CameraErrorCallback";
+
+    @Override
+    public void onError(int error, android.hardware.Camera camera) {
+        Log.e(TAG, "Got camera error callback. error=" + error);
+        if (error == android.hardware.Camera.CAMERA_ERROR_SERVER_DIED) {
+            // We are not sure about the current state of the app (in preview or
+            // snapshot or recording). Closing the app is better than creating a
+            // new Camera object.
+            throw new RuntimeException("Media server died.");
+        }
+    }
+}
diff --git a/src/com/android/camera/CameraHardwareException.java b/src/com/android/camera/CameraHardwareException.java
new file mode 100644
index 0000000..8209055
--- /dev/null
+++ b/src/com/android/camera/CameraHardwareException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+/**
+ * This class represents the condition that we cannot open the camera hardware
+ * successfully. For example, another process is using the camera.
+ */
+public class CameraHardwareException extends Exception {
+
+    public CameraHardwareException(Throwable t) {
+        super(t);
+    }
+}
diff --git a/src/com/android/camera/CameraHolder.java b/src/com/android/camera/CameraHolder.java
new file mode 100644
index 0000000..d913df7
--- /dev/null
+++ b/src/com/android/camera/CameraHolder.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import static com.android.camera.Util.Assert;
+
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.camera.CameraManager.CameraProxy;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+
+/**
+ * The class is used to hold an {@code android.hardware.Camera} instance.
+ *
+ * <p>The {@code open()} and {@code release()} calls are similar to the ones
+ * in {@code android.hardware.Camera}. The difference is if {@code keep()} is
+ * called before {@code release()}, CameraHolder will try to hold the {@code
+ * android.hardware.Camera} instance for a while, so if {@code open()} is
+ * called soon after, we can avoid the cost of {@code open()} in {@code
+ * android.hardware.Camera}.
+ *
+ * <p>This is used in switching between different modules.
+ */
+public class CameraHolder {
+    private static final String TAG = "CameraHolder";
+    private static final int KEEP_CAMERA_TIMEOUT = 3000; // 3 seconds
+    private CameraProxy mCameraDevice;
+    private long mKeepBeforeTime;  // Keep the Camera before this time.
+    private final Handler mHandler;
+    private boolean mCameraOpened;  // true if camera is opened
+    private final int mNumberOfCameras;
+    private int mCameraId = -1;  // current camera id
+    private int mBackCameraId = -1;
+    private int mFrontCameraId = -1;
+    private final CameraInfo[] mInfo;
+    private static CameraProxy mMockCamera[];
+    private static CameraInfo mMockCameraInfo[];
+
+    /* Debug double-open issue */
+    private static final boolean DEBUG_OPEN_RELEASE = true;
+    private static class OpenReleaseState {
+        long time;
+        int id;
+        String device;
+        String[] stack;
+    }
+    private static ArrayList<OpenReleaseState> sOpenReleaseStates =
+            new ArrayList<OpenReleaseState>();
+    private static SimpleDateFormat sDateFormat = new SimpleDateFormat(
+            "yyyy-MM-dd HH:mm:ss.SSS");
+
+    private static synchronized void collectState(int id, CameraProxy device) {
+        OpenReleaseState s = new OpenReleaseState();
+        s.time = System.currentTimeMillis();
+        s.id = id;
+        if (device == null) {
+            s.device = "(null)";
+        } else {
+            s.device = device.toString();
+        }
+
+        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+        String[] lines = new String[stack.length];
+        for (int i = 0; i < stack.length; i++) {
+            lines[i] = stack[i].toString();
+        }
+        s.stack = lines;
+
+        if (sOpenReleaseStates.size() > 10) {
+            sOpenReleaseStates.remove(0);
+        }
+        sOpenReleaseStates.add(s);
+    }
+
+    private static synchronized void dumpStates() {
+        for (int i = sOpenReleaseStates.size() - 1; i >= 0; i--) {
+            OpenReleaseState s = sOpenReleaseStates.get(i);
+            String date = sDateFormat.format(new Date(s.time));
+            Log.d(TAG, "State " + i + " at " + date);
+            Log.d(TAG, "mCameraId = " + s.id + ", mCameraDevice = " + s.device);
+            Log.d(TAG, "Stack:");
+            for (int j = 0; j < s.stack.length; j++) {
+                Log.d(TAG, "  " + s.stack[j]);
+            }
+        }
+    }
+
+    // We store the camera parameters when we actually open the device,
+    // so we can restore them in the subsequent open() requests by the user.
+    // This prevents the parameters set by PhotoModule used by VideoModule
+    // inadvertently.
+    private Parameters mParameters;
+
+    // Use a singleton.
+    private static CameraHolder sHolder;
+    public static synchronized CameraHolder instance() {
+        if (sHolder == null) {
+            sHolder = new CameraHolder();
+        }
+        return sHolder;
+    }
+
+    private static final int RELEASE_CAMERA = 1;
+    private class MyHandler extends Handler {
+        MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch(msg.what) {
+                case RELEASE_CAMERA:
+                    synchronized (CameraHolder.this) {
+                        // In 'CameraHolder.open', the 'RELEASE_CAMERA' message
+                        // will be removed if it is found in the queue. However,
+                        // there is a chance that this message has been handled
+                        // before being removed. So, we need to add a check
+                        // here:
+                        if (!mCameraOpened) release();
+                    }
+                    break;
+            }
+        }
+    }
+
+    public static void injectMockCamera(CameraInfo[] info, CameraProxy[] camera) {
+        mMockCameraInfo = info;
+        mMockCamera = camera;
+        sHolder = new CameraHolder();
+    }
+
+    private CameraHolder() {
+        HandlerThread ht = new HandlerThread("CameraHolder");
+        ht.start();
+        mHandler = new MyHandler(ht.getLooper());
+        if (mMockCameraInfo != null) {
+            mNumberOfCameras = mMockCameraInfo.length;
+            mInfo = mMockCameraInfo;
+        } else {
+            mNumberOfCameras = android.hardware.Camera.getNumberOfCameras();
+            mInfo = new CameraInfo[mNumberOfCameras];
+            for (int i = 0; i < mNumberOfCameras; i++) {
+                mInfo[i] = new CameraInfo();
+                android.hardware.Camera.getCameraInfo(i, mInfo[i]);
+            }
+        }
+
+        // get the first (smallest) back and first front camera id
+        for (int i = 0; i < mNumberOfCameras; i++) {
+            if (mBackCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_BACK) {
+                mBackCameraId = i;
+            } else if (mFrontCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_FRONT) {
+                mFrontCameraId = i;
+            }
+        }
+    }
+
+    public int getNumberOfCameras() {
+        return mNumberOfCameras;
+    }
+
+    public CameraInfo[] getCameraInfo() {
+        return mInfo;
+    }
+
+    public synchronized CameraProxy open(int cameraId)
+            throws CameraHardwareException {
+        if (DEBUG_OPEN_RELEASE) {
+            collectState(cameraId, mCameraDevice);
+            if (mCameraOpened) {
+                Log.e(TAG, "double open");
+                dumpStates();
+            }
+        }
+        Assert(!mCameraOpened);
+        if (mCameraDevice != null && mCameraId != cameraId) {
+            mCameraDevice.release();
+            mCameraDevice = null;
+            mCameraId = -1;
+        }
+        if (mCameraDevice == null) {
+            try {
+                Log.v(TAG, "open camera " + cameraId);
+                if (mMockCameraInfo == null) {
+                    mCameraDevice = CameraManagerFactory
+                            .getAndroidCameraManager().cameraOpen(cameraId);
+                } else {
+                    if (mMockCamera == null)
+                        throw new RuntimeException();
+                    mCameraDevice = mMockCamera[cameraId];
+                }
+                mCameraId = cameraId;
+            } catch (RuntimeException e) {
+                Log.e(TAG, "fail to connect Camera", e);
+                throw new CameraHardwareException(e);
+            }
+            mParameters = mCameraDevice.getParameters();
+        } else {
+            try {
+                mCameraDevice.reconnect();
+            } catch (IOException e) {
+                Log.e(TAG, "reconnect failed.");
+                throw new CameraHardwareException(e);
+            }
+            mCameraDevice.setParameters(mParameters);
+        }
+        mCameraOpened = true;
+        mHandler.removeMessages(RELEASE_CAMERA);
+        mKeepBeforeTime = 0;
+        return mCameraDevice;
+    }
+
+    /**
+     * Tries to open the hardware camera. If the camera is being used or
+     * unavailable then return {@code null}.
+     */
+    public synchronized CameraProxy tryOpen(int cameraId) {
+        try {
+            return !mCameraOpened ? open(cameraId) : null;
+        } catch (CameraHardwareException e) {
+            // In eng build, we throw the exception so that test tool
+            // can detect it and report it
+            if ("eng".equals(Build.TYPE)) {
+                throw new RuntimeException(e);
+            }
+            return null;
+        }
+    }
+
+    public synchronized void release() {
+        if (DEBUG_OPEN_RELEASE) {
+            collectState(mCameraId, mCameraDevice);
+        }
+
+        if (mCameraDevice == null) return;
+
+        long now = System.currentTimeMillis();
+        if (now < mKeepBeforeTime) {
+            if (mCameraOpened) {
+                mCameraOpened = false;
+                mCameraDevice.stopPreview();
+            }
+            mHandler.sendEmptyMessageDelayed(RELEASE_CAMERA,
+                    mKeepBeforeTime - now);
+            return;
+        }
+        mCameraOpened = false;
+        mCameraDevice.release();
+        mCameraDevice = null;
+        // We must set this to null because it has a reference to Camera.
+        // Camera has references to the listeners.
+        mParameters = null;
+        mCameraId = -1;
+    }
+
+    public void keep() {
+        keep(KEEP_CAMERA_TIMEOUT);
+    }
+
+    public synchronized void keep(int time) {
+        // We allow mCameraOpened in either state for the convenience of the
+        // calling activity. The activity may not have a chance to call open()
+        // before the user switches to another activity.
+        mKeepBeforeTime = System.currentTimeMillis() + time;
+    }
+
+    public int getBackCameraId() {
+        return mBackCameraId;
+    }
+
+    public int getFrontCameraId() {
+        return mFrontCameraId;
+    }
+}
diff --git a/src/com/android/camera/CameraManager.java b/src/com/android/camera/CameraManager.java
new file mode 100644
index 0000000..90a838c
--- /dev/null
+++ b/src/com/android/camera/CameraManager.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.ErrorCallback;
+import android.hardware.Camera.OnZoomChangeListener;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.view.SurfaceHolder;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.IOException;
+
+/**
+ * An interface which provides possible camera device operations.
+ *
+ * The client should call {@code CameraManager.cameraOpen} to get an instance
+ * of {@link CameraManager.CameraProxy} to control the camera. Classes
+ * implementing this interface should have its own one unique {@code Thread}
+ * other than the main thread for camera operations. Camera device callbacks
+ * are wrapped since the client should not deal with
+ * {@code android.hardware.Camera} directly.
+ *
+ * TODO: provide callback interfaces for:
+ * {@code android.hardware.Camera.ErrorCallback},
+ * {@code android.hardware.Camera.OnZoomChangeListener}, and
+ * {@code android.hardware.Camera.Parameters}.
+ */
+public interface CameraManager {
+
+    /**
+     * An interface which wraps
+     * {@link android.hardware.Camera.AutoFocusCallback}.
+     */
+    public interface CameraAFCallback {
+        public void onAutoFocus(boolean focused, CameraProxy camera);
+    }
+
+    /**
+     * An interface which wraps
+     * {@link android.hardware.Camera.AutoFocusMoveCallback}.
+     */
+    public interface CameraAFMoveCallback {
+        public void onAutoFocusMoving(boolean moving, CameraProxy camera);
+    }
+
+    /**
+     * An interface which wraps
+     * {@link android.hardware.Camera.ShutterCallback}.
+     */
+    public interface CameraShutterCallback {
+        public void onShutter(CameraProxy camera);
+    }
+
+    /**
+     * An interface which wraps
+     * {@link android.hardware.Camera.PictureCallback}.
+     */
+    public interface CameraPictureCallback {
+        public void onPictureTaken(byte[] data, CameraProxy camera);
+    }
+
+    /**
+     * An interface which wraps
+     * {@link android.hardware.Camera.PreviewCallback}.
+     */
+    public interface CameraPreviewDataCallback {
+        public void onPreviewFrame(byte[] data, CameraProxy camera);
+    }
+
+    /**
+     * An interface which wraps
+     * {@link android.hardware.Camera.FaceDetectionListener}.
+     */
+    public interface CameraFaceDetectionCallback {
+        /**
+         * Callback for face detection.
+         *
+         * @param faces   Recognized face in the preview.
+         * @param camera  The camera which the preview image comes from.
+         */
+        public void onFaceDetection(Camera.Face[] faces, CameraProxy camera);
+    }
+
+    /**
+     * Opens the camera of the specified ID synchronously.
+     *
+     * @param cameraId      The camera ID to open.
+     * @return   An instance of {@link CameraProxy} on success. null on failure.
+     */
+    public CameraProxy cameraOpen(int cameraId);
+
+    /**
+     * An interface that takes camera operation requests and post messages to the
+     * camera handler thread. All camera operations made through this interface is
+     * asynchronous by default except those mentioned specifically.
+     */
+    public interface CameraProxy {
+
+        /**
+         * Returns the underlying {@link android.hardware.Camera} object used
+         * by this proxy. This method should only be used when handing the
+         * camera device over to {@link android.media.MediaRecorder} for
+         * recording.
+         */
+        public android.hardware.Camera getCamera();
+
+        /**
+         * Releases the camera device synchronously.
+         * This function must be synchronous so the caller knows exactly when the camera
+         * is released and can continue on.
+         */
+        public void release();
+
+        /**
+         * Reconnects to the camera device.
+         *
+         * @see android.hardware.Camera#reconnect()
+         */
+        public void reconnect() throws IOException;
+
+        /**
+         * Unlocks the camera device.
+         *
+         * @see android.hardware.Camera#unlock()
+         */
+        public void unlock();
+
+        /**
+         * Locks the camera device.
+         * @see android.hardware.Camera#lock()
+         */
+        public void lock();
+
+        /**
+         * Sets the {@link android.graphics.SurfaceTexture} for preview.
+         *
+         * @param surfaceTexture The {@link SurfaceTexture} for preview.
+         */
+        @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+        public void setPreviewTexture(final SurfaceTexture surfaceTexture);
+
+        /**
+         * Sets the {@link android.view.SurfaceHolder} for preview.
+         *
+         * @param surfaceHolder The {@link SurfaceHolder} for preview.
+         */
+        public void setPreviewDisplay(final SurfaceHolder surfaceHolder);
+
+        /**
+         * Starts the camera preview.
+         */
+        public void startPreview();
+
+        /**
+         * Stops the camera preview synchronously.
+         * {@code stopPreview()} must be synchronous to ensure that the caller can
+         * continues to release resources related to camera preview.
+         */
+        public void stopPreview();
+
+        /**
+         * Sets the callback for preview data.
+         *
+         * @param handler    handler in which the callback was handled.
+         * @param cb         The callback to be invoked when the preview data is available.
+         * @see  android.hardware.Camera#setPreviewCallback(android.hardware.Camera.PreviewCallback)
+         */
+        public void setPreviewDataCallback(Handler handler, CameraPreviewDataCallback cb);
+
+        /**
+         * Sets the callback for preview data.
+         *
+         * @param handler The handler in which the callback will be invoked.
+         * @param cb      The callback to be invoked when the preview data is available.
+         * @see android.hardware.Camera#setPreviewCallbackWithBuffer(android.hardware.Camera.PreviewCallback)
+         */
+        public void setPreviewDataCallbackWithBuffer(Handler handler, CameraPreviewDataCallback cb);
+
+        /**
+         * Adds buffer for the preview callback.
+         *
+         * @param callbackBuffer The buffer allocated for the preview data.
+         */
+        public void addCallbackBuffer(byte[] callbackBuffer);
+
+        /**
+         * Starts the auto-focus process. The result will be returned through the callback.
+         *
+         * @param handler The handler in which the callback will be invoked.
+         * @param cb      The auto-focus callback.
+         */
+        public void autoFocus(Handler handler, CameraAFCallback cb);
+
+        /**
+         * Cancels the auto-focus process.
+         */
+        public void cancelAutoFocus();
+
+        /**
+         * Sets the auto-focus callback
+         *
+         * @param handler The handler in which the callback will be invoked.
+         * @param cb      The callback to be invoked when the preview data is available.
+         */
+        @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+        public void setAutoFocusMoveCallback(Handler handler, CameraAFMoveCallback cb);
+
+        /**
+         * Instrument the camera to take a picture.
+         *
+         * @param handler   The handler in which the callback will be invoked.
+         * @param shutter   The callback for shutter action, may be null.
+         * @param raw       The callback for uncompressed data, may be null.
+         * @param postview  The callback for postview image data, may be null.
+         * @param jpeg      The callback for jpeg image data, may be null.
+         * @see android.hardware.Camera#takePicture(
+         *         android.hardware.Camera.ShutterCallback,
+         *         android.hardware.Camera.PictureCallback,
+         *         android.hardware.Camera.PictureCallback)
+         */
+        public void takePicture(
+                Handler handler,
+                CameraShutterCallback shutter,
+                CameraPictureCallback raw,
+                CameraPictureCallback postview,
+                CameraPictureCallback jpeg);
+
+        /**
+         * Sets the display orientation for camera to adjust the preview orientation.
+         *
+         * @param degrees The rotation in degrees. Should be 0, 90, 180 or 270.
+         */
+        public void setDisplayOrientation(int degrees);
+
+        /**
+         * Sets the listener for zoom change.
+         *
+         * @param listener The listener.
+         */
+        public void setZoomChangeListener(OnZoomChangeListener listener);
+
+        /**
+         * Sets the face detection listener.
+         *
+         * @param handler  The handler in which the callback will be invoked.
+         * @param callback The callback for face detection results.
+         */
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        public void setFaceDetectionCallback(Handler handler, CameraFaceDetectionCallback callback);
+
+        /**
+         * Starts the face detection.
+         */
+        public void startFaceDetection();
+
+        /**
+         * Stops the face detection.
+         */
+        public void stopFaceDetection();
+
+        /**
+         * Registers an error callback.
+         *
+         * @param cb The error callback.
+         * @see android.hardware.Camera#setErrorCallback(android.hardware.Camera.ErrorCallback)
+         */
+        public void setErrorCallback(ErrorCallback cb);
+
+        /**
+         * Sets the camera parameters.
+         *
+         * @param params The camera parameters to use.
+         */
+        public void setParameters(Parameters params);
+
+        /**
+         * Gets the current camera parameters synchronously. This method is
+         * synchronous since the caller has to wait for the camera to return
+         * the parameters. If the parameters are already cached, it returns
+         * immediately.
+         */
+        public Parameters getParameters();
+
+        /**
+         * Forces {@code CameraProxy} to update the cached version of the camera
+         * parameters regardless of the dirty bit.
+         */
+        public void refreshParameters();
+
+        /**
+         * Enables/Disables the camera shutter sound.
+         *
+         * @param enable   {@code true} to enable the shutter sound,
+         *                 {@code false} to disable it.
+         */
+        public void enableShutterSound(boolean enable);
+    }
+}
diff --git a/src/com/android/camera/CameraManagerFactory.java b/src/com/android/camera/CameraManagerFactory.java
new file mode 100644
index 0000000..914ebb2
--- /dev/null
+++ b/src/com/android/camera/CameraManagerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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;
+
+/**
+ * A factory class for {@link CameraManager}.
+ */
+public class CameraManagerFactory {
+
+    private static AndroidCameraManagerImpl sAndroidCameraManager;
+
+    /**
+     * Returns the android camera implementation of {@link CameraManager}.
+     *
+     * @return The {@link CameraManager} to control the camera device.
+     */
+    public static synchronized CameraManager getAndroidCameraManager() {
+        if (sAndroidCameraManager == null) {
+            sAndroidCameraManager = new AndroidCameraManagerImpl();
+        }
+        return sAndroidCameraManager;
+    }
+}
diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java
new file mode 100644
index 0000000..bcfe98d
--- /dev/null
+++ b/src/com/android/camera/CameraModule.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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.res.Configuration;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface CameraModule {
+
+    public void init(CameraActivity activity, View frame);
+
+    public void onSwitchMode(boolean toCamera);
+
+    public void onPauseBeforeSuper();
+
+    public void onPauseAfterSuper();
+
+    public void onResumeBeforeSuper();
+
+    public void onResumeAfterSuper();
+
+    public void onConfigurationChanged(Configuration config);
+
+    public void onStop();
+
+    public void installIntentFilter();
+
+    public void onActivityResult(int requestCode, int resultCode, Intent data);
+
+    public boolean onBackPressed();
+
+    public boolean onKeyDown(int keyCode, KeyEvent event);
+
+    public boolean onKeyUp(int keyCode, KeyEvent event);
+
+    public void onSingleTapUp(View view, int x, int y);
+
+    public void onPreviewTextureCopied();
+
+    public void onCaptureTextureCopied();
+
+    public void onUserInteraction();
+
+    public boolean updateStorageHintOnResume();
+
+    public void updateCameraAppView();
+
+    public void onOrientationChanged(int orientation);
+
+    public void onShowSwitcherPopup();
+
+    public void onMediaSaveServiceConnected(MediaSaveService s);
+}
diff --git a/src/com/android/camera/CameraPreference.java b/src/com/android/camera/CameraPreference.java
new file mode 100644
index 0000000..5ddd86d
--- /dev/null
+++ b/src/com/android/camera/CameraPreference.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.R;
+
+/**
+ * The base class of all Preferences used in Camera. The preferences can be
+ * loaded from XML resource by <code>PreferenceInflater</code>.
+ */
+public abstract class CameraPreference {
+
+    private final String mTitle;
+    private SharedPreferences mSharedPreferences;
+    private final Context mContext;
+
+    static public interface OnPreferenceChangedListener {
+        public void onSharedPreferenceChanged();
+        public void onRestorePreferencesClicked();
+        public void onOverriddenPreferencesClicked();
+        public void onCameraPickerClicked(int cameraId);
+    }
+
+    public CameraPreference(Context context, AttributeSet attrs) {
+        mContext = context;
+        TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.CameraPreference, 0, 0);
+        mTitle = a.getString(R.styleable.CameraPreference_title);
+        a.recycle();
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public SharedPreferences getSharedPreferences() {
+        if (mSharedPreferences == null) {
+            mSharedPreferences = ComboPreferences.get(mContext);
+        }
+        return mSharedPreferences;
+    }
+
+    public abstract void reloadValue();
+}
diff --git a/src/com/android/camera/CameraScreenNail.java b/src/com/android/camera/CameraScreenNail.java
new file mode 100644
index 0000000..993a7d3
--- /dev/null
+++ b/src/com/android/camera/CameraScreenNail.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.SurfaceTextureScreenNail;
+
+/*
+ * This is a ScreenNail which can display camera's preview.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+public class CameraScreenNail extends SurfaceTextureScreenNail {
+    private static final String TAG = "CAM_ScreenNail";
+    private static final int ANIM_NONE = 0;
+    // Capture animation is about to start.
+    private static final int ANIM_CAPTURE_START = 1;
+    // Capture animation is running.
+    private static final int ANIM_CAPTURE_RUNNING = 2;
+    // Switch camera animation needs to copy texture.
+    private static final int ANIM_SWITCH_COPY_TEXTURE = 3;
+    // Switch camera animation shows the initial feedback by darkening the
+    // preview.
+    private static final int ANIM_SWITCH_DARK_PREVIEW = 4;
+    // Switch camera animation is waiting for the first frame.
+    private static final int ANIM_SWITCH_WAITING_FIRST_FRAME = 5;
+    // Switch camera animation is about to start.
+    private static final int ANIM_SWITCH_START = 6;
+    // Switch camera animation is running.
+    private static final int ANIM_SWITCH_RUNNING = 7;
+
+    private boolean mVisible;
+    // True if first onFrameAvailable has been called. If screen nail is drawn
+    // too early, it will be all white.
+    private boolean mFirstFrameArrived;
+    private Listener mListener;
+    private final float[] mTextureTransformMatrix = new float[16];
+
+    // Animation.
+    private CaptureAnimManager mCaptureAnimManager;
+    private SwitchAnimManager mSwitchAnimManager = new SwitchAnimManager();
+    private int mAnimState = ANIM_NONE;
+    private RawTexture mAnimTexture;
+    // Some methods are called by GL thread and some are called by main thread.
+    // This protects mAnimState, mVisible, and surface texture. This also makes
+    // sure some code are atomic. For example, requestRender and setting
+    // mAnimState.
+    private Object mLock = new Object();
+
+    private OnFrameDrawnListener mOneTimeFrameDrawnListener;
+    private int mRenderWidth;
+    private int mRenderHeight;
+    // This represents the scaled, uncropped size of the texture
+    // Needed for FaceView
+    private int mUncroppedRenderWidth;
+    private int mUncroppedRenderHeight;
+    private float mScaleX = 1f, mScaleY = 1f;
+    private boolean mFullScreen;
+    private boolean mEnableAspectRatioClamping = false;
+    private boolean mAcquireTexture = false;
+    private final DrawClient mDefaultDraw = new DrawClient() {
+        @Override
+        public void onDraw(GLCanvas canvas, int x, int y, int width, int height) {
+            CameraScreenNail.super.draw(canvas, x, y, width, height);
+        }
+
+        @Override
+        public boolean requiresSurfaceTexture() {
+            return true;
+        }
+
+        @Override
+        public RawTexture copyToTexture(GLCanvas c, RawTexture texture, int w, int h) {
+            // We shouldn't be here since requireSurfaceTexture() returns true.
+            return null;
+        }
+    };
+    private DrawClient mDraw = mDefaultDraw;
+    private float mAlpha = 1f;
+    private Runnable mOnFrameDrawnListener;
+
+    public interface Listener {
+        void requestRender();
+        // Preview has been copied to a texture.
+        void onPreviewTextureCopied();
+
+        void onCaptureTextureCopied();
+    }
+
+    public interface OnFrameDrawnListener {
+        void onFrameDrawn(CameraScreenNail c);
+    }
+
+    public interface DrawClient {
+        void onDraw(GLCanvas canvas, int x, int y, int width, int height);
+
+        boolean requiresSurfaceTexture();
+        // The client should implement this if requiresSurfaceTexture() is false;
+        RawTexture copyToTexture(GLCanvas c, RawTexture texture, int width, int height);
+    }
+
+    public CameraScreenNail(Listener listener, Context ctx) {
+        mListener = listener;
+        mCaptureAnimManager = new CaptureAnimManager(ctx);
+    }
+
+    public void setFullScreen(boolean full) {
+        synchronized (mLock) {
+            mFullScreen = full;
+        }
+    }
+
+    /**
+     * returns the uncropped, but scaled, width of the rendered texture
+     */
+    public int getUncroppedRenderWidth() {
+        return mUncroppedRenderWidth;
+    }
+
+    /**
+     * returns the uncropped, but scaled, width of the rendered texture
+     */
+    public int getUncroppedRenderHeight() {
+        return mUncroppedRenderHeight;
+    }
+
+    @Override
+    public int getWidth() {
+        return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth();
+    }
+
+    @Override
+    public int getHeight() {
+        return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight();
+    }
+
+    private int getTextureWidth() {
+        return super.getWidth();
+    }
+
+    private int getTextureHeight() {
+        return super.getHeight();
+    }
+
+    @Override
+    public void setSize(int w, int h) {
+        super.setSize(w,  h);
+        mEnableAspectRatioClamping = false;
+        if (mRenderWidth == 0) {
+            mRenderWidth = w;
+            mRenderHeight = h;
+        }
+        updateRenderSize();
+    }
+
+    /**
+     * Tells the ScreenNail to override the default aspect ratio scaling
+     * and instead perform custom scaling to basically do a centerCrop instead
+     * of the default centerInside
+     *
+     * Note that calls to setSize will disable this
+     */
+    public void enableAspectRatioClamping() {
+        mEnableAspectRatioClamping = true;
+        updateRenderSize();
+    }
+
+    private void setPreviewLayoutSize(int w, int h) {
+        Log.i(TAG, "preview layout size: "+w+"/"+h);
+        mRenderWidth = w;
+        mRenderHeight = h;
+        updateRenderSize();
+    }
+
+    private void updateRenderSize() {
+        if (!mEnableAspectRatioClamping) {
+            mScaleX = mScaleY = 1f;
+            mUncroppedRenderWidth = getTextureWidth();
+            mUncroppedRenderHeight = getTextureHeight();
+            Log.i(TAG, "aspect ratio clamping disabled");
+            return;
+        }
+
+        float aspectRatio;
+        if (getTextureWidth() > getTextureHeight()) {
+            aspectRatio = (float) getTextureWidth() / (float) getTextureHeight();
+        } else {
+            aspectRatio = (float) getTextureHeight() / (float) getTextureWidth();
+        }
+        float scaledTextureWidth, scaledTextureHeight;
+        if (mRenderWidth > mRenderHeight) {
+            scaledTextureWidth = Math.max(mRenderWidth,
+                    (int) (mRenderHeight * aspectRatio));
+            scaledTextureHeight = Math.max(mRenderHeight,
+                    (int)(mRenderWidth / aspectRatio));
+        } else {
+            scaledTextureWidth = Math.max(mRenderWidth,
+                    (int) (mRenderHeight / aspectRatio));
+            scaledTextureHeight = Math.max(mRenderHeight,
+                    (int) (mRenderWidth * aspectRatio));
+        }
+        mScaleX = mRenderWidth / scaledTextureWidth;
+        mScaleY = mRenderHeight / scaledTextureHeight;
+        mUncroppedRenderWidth = Math.round(scaledTextureWidth);
+        mUncroppedRenderHeight = Math.round(scaledTextureHeight);
+        Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY);
+    }
+
+    public void acquireSurfaceTexture() {
+        synchronized (mLock) {
+            mFirstFrameArrived = false;
+            mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
+            mAcquireTexture = true;
+        }
+        mListener.requestRender();
+    }
+
+    @Override
+    public void releaseSurfaceTexture() {
+        synchronized (mLock) {
+            if (mAcquireTexture) {
+                mAcquireTexture = false;
+                mLock.notifyAll();
+            } else {
+                if (super.getSurfaceTexture() != null) {
+                    super.releaseSurfaceTexture();
+                }
+                mAnimState = ANIM_NONE; // stop the animation
+            }
+        }
+    }
+
+    public void copyTexture() {
+        synchronized (mLock) {
+            mListener.requestRender();
+            mAnimState = ANIM_SWITCH_COPY_TEXTURE;
+        }
+    }
+
+    public void animateSwitchCamera() {
+        Log.v(TAG, "animateSwitchCamera");
+        synchronized (mLock) {
+            if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) {
+                // Do not request render here because camera has been just
+                // started. We do not want to draw black frames.
+                mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME;
+            }
+        }
+    }
+
+    public void animateCapture(int displayRotation) {
+        synchronized (mLock) {
+            mCaptureAnimManager.setOrientation(displayRotation);
+            mCaptureAnimManager.animateFlashAndSlide();
+            mListener.requestRender();
+            mAnimState = ANIM_CAPTURE_START;
+        }
+    }
+
+    public RawTexture getAnimationTexture() {
+        return mAnimTexture;
+    }
+
+    public void animateFlash(int displayRotation) {
+        synchronized (mLock) {
+            mCaptureAnimManager.setOrientation(displayRotation);
+            mCaptureAnimManager.animateFlash();
+            mListener.requestRender();
+            mAnimState = ANIM_CAPTURE_START;
+        }
+    }
+
+    public void animateSlide() {
+        synchronized (mLock) {
+            mCaptureAnimManager.animateSlide();
+            mListener.requestRender();
+        }
+    }
+
+    private void callbackIfNeeded() {
+        if (mOneTimeFrameDrawnListener != null) {
+            mOneTimeFrameDrawnListener.onFrameDrawn(this);
+            mOneTimeFrameDrawnListener = null;
+        }
+    }
+
+    @Override
+    protected void updateTransformMatrix(float[] matrix) {
+        super.updateTransformMatrix(matrix);
+        Matrix.translateM(matrix, 0, .5f, .5f, 0);
+        Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f);
+        Matrix.translateM(matrix, 0, -.5f, -.5f, 0);
+    }
+
+    public void directDraw(GLCanvas canvas, int x, int y, int width, int height) {
+        DrawClient draw;
+        synchronized (mLock) {
+            draw = mDraw;
+        }
+        draw.onDraw(canvas, x, y, width, height);
+    }
+
+    public void setDraw(DrawClient draw) {
+        synchronized (mLock) {
+            if (draw == null) {
+                mDraw = mDefaultDraw;
+            } else {
+                mDraw = draw;
+            }
+        }
+        mListener.requestRender();
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        synchronized (mLock) {
+            allocateTextureIfRequested(canvas);
+            if (!mVisible) mVisible = true;
+            SurfaceTexture surfaceTexture = getSurfaceTexture();
+            if (mDraw.requiresSurfaceTexture() && (surfaceTexture == null || !mFirstFrameArrived)) {
+                return;
+            }
+            if (mOnFrameDrawnListener != null) {
+                mOnFrameDrawnListener.run();
+                mOnFrameDrawnListener = null;
+            }
+            float oldAlpha = canvas.getAlpha();
+            canvas.setAlpha(mAlpha);
+
+            switch (mAnimState) {
+                case ANIM_NONE:
+                    directDraw(canvas, x, y, width, height);
+                    break;
+                case ANIM_SWITCH_COPY_TEXTURE:
+                    copyPreviewTexture(canvas);
+                    mSwitchAnimManager.setReviewDrawingSize(width, height);
+                    mListener.onPreviewTextureCopied();
+                    mAnimState = ANIM_SWITCH_DARK_PREVIEW;
+                    // The texture is ready. Fall through to draw darkened
+                    // preview.
+                case ANIM_SWITCH_DARK_PREVIEW:
+                case ANIM_SWITCH_WAITING_FIRST_FRAME:
+                    // Consume the frame. If the buffers are full,
+                    // onFrameAvailable will not be called. Animation state
+                    // relies on onFrameAvailable.
+                    surfaceTexture.updateTexImage();
+                    mSwitchAnimManager.drawDarkPreview(canvas, x, y, width,
+                            height, mAnimTexture);
+                    break;
+                case ANIM_SWITCH_START:
+                    mSwitchAnimManager.startAnimation();
+                    mAnimState = ANIM_SWITCH_RUNNING;
+                    break;
+                case ANIM_CAPTURE_START:
+                    copyPreviewTexture(canvas);
+                    mListener.onCaptureTextureCopied();
+                    mCaptureAnimManager.startAnimation();
+                    mAnimState = ANIM_CAPTURE_RUNNING;
+                    break;
+            }
+
+            if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) {
+                boolean drawn;
+                if (mAnimState == ANIM_CAPTURE_RUNNING) {
+                    if (!mFullScreen) {
+                        // Skip the animation if no longer in full screen mode
+                        drawn = false;
+                    } else {
+                        drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture,
+                                x, y, width, height);
+                    }
+                } else {
+                    drawn = mSwitchAnimManager.drawAnimation(canvas, x, y,
+                            width, height, this, mAnimTexture);
+                }
+                if (drawn) {
+                    mListener.requestRender();
+                } else {
+                    // Continue to the normal draw procedure if the animation is
+                    // not drawn.
+                    mAnimState = ANIM_NONE;
+                    directDraw(canvas, x, y, width, height);
+                }
+            }
+            canvas.setAlpha(oldAlpha);
+            callbackIfNeeded();
+        } // mLock
+    }
+
+    private void copyPreviewTexture(GLCanvas canvas) {
+        if (!mDraw.requiresSurfaceTexture()) {
+            mAnimTexture =  mDraw.copyToTexture(
+                    canvas, mAnimTexture, getTextureWidth(), getTextureHeight());
+        } else {
+            int width = mAnimTexture.getWidth();
+            int height = mAnimTexture.getHeight();
+            canvas.beginRenderTarget(mAnimTexture);
+            // Flip preview texture vertically. OpenGL uses bottom left point
+            // as the origin (0, 0).
+            canvas.translate(0, height);
+            canvas.scale(1, -1, 1);
+            getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix);
+            updateTransformMatrix(mTextureTransformMatrix);
+            canvas.drawTexture(mExtTexture, mTextureTransformMatrix, 0, 0, width, height);
+            canvas.endRenderTarget();
+        }
+    }
+
+    @Override
+    public void noDraw() {
+        synchronized (mLock) {
+            mVisible = false;
+        }
+    }
+
+    @Override
+    public void recycle() {
+        synchronized (mLock) {
+            mVisible = false;
+        }
+    }
+
+    @Override
+    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+        synchronized (mLock) {
+            if (getSurfaceTexture() != surfaceTexture) {
+                return;
+            }
+            mFirstFrameArrived = true;
+            if (mVisible) {
+                if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) {
+                    mAnimState = ANIM_SWITCH_START;
+                }
+                // We need to ask for re-render if the SurfaceTexture receives a new
+                // frame.
+                mListener.requestRender();
+            }
+        }
+    }
+
+    // We need to keep track of the size of preview frame on the screen because
+    // it's needed when we do switch-camera animation. See comments in
+    // SwitchAnimManager.java. This is based on the natural orientation, not the
+    // view system orientation.
+    public void setPreviewFrameLayoutSize(int width, int height) {
+        synchronized (mLock) {
+            mSwitchAnimManager.setPreviewFrameLayoutSize(width, height);
+            setPreviewLayoutSize(width, height);
+        }
+    }
+
+    public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) {
+        synchronized (mLock) {
+            mFirstFrameArrived = false;
+            mOneTimeFrameDrawnListener = l;
+        }
+    }
+
+    @Override
+    public SurfaceTexture getSurfaceTexture() {
+        synchronized (mLock) {
+            SurfaceTexture surfaceTexture = super.getSurfaceTexture();
+            if (surfaceTexture == null && mAcquireTexture) {
+                try {
+                    mLock.wait();
+                    surfaceTexture = super.getSurfaceTexture();
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "unexpected interruption");
+                }
+            }
+            return surfaceTexture;
+        }
+    }
+
+    private void allocateTextureIfRequested(GLCanvas canvas) {
+        synchronized (mLock) {
+            if (mAcquireTexture) {
+                super.acquireSurfaceTexture(canvas);
+                mAcquireTexture = false;
+                mLock.notifyAll();
+            }
+        }
+    }
+
+    public void setOnFrameDrawnOneShot(Runnable run) {
+        synchronized (mLock) {
+            mOnFrameDrawnListener = run;
+        }
+    }
+
+    public float getAlpha() {
+        synchronized (mLock) {
+            return mAlpha;
+        }
+    }
+
+    public void setAlpha(float alpha) {
+        synchronized (mLock) {
+            mAlpha = alpha;
+            mListener.requestRender();
+        }
+    }
+}
diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java
new file mode 100644
index 0000000..3558014
--- /dev/null
+++ b/src/com/android/camera/CameraSettings.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.media.CamcorderProfile;
+import android.util.FloatMath;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ *  Provides utilities and keys for Camera settings.
+ */
+public class CameraSettings {
+    private static final int NOT_FOUND = -1;
+
+    public static final String KEY_VERSION = "pref_version_key";
+    public static final String KEY_LOCAL_VERSION = "pref_local_version_key";
+    public static final String KEY_RECORD_LOCATION = "pref_camera_recordlocation_key";
+    public static final String KEY_VIDEO_QUALITY = "pref_video_quality_key";
+    public static final String KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL = "pref_video_time_lapse_frame_interval_key";
+    public static final String KEY_PICTURE_SIZE = "pref_camera_picturesize_key";
+    public static final String KEY_JPEG_QUALITY = "pref_camera_jpegquality_key";
+    public static final String KEY_FOCUS_MODE = "pref_camera_focusmode_key";
+    public static final String KEY_FLASH_MODE = "pref_camera_flashmode_key";
+    public static final String KEY_VIDEOCAMERA_FLASH_MODE = "pref_camera_video_flashmode_key";
+    public static final String KEY_WHITE_BALANCE = "pref_camera_whitebalance_key";
+    public static final String KEY_SCENE_MODE = "pref_camera_scenemode_key";
+    public static final String KEY_EXPOSURE = "pref_camera_exposure_key";
+    public static final String KEY_TIMER = "pref_camera_timer_key";
+    public static final String KEY_TIMER_SOUND_EFFECTS = "pref_camera_timer_sound_key";
+    public static final String KEY_VIDEO_EFFECT = "pref_video_effect_key";
+    public static final String KEY_CAMERA_ID = "pref_camera_id_key";
+    public static final String KEY_CAMERA_HDR = "pref_camera_hdr_key";
+    public static final String KEY_CAMERA_FIRST_USE_HINT_SHOWN = "pref_camera_first_use_hint_shown_key";
+    public static final String KEY_VIDEO_FIRST_USE_HINT_SHOWN = "pref_video_first_use_hint_shown_key";
+    public static final String KEY_PHOTOSPHERE_PICTURESIZE = "pref_photosphere_picturesize_key";
+
+    public static final String EXPOSURE_DEFAULT_VALUE = "0";
+
+    public static final int CURRENT_VERSION = 5;
+    public static final int CURRENT_LOCAL_VERSION = 2;
+
+    private static final String TAG = "CameraSettings";
+
+    private final Context mContext;
+    private final Parameters mParameters;
+    private final CameraInfo[] mCameraInfo;
+    private final int mCameraId;
+
+    public CameraSettings(Activity activity, Parameters parameters,
+                          int cameraId, CameraInfo[] cameraInfo) {
+        mContext = activity;
+        mParameters = parameters;
+        mCameraId = cameraId;
+        mCameraInfo = cameraInfo;
+    }
+
+    public PreferenceGroup getPreferenceGroup(int preferenceRes) {
+        PreferenceInflater inflater = new PreferenceInflater(mContext);
+        PreferenceGroup group =
+                (PreferenceGroup) inflater.inflate(preferenceRes);
+        if (mParameters != null) initPreference(group);
+        return group;
+    }
+
+    public static String getSupportedHighestVideoQuality(int cameraId,
+            String defaultQuality) {
+        // When launching the camera app first time, we will set the video quality
+        // to the first one (i.e. highest quality) in the supported list
+        List<String> supported = getSupportedVideoQuality(cameraId);
+        if (supported == null) {
+            Log.e(TAG, "No supported video quality is found");
+            return defaultQuality;
+        }
+        return supported.get(0);
+    }
+
+    public static void initialCameraPictureSize(
+            Context context, Parameters parameters) {
+        // When launching the camera app first time, we will set the picture
+        // size to the first one in the list defined in "arrays.xml" and is also
+        // supported by the driver.
+        List<Size> supported = parameters.getSupportedPictureSizes();
+        if (supported == null) return;
+        for (String candidate : context.getResources().getStringArray(
+                R.array.pref_camera_picturesize_entryvalues)) {
+            if (setCameraPictureSize(candidate, supported, parameters)) {
+                SharedPreferences.Editor editor = ComboPreferences
+                        .get(context).edit();
+                editor.putString(KEY_PICTURE_SIZE, candidate);
+                editor.apply();
+                return;
+            }
+        }
+        Log.e(TAG, "No supported picture size found");
+    }
+
+    public static void removePreferenceFromScreen(
+            PreferenceGroup group, String key) {
+        removePreference(group, key);
+    }
+
+    public static boolean setCameraPictureSize(
+            String candidate, List<Size> supported, Parameters parameters) {
+        int index = candidate.indexOf('x');
+        if (index == NOT_FOUND) return false;
+        int width = Integer.parseInt(candidate.substring(0, index));
+        int height = Integer.parseInt(candidate.substring(index + 1));
+        for (Size size : supported) {
+            if (size.width == width && size.height == height) {
+                parameters.setPictureSize(width, height);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static int getMaxVideoDuration(Context context) {
+        int duration = 0;  // in milliseconds, 0 means unlimited.
+        try {
+            duration = context.getResources().getInteger(R.integer.max_video_recording_length);
+        } catch (Resources.NotFoundException ex) {
+        }
+        return duration;
+    }
+
+    private void initPreference(PreferenceGroup group) {
+        ListPreference videoQuality = group.findPreference(KEY_VIDEO_QUALITY);
+        ListPreference timeLapseInterval = group.findPreference(KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL);
+        ListPreference pictureSize = group.findPreference(KEY_PICTURE_SIZE);
+        ListPreference whiteBalance =  group.findPreference(KEY_WHITE_BALANCE);
+        ListPreference sceneMode = group.findPreference(KEY_SCENE_MODE);
+        ListPreference flashMode = group.findPreference(KEY_FLASH_MODE);
+        ListPreference focusMode = group.findPreference(KEY_FOCUS_MODE);
+        IconListPreference exposure =
+                (IconListPreference) group.findPreference(KEY_EXPOSURE);
+        CountDownTimerPreference timer =
+                (CountDownTimerPreference) group.findPreference(KEY_TIMER);
+        ListPreference countDownSoundEffects = group.findPreference(KEY_TIMER_SOUND_EFFECTS);
+        IconListPreference cameraIdPref =
+                (IconListPreference) group.findPreference(KEY_CAMERA_ID);
+        ListPreference videoFlashMode =
+                group.findPreference(KEY_VIDEOCAMERA_FLASH_MODE);
+        ListPreference videoEffect = group.findPreference(KEY_VIDEO_EFFECT);
+        ListPreference cameraHdr = group.findPreference(KEY_CAMERA_HDR);
+
+        // Since the screen could be loaded from different resources, we need
+        // to check if the preference is available here
+        if (videoQuality != null) {
+            filterUnsupportedOptions(group, videoQuality, getSupportedVideoQuality(mCameraId));
+        }
+
+        if (pictureSize != null) {
+            filterUnsupportedOptions(group, pictureSize, sizeListToStringList(
+                    mParameters.getSupportedPictureSizes()));
+            filterSimilarPictureSize(group, pictureSize);
+        }
+        if (whiteBalance != null) {
+            filterUnsupportedOptions(group,
+                    whiteBalance, mParameters.getSupportedWhiteBalance());
+        }
+        if (sceneMode != null) {
+            filterUnsupportedOptions(group,
+                    sceneMode, mParameters.getSupportedSceneModes());
+        }
+        if (flashMode != null) {
+            filterUnsupportedOptions(group,
+                    flashMode, mParameters.getSupportedFlashModes());
+        }
+        if (focusMode != null) {
+            if (!Util.isFocusAreaSupported(mParameters)) {
+                filterUnsupportedOptions(group,
+                        focusMode, mParameters.getSupportedFocusModes());
+            } else {
+                // Remove the focus mode if we can use tap-to-focus.
+                removePreference(group, focusMode.getKey());
+            }
+        }
+        if (videoFlashMode != null) {
+            filterUnsupportedOptions(group,
+                    videoFlashMode, mParameters.getSupportedFlashModes());
+        }
+        if (exposure != null) buildExposureCompensation(group, exposure);
+        if (cameraIdPref != null) buildCameraId(group, cameraIdPref);
+
+        if (timeLapseInterval != null) {
+            if (ApiHelper.HAS_TIME_LAPSE_RECORDING) {
+                resetIfInvalid(timeLapseInterval);
+            } else {
+                removePreference(group, timeLapseInterval.getKey());
+            }
+        }
+        if (videoEffect != null) {
+            if (ApiHelper.HAS_EFFECTS_RECORDING) {
+                initVideoEffect(group, videoEffect);
+                resetIfInvalid(videoEffect);
+            } else {
+                filterUnsupportedOptions(group, videoEffect, null);
+            }
+        }
+        if (cameraHdr != null && (!ApiHelper.HAS_CAMERA_HDR
+                    || !Util.isCameraHdrSupported(mParameters))) {
+            removePreference(group, cameraHdr.getKey());
+        }
+    }
+
+    private void buildExposureCompensation(
+            PreferenceGroup group, IconListPreference exposure) {
+        int max = mParameters.getMaxExposureCompensation();
+        int min = mParameters.getMinExposureCompensation();
+        if (max == 0 && min == 0) {
+            removePreference(group, exposure.getKey());
+            return;
+        }
+        float step = mParameters.getExposureCompensationStep();
+
+        // show only integer values for exposure compensation
+        int maxValue = Math.min(3, (int) FloatMath.floor(max * step));
+        int minValue = Math.max(-3, (int) FloatMath.ceil(min * step));
+        String explabel = mContext.getResources().getString(R.string.pref_exposure_label);
+        CharSequence entries[] = new CharSequence[maxValue - minValue + 1];
+        CharSequence entryValues[] = new CharSequence[maxValue - minValue + 1];
+        CharSequence labels[] = new CharSequence[maxValue - minValue + 1];
+        int[] icons = new int[maxValue - minValue + 1];
+        TypedArray iconIds = mContext.getResources().obtainTypedArray(
+                R.array.pref_camera_exposure_icons);
+        for (int i = minValue; i <= maxValue; ++i) {
+            entryValues[i - minValue] = Integer.toString(Math.round(i / step));
+            StringBuilder builder = new StringBuilder();
+            if (i > 0) builder.append('+');
+            entries[i - minValue] = builder.append(i).toString();
+            labels[i - minValue] = explabel + " " + builder.toString();
+            icons[i - minValue] = iconIds.getResourceId(3 + i, 0);
+        }
+        exposure.setUseSingleIcon(true);
+        exposure.setEntries(entries);
+        exposure.setLabels(labels);
+        exposure.setEntryValues(entryValues);
+        exposure.setLargeIconIds(icons);
+    }
+
+    private void buildCameraId(
+            PreferenceGroup group, IconListPreference preference) {
+        int numOfCameras = mCameraInfo.length;
+        if (numOfCameras < 2) {
+            removePreference(group, preference.getKey());
+            return;
+        }
+
+        CharSequence[] entryValues = new CharSequence[numOfCameras];
+        for (int i = 0; i < numOfCameras; ++i) {
+            entryValues[i] = "" + i;
+        }
+        preference.setEntryValues(entryValues);
+    }
+
+    private static boolean removePreference(PreferenceGroup group, String key) {
+        for (int i = 0, n = group.size(); i < n; i++) {
+            CameraPreference child = group.get(i);
+            if (child instanceof PreferenceGroup) {
+                if (removePreference((PreferenceGroup) child, key)) {
+                    return true;
+                }
+            }
+            if (child instanceof ListPreference &&
+                    ((ListPreference) child).getKey().equals(key)) {
+                group.removePreference(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void filterUnsupportedOptions(PreferenceGroup group,
+            ListPreference pref, List<String> supported) {
+
+        // Remove the preference if the parameter is not supported or there is
+        // only one options for the settings.
+        if (supported == null || supported.size() <= 1) {
+            removePreference(group, pref.getKey());
+            return;
+        }
+
+        pref.filterUnsupported(supported);
+        if (pref.getEntries().length <= 1) {
+            removePreference(group, pref.getKey());
+            return;
+        }
+
+        resetIfInvalid(pref);
+    }
+
+    private void filterSimilarPictureSize(PreferenceGroup group,
+            ListPreference pref) {
+        pref.filterDuplicated();
+        if (pref.getEntries().length <= 1) {
+            removePreference(group, pref.getKey());
+            return;
+        }
+        resetIfInvalid(pref);
+    }
+
+    private void resetIfInvalid(ListPreference pref) {
+        // Set the value to the first entry if it is invalid.
+        String value = pref.getValue();
+        if (pref.findIndexOfValue(value) == NOT_FOUND) {
+            pref.setValueIndex(0);
+        }
+    }
+
+    private static List<String> sizeListToStringList(List<Size> sizes) {
+        ArrayList<String> list = new ArrayList<String>();
+        for (Size size : sizes) {
+            list.add(String.format(Locale.ENGLISH, "%dx%d", size.width, size.height));
+        }
+        return list;
+    }
+
+    public static void upgradeLocalPreferences(SharedPreferences pref) {
+        int version;
+        try {
+            version = pref.getInt(KEY_LOCAL_VERSION, 0);
+        } catch (Exception ex) {
+            version = 0;
+        }
+        if (version == CURRENT_LOCAL_VERSION) return;
+
+        SharedPreferences.Editor editor = pref.edit();
+        if (version == 1) {
+            // We use numbers to represent the quality now. The quality definition is identical to
+            // that of CamcorderProfile.java.
+            editor.remove("pref_video_quality_key");
+        }
+        editor.putInt(KEY_LOCAL_VERSION, CURRENT_LOCAL_VERSION);
+        editor.apply();
+    }
+
+    public static void upgradeGlobalPreferences(SharedPreferences pref) {
+        upgradeOldVersion(pref);
+        upgradeCameraId(pref);
+    }
+
+    private static void upgradeOldVersion(SharedPreferences pref) {
+        int version;
+        try {
+            version = pref.getInt(KEY_VERSION, 0);
+        } catch (Exception ex) {
+            version = 0;
+        }
+        if (version == CURRENT_VERSION) return;
+
+        SharedPreferences.Editor editor = pref.edit();
+        if (version == 0) {
+            // We won't use the preference which change in version 1.
+            // So, just upgrade to version 1 directly
+            version = 1;
+        }
+        if (version == 1) {
+            // Change jpeg quality {65,75,85} to {normal,fine,superfine}
+            String quality = pref.getString(KEY_JPEG_QUALITY, "85");
+            if (quality.equals("65")) {
+                quality = "normal";
+            } else if (quality.equals("75")) {
+                quality = "fine";
+            } else {
+                quality = "superfine";
+            }
+            editor.putString(KEY_JPEG_QUALITY, quality);
+            version = 2;
+        }
+        if (version == 2) {
+            editor.putString(KEY_RECORD_LOCATION,
+                    pref.getBoolean(KEY_RECORD_LOCATION, false)
+                    ? RecordLocationPreference.VALUE_ON
+                    : RecordLocationPreference.VALUE_NONE);
+            version = 3;
+        }
+        if (version == 3) {
+            // Just use video quality to replace it and
+            // ignore the current settings.
+            editor.remove("pref_camera_videoquality_key");
+            editor.remove("pref_camera_video_duration_key");
+        }
+
+        editor.putInt(KEY_VERSION, CURRENT_VERSION);
+        editor.apply();
+    }
+
+    private static void upgradeCameraId(SharedPreferences pref) {
+        // The id stored in the preference may be out of range if we are running
+        // inside the emulator and a webcam is removed.
+        // Note: This method accesses the global preferences directly, not the
+        // combo preferences.
+        int cameraId = readPreferredCameraId(pref);
+        if (cameraId == 0) return;  // fast path
+
+        int n = CameraHolder.instance().getNumberOfCameras();
+        if (cameraId < 0 || cameraId >= n) {
+            writePreferredCameraId(pref, 0);
+        }
+    }
+
+    public static int readPreferredCameraId(SharedPreferences pref) {
+        return Integer.parseInt(pref.getString(KEY_CAMERA_ID, "0"));
+    }
+
+    public static void writePreferredCameraId(SharedPreferences pref,
+            int cameraId) {
+        Editor editor = pref.edit();
+        editor.putString(KEY_CAMERA_ID, Integer.toString(cameraId));
+        editor.apply();
+    }
+
+    public static int readExposure(ComboPreferences preferences) {
+        String exposure = preferences.getString(
+                CameraSettings.KEY_EXPOSURE,
+                EXPOSURE_DEFAULT_VALUE);
+        try {
+            return Integer.parseInt(exposure);
+        } catch (Exception ex) {
+            Log.e(TAG, "Invalid exposure: " + exposure);
+        }
+        return 0;
+    }
+
+    public static int readEffectType(SharedPreferences pref) {
+        String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none");
+        if (effectSelection.equals("none")) {
+            return EffectsRecorder.EFFECT_NONE;
+        } else if (effectSelection.startsWith("goofy_face")) {
+            return EffectsRecorder.EFFECT_GOOFY_FACE;
+        } else if (effectSelection.startsWith("backdropper")) {
+            return EffectsRecorder.EFFECT_BACKDROPPER;
+        }
+        Log.e(TAG, "Invalid effect selection: " + effectSelection);
+        return EffectsRecorder.EFFECT_NONE;
+    }
+
+    public static Object readEffectParameter(SharedPreferences pref) {
+        String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none");
+        if (effectSelection.equals("none")) {
+            return null;
+        }
+        int separatorIndex = effectSelection.indexOf('/');
+        String effectParameter =
+                effectSelection.substring(separatorIndex + 1);
+        if (effectSelection.startsWith("goofy_face")) {
+            if (effectParameter.equals("squeeze")) {
+                return EffectsRecorder.EFFECT_GF_SQUEEZE;
+            } else if (effectParameter.equals("big_eyes")) {
+                return EffectsRecorder.EFFECT_GF_BIG_EYES;
+            } else if (effectParameter.equals("big_mouth")) {
+                return EffectsRecorder.EFFECT_GF_BIG_MOUTH;
+            } else if (effectParameter.equals("small_mouth")) {
+                return EffectsRecorder.EFFECT_GF_SMALL_MOUTH;
+            } else if (effectParameter.equals("big_nose")) {
+                return EffectsRecorder.EFFECT_GF_BIG_NOSE;
+            } else if (effectParameter.equals("small_eyes")) {
+                return EffectsRecorder.EFFECT_GF_SMALL_EYES;
+            }
+        } else if (effectSelection.startsWith("backdropper")) {
+            // Parameter is a string that either encodes the URI to use,
+            // or specifies 'gallery'.
+            return effectParameter;
+        }
+
+        Log.e(TAG, "Invalid effect selection: " + effectSelection);
+        return null;
+    }
+
+    public static void restorePreferences(Context context,
+            ComboPreferences preferences, Parameters parameters) {
+        int currentCameraId = readPreferredCameraId(preferences);
+
+        // Clear the preferences of both cameras.
+        int backCameraId = CameraHolder.instance().getBackCameraId();
+        if (backCameraId != -1) {
+            preferences.setLocalId(context, backCameraId);
+            Editor editor = preferences.edit();
+            editor.clear();
+            editor.apply();
+        }
+        int frontCameraId = CameraHolder.instance().getFrontCameraId();
+        if (frontCameraId != -1) {
+            preferences.setLocalId(context, frontCameraId);
+            Editor editor = preferences.edit();
+            editor.clear();
+            editor.apply();
+        }
+
+        // Switch back to the preferences of the current camera. Otherwise,
+        // we may write the preference to wrong camera later.
+        preferences.setLocalId(context, currentCameraId);
+
+        upgradeGlobalPreferences(preferences.getGlobal());
+        upgradeLocalPreferences(preferences.getLocal());
+
+        // Write back the current camera id because parameters are related to
+        // the camera. Otherwise, we may switch to the front camera but the
+        // initial picture size is that of the back camera.
+        initialCameraPictureSize(context, parameters);
+        writePreferredCameraId(preferences, currentCameraId);
+    }
+
+    private static ArrayList<String> getSupportedVideoQuality(int cameraId) {
+        ArrayList<String> supported = new ArrayList<String>();
+        // Check for supported quality
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
+            supported.add(Integer.toString(CamcorderProfile.QUALITY_1080P));
+        }
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
+            supported.add(Integer.toString(CamcorderProfile.QUALITY_720P));
+        }
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
+            supported.add(Integer.toString(CamcorderProfile.QUALITY_480P));
+        }
+        return supported;
+    }
+
+    private void initVideoEffect(PreferenceGroup group, ListPreference videoEffect) {
+        CharSequence[] values = videoEffect.getEntryValues();
+
+        boolean goofyFaceSupported =
+                EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_GOOFY_FACE);
+        boolean backdropperSupported =
+                EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_BACKDROPPER) &&
+                Util.isAutoExposureLockSupported(mParameters) &&
+                Util.isAutoWhiteBalanceLockSupported(mParameters);
+
+        ArrayList<String> supported = new ArrayList<String>();
+        for (CharSequence value : values) {
+            String effectSelection = value.toString();
+            if (!goofyFaceSupported && effectSelection.startsWith("goofy_face")) continue;
+            if (!backdropperSupported && effectSelection.startsWith("backdropper")) continue;
+            supported.add(effectSelection);
+        }
+
+        filterUnsupportedOptions(group, videoEffect, supported);
+    }
+}
diff --git a/src/com/android/camera/CaptureAnimManager.java b/src/com/android/camera/CaptureAnimManager.java
new file mode 100644
index 0000000..6e80925
--- /dev/null
+++ b/src/com/android/camera/CaptureAnimManager.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2012 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.res.Resources;
+import android.graphics.Color;
+import android.os.SystemClock;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.RawTexture;
+
+/**
+ * Class to handle the capture animation.
+ */
+public class CaptureAnimManager {
+    @SuppressWarnings("unused")
+    private static final String TAG = "CAM_Capture";
+    // times mark endpoint of animation phase
+    private static final int TIME_FLASH = 200;
+    private static final int TIME_HOLD = 400;
+    private static final int TIME_SLIDE = 800;
+    private static final int TIME_HOLD2 = 3300;
+    private static final int TIME_SLIDE2 = 4100;
+
+    private static final int ANIM_BOTH = 0;
+    private static final int ANIM_FLASH = 1;
+    private static final int ANIM_SLIDE = 2;
+    private static final int ANIM_HOLD2 = 3;
+    private static final int ANIM_SLIDE2 = 4;
+
+    private final Interpolator mSlideInterpolator = new DecelerateInterpolator();
+
+    private volatile int mAnimOrientation;  // Could be 0, 90, 180 or 270 degrees.
+    private long mAnimStartTime;  // milliseconds.
+    private float mX;  // The center of the whole view including preview and review.
+    private float mY;
+    private int mDrawWidth;
+    private int mDrawHeight;
+    private int mAnimType;
+
+    private int mHoldX;
+    private int mHoldY;
+    private int mHoldW;
+    private int mHoldH;
+
+    private int mOffset;
+
+    private int mMarginRight;
+    private int mMarginTop;
+    private int mSize;
+    private Resources mResources;
+    private NinePatchTexture mBorder;
+    private int mShadowSize;
+
+    public static int getAnimationDuration() {
+        return TIME_SLIDE2;
+    }
+
+    /* preview: camera preview view.
+     * review: view of picture just taken.
+     */
+    public CaptureAnimManager(Context ctx) {
+        mBorder = new NinePatchTexture(ctx, R.drawable.capture_thumbnail_shadow);
+        mResources = ctx.getResources();
+    }
+
+    public void setOrientation(int displayRotation) {
+        mAnimOrientation = (360 - displayRotation) % 360;
+    }
+
+    public void animateSlide() {
+        if (mAnimType != ANIM_FLASH) {
+            return;
+        }
+        mAnimType = ANIM_SLIDE;
+        mAnimStartTime = SystemClock.uptimeMillis();
+    }
+
+    public void animateFlash() {
+        mAnimType = ANIM_FLASH;
+    }
+
+    public void animateFlashAndSlide() {
+        mAnimType = ANIM_BOTH;
+    }
+
+    public void startAnimation() {
+        mAnimStartTime = SystemClock.uptimeMillis();
+    }
+
+    private void setAnimationGeometry(int x, int y, int w, int h) {
+        mMarginRight = mResources.getDimensionPixelSize(R.dimen.capture_margin_right);
+        mMarginTop = mResources.getDimensionPixelSize(R.dimen.capture_margin_top);
+        mSize = mResources.getDimensionPixelSize(R.dimen.capture_size);
+        mShadowSize = mResources.getDimensionPixelSize(R.dimen.capture_border);
+        mOffset = mMarginRight + mSize;
+        // Set the views to the initial positions.
+        mDrawWidth = w;
+        mDrawHeight = h;
+        mX = x;
+        mY = y;
+        mHoldW = mSize;
+        mHoldH = mSize;
+        switch (mAnimOrientation) {
+            case 0:  // Preview is on the left.
+                mHoldX = x + w - mMarginRight - mSize;
+                mHoldY = y + mMarginTop;
+                break;
+            case 90:  // Preview is below.
+                mHoldX = x + mMarginTop;
+                mHoldY = y + mMarginRight;
+                break;
+            case 180:  // Preview on the right.
+                mHoldX = x + mMarginRight;
+                mHoldY = y + h - mMarginTop - mSize;
+                break;
+            case 270:  // Preview is above.
+                mHoldX = x + w - mMarginTop - mSize;
+                mHoldY = y + h - mMarginRight - mSize;
+                break;
+        }
+    }
+
+    // Returns true if the animation has been drawn.
+    public boolean drawAnimation(GLCanvas canvas, CameraScreenNail preview,
+                RawTexture review, int lx, int ly, int lw, int lh) {
+        setAnimationGeometry(lx, ly, lw, lh);
+        long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime;
+        // Check if the animation is over
+        if (mAnimType == ANIM_SLIDE && timeDiff > TIME_SLIDE2 - TIME_HOLD) return false;
+        if (mAnimType == ANIM_BOTH && timeDiff > TIME_SLIDE2) return false;
+
+        // determine phase and time in phase
+        int animStep = mAnimType;
+        if (mAnimType == ANIM_SLIDE) {
+            timeDiff += TIME_HOLD;
+        }
+        if (mAnimType == ANIM_SLIDE || mAnimType == ANIM_BOTH) {
+            if (timeDiff < TIME_HOLD) {
+                animStep = ANIM_FLASH;
+            } else if (timeDiff < TIME_SLIDE) {
+                animStep = ANIM_SLIDE;
+                timeDiff -= TIME_HOLD;
+            } else if (timeDiff < TIME_HOLD2) {
+                animStep = ANIM_HOLD2;
+                timeDiff -= TIME_SLIDE;
+            } else {
+                // SLIDE2
+                animStep = ANIM_SLIDE2;
+                timeDiff -= TIME_HOLD2;
+            }
+        }
+
+        if (animStep == ANIM_FLASH) {
+            review.draw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+            if (timeDiff < TIME_FLASH) {
+                float f = 0.3f - 0.3f * timeDiff / TIME_FLASH;
+                int color = Color.argb((int) (255 * f), 255, 255, 255);
+                canvas.fillRect(mX, mY, mDrawWidth, mDrawHeight, color);
+            }
+        } else if (animStep == ANIM_SLIDE) {
+            float fraction = mSlideInterpolator.getInterpolation((float) (timeDiff) / (TIME_SLIDE - TIME_HOLD));
+            float x = mX;
+            float y = mY;
+            float w = 0;
+            float h = 0;
+            x = interpolate(mX, mHoldX, fraction);
+            y = interpolate(mY, mHoldY, fraction);
+            w = interpolate(mDrawWidth, mHoldW, fraction);
+            h = interpolate(mDrawHeight, mHoldH, fraction);
+            preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+            review.draw(canvas, (int) x, (int) y, (int) w, (int) h);
+        } else if (animStep == ANIM_HOLD2) {
+            preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+            review.draw(canvas, mHoldX, mHoldY, mHoldW, mHoldH);
+            mBorder.draw(canvas, (int) mHoldX - mShadowSize, (int) mHoldY - mShadowSize,
+                    (int) mHoldW + 2 * mShadowSize, (int) mHoldH + 2 * mShadowSize);
+        } else if (animStep == ANIM_SLIDE2) {
+            float fraction = (float)(timeDiff) / (TIME_SLIDE2 - TIME_HOLD2);
+            float x = mHoldX;
+            float y = mHoldY;
+            float d = mOffset * fraction;
+            switch (mAnimOrientation) {
+            case 0:
+                x = mHoldX + d;
+                break;
+            case 180:
+                x = mHoldX - d;
+                break;
+            case 90:
+                y = mHoldY - d;
+                break;
+            case 270:
+                y = mHoldY + d;
+                break;
+            }
+            preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+            mBorder.draw(canvas, (int) x - mShadowSize, (int) y - mShadowSize,
+                    (int) mHoldW + 2 * mShadowSize, (int) mHoldH + 2 * mShadowSize);
+            review.draw(canvas, (int) x, (int) y, mHoldW, mHoldH);
+        }
+        return true;
+    }
+
+    private static float interpolate(float start, float end, float fraction) {
+        return start + (end - start) * fraction;
+    }
+
+}
diff --git a/src/com/android/camera/ComboPreferences.java b/src/com/android/camera/ComboPreferences.java
new file mode 100644
index 0000000..e17e47a
--- /dev/null
+++ b/src/com/android/camera/ComboPreferences.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2010 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.backup.BackupManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class ComboPreferences implements
+        SharedPreferences,
+        OnSharedPreferenceChangeListener {
+    private SharedPreferences mPrefGlobal;  // global preferences
+    private SharedPreferences mPrefLocal;  // per-camera preferences
+    private String mPackageName;
+    private CopyOnWriteArrayList<OnSharedPreferenceChangeListener> mListeners;
+    // TODO: Remove this WeakHashMap in the camera code refactoring
+    private static WeakHashMap<Context, ComboPreferences> sMap =
+            new WeakHashMap<Context, ComboPreferences>();
+
+    public ComboPreferences(Context context) {
+        mPackageName = context.getPackageName();
+        mPrefGlobal = context.getSharedPreferences(
+                getGlobalSharedPreferencesName(context), Context.MODE_PRIVATE);
+        mPrefGlobal.registerOnSharedPreferenceChangeListener(this);
+
+        synchronized (sMap) {
+            sMap.put(context, this);
+        }
+        mListeners = new CopyOnWriteArrayList<OnSharedPreferenceChangeListener>();
+
+        // The global preferences was previously stored in the default
+        // shared preferences file. They should be stored in the camera-specific
+        // shared preferences file so we can backup them solely.
+        SharedPreferences oldprefs =
+                PreferenceManager.getDefaultSharedPreferences(context);
+        if (!mPrefGlobal.contains(CameraSettings.KEY_VERSION)
+                && oldprefs.contains(CameraSettings.KEY_VERSION)) {
+            moveGlobalPrefsFrom(oldprefs);
+        }
+    }
+
+    public static ComboPreferences get(Context context) {
+        synchronized (sMap) {
+            return sMap.get(context);
+        }
+    }
+
+    private static String getLocalSharedPreferencesName(
+            Context context, int cameraId) {
+        return context.getPackageName() + "_preferences_" + cameraId;
+    }
+
+    private static String getGlobalSharedPreferencesName(Context context) {
+        return context.getPackageName() + "_preferences_camera";
+    }
+
+    private void movePrefFrom(
+            Map<String, ?> m, String key, SharedPreferences src) {
+        if (m.containsKey(key)) {
+            Object v = m.get(key);
+            if (v instanceof String) {
+                mPrefGlobal.edit().putString(key, (String) v).apply();
+            } else if (v instanceof Integer) {
+                mPrefGlobal.edit().putInt(key, (Integer) v).apply();
+            } else if (v instanceof Long) {
+                mPrefGlobal.edit().putLong(key, (Long) v).apply();
+            } else if (v instanceof Float) {
+                mPrefGlobal.edit().putFloat(key, (Float) v).apply();
+            } else if (v instanceof Boolean) {
+                mPrefGlobal.edit().putBoolean(key, (Boolean) v).apply();
+            }
+            src.edit().remove(key).apply();
+        }
+    }
+
+    private void moveGlobalPrefsFrom(SharedPreferences src) {
+        Map<String, ?> prefMap = src.getAll();
+        movePrefFrom(prefMap, CameraSettings.KEY_VERSION, src);
+        movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, src);
+        movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_ID, src);
+        movePrefFrom(prefMap, CameraSettings.KEY_RECORD_LOCATION, src);
+        movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, src);
+        movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, src);
+        movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_EFFECT, src);
+    }
+
+    public static String[] getSharedPreferencesNames(Context context) {
+        int numOfCameras = CameraHolder.instance().getNumberOfCameras();
+        String prefNames[] = new String[numOfCameras + 1];
+        prefNames[0] = getGlobalSharedPreferencesName(context);
+        for (int i = 0; i < numOfCameras; i++) {
+            prefNames[i + 1] = getLocalSharedPreferencesName(context, i);
+        }
+        return prefNames;
+    }
+
+    // Sets the camera id and reads its preferences. Each camera has its own
+    // preferences.
+    public void setLocalId(Context context, int cameraId) {
+        String prefName = getLocalSharedPreferencesName(context, cameraId);
+        if (mPrefLocal != null) {
+            mPrefLocal.unregisterOnSharedPreferenceChangeListener(this);
+        }
+        mPrefLocal = context.getSharedPreferences(
+                prefName, Context.MODE_PRIVATE);
+        mPrefLocal.registerOnSharedPreferenceChangeListener(this);
+    }
+
+    public SharedPreferences getGlobal() {
+        return mPrefGlobal;
+    }
+
+    public SharedPreferences getLocal() {
+        return mPrefLocal;
+    }
+
+    @Override
+    public Map<String, ?> getAll() {
+        throw new UnsupportedOperationException(); // Can be implemented if needed.
+    }
+
+    private static boolean isGlobal(String key) {
+        return key.equals(CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL)
+                || key.equals(CameraSettings.KEY_CAMERA_ID)
+                || key.equals(CameraSettings.KEY_RECORD_LOCATION)
+                || key.equals(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN)
+                || key.equals(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN)
+                || key.equals(CameraSettings.KEY_VIDEO_EFFECT)
+                || key.equals(CameraSettings.KEY_TIMER)
+                || key.equals(CameraSettings.KEY_TIMER_SOUND_EFFECTS)
+                || key.equals(CameraSettings.KEY_PHOTOSPHERE_PICTURESIZE);
+    }
+
+    @Override
+    public String getString(String key, String defValue) {
+        if (isGlobal(key) || !mPrefLocal.contains(key)) {
+            return mPrefGlobal.getString(key, defValue);
+        } else {
+            return mPrefLocal.getString(key, defValue);
+        }
+    }
+
+    @Override
+    public int getInt(String key, int defValue) {
+        if (isGlobal(key) || !mPrefLocal.contains(key)) {
+            return mPrefGlobal.getInt(key, defValue);
+        } else {
+            return mPrefLocal.getInt(key, defValue);
+        }
+    }
+
+    @Override
+    public long getLong(String key, long defValue) {
+        if (isGlobal(key) || !mPrefLocal.contains(key)) {
+            return mPrefGlobal.getLong(key, defValue);
+        } else {
+            return mPrefLocal.getLong(key, defValue);
+        }
+    }
+
+    @Override
+    public float getFloat(String key, float defValue) {
+        if (isGlobal(key) || !mPrefLocal.contains(key)) {
+            return mPrefGlobal.getFloat(key, defValue);
+        } else {
+            return mPrefLocal.getFloat(key, defValue);
+        }
+    }
+
+    @Override
+    public boolean getBoolean(String key, boolean defValue) {
+        if (isGlobal(key) || !mPrefLocal.contains(key)) {
+            return mPrefGlobal.getBoolean(key, defValue);
+        } else {
+            return mPrefLocal.getBoolean(key, defValue);
+        }
+    }
+
+    // This method is not used.
+    @Override
+    public Set<String> getStringSet(String key, Set<String> defValues) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean contains(String key) {
+        return mPrefLocal.contains(key) || mPrefGlobal.contains(key);
+    }
+
+    private class MyEditor implements Editor {
+        private Editor mEditorGlobal;
+        private Editor mEditorLocal;
+
+        MyEditor() {
+            mEditorGlobal = mPrefGlobal.edit();
+            mEditorLocal = mPrefLocal.edit();
+        }
+
+        @Override
+        public boolean commit() {
+            boolean result1 = mEditorGlobal.commit();
+            boolean result2 = mEditorLocal.commit();
+            return result1 && result2;
+        }
+
+        @Override
+        public void apply() {
+            mEditorGlobal.apply();
+            mEditorLocal.apply();
+        }
+
+        // Note: clear() and remove() affects both local and global preferences.
+        @Override
+        public Editor clear() {
+            mEditorGlobal.clear();
+            mEditorLocal.clear();
+            return this;
+        }
+
+        @Override
+        public Editor remove(String key) {
+            mEditorGlobal.remove(key);
+            mEditorLocal.remove(key);
+            return this;
+        }
+
+        @Override
+        public Editor putString(String key, String value) {
+            if (isGlobal(key)) {
+                mEditorGlobal.putString(key, value);
+            } else {
+                mEditorLocal.putString(key, value);
+            }
+            return this;
+        }
+
+        @Override
+        public Editor putInt(String key, int value) {
+            if (isGlobal(key)) {
+                mEditorGlobal.putInt(key, value);
+            } else {
+                mEditorLocal.putInt(key, value);
+            }
+            return this;
+        }
+
+        @Override
+        public Editor putLong(String key, long value) {
+            if (isGlobal(key)) {
+                mEditorGlobal.putLong(key, value);
+            } else {
+                mEditorLocal.putLong(key, value);
+            }
+            return this;
+        }
+
+        @Override
+        public Editor putFloat(String key, float value) {
+            if (isGlobal(key)) {
+                mEditorGlobal.putFloat(key, value);
+            } else {
+                mEditorLocal.putFloat(key, value);
+            }
+            return this;
+        }
+
+        @Override
+        public Editor putBoolean(String key, boolean value) {
+            if (isGlobal(key)) {
+                mEditorGlobal.putBoolean(key, value);
+            } else {
+                mEditorLocal.putBoolean(key, value);
+            }
+            return this;
+        }
+
+        // This method is not used.
+        @Override
+        public Editor putStringSet(String key, Set<String> values) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    // Note the remove() and clear() of the returned Editor may not work as
+    // expected because it doesn't touch the global preferences at all.
+    @Override
+    public Editor edit() {
+        return new MyEditor();
+    }
+
+    @Override
+    public void registerOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        mListeners.add(listener);
+    }
+
+    @Override
+    public void unregisterOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+            String key) {
+        for (OnSharedPreferenceChangeListener listener : mListeners) {
+            listener.onSharedPreferenceChanged(this, key);
+        }
+        BackupManager.dataChanged(mPackageName);
+        UsageStatistics.onEvent("CameraSettingsChange", null, key);
+    }
+}
diff --git a/src/com/android/camera/CountDownTimerPreference.java b/src/com/android/camera/CountDownTimerPreference.java
new file mode 100644
index 0000000..9c66dda
--- /dev/null
+++ b/src/com/android/camera/CountDownTimerPreference.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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.util.AttributeSet;
+
+import com.android.gallery3d.R;
+
+public class CountDownTimerPreference extends ListPreference {
+    private static final int[] DURATIONS = {
+        0, 1, 2, 3, 4, 5, 10, 15, 20, 30, 60
+    };
+    public CountDownTimerPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initCountDownDurationChoices(context);
+    }
+
+    private void initCountDownDurationChoices(Context context) {
+        CharSequence[] entryValues = new CharSequence[DURATIONS.length];
+        CharSequence[] entries = new CharSequence[DURATIONS.length];
+        for (int i = 0; i < DURATIONS.length; i++) {
+            entryValues[i] = Integer.toString(DURATIONS[i]);
+            if (i == 0) {
+                entries[0] = context.getString(R.string.setting_off); // Off
+            } else {
+                entries[i] = context.getResources()
+                        .getQuantityString(R.plurals.pref_camera_timer_entry, i, i);
+            }
+        }
+        setEntries(entries);
+        setEntryValues(entryValues);
+    }
+}
diff --git a/src/com/android/camera/DisableCameraReceiver.java b/src/com/android/camera/DisableCameraReceiver.java
new file mode 100644
index 0000000..3517405
--- /dev/null
+++ b/src/com/android/camera/DisableCameraReceiver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 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.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.Camera.CameraInfo;
+import android.util.Log;
+
+// We want to disable camera-related activities if there is no camera. This
+// receiver runs when BOOT_COMPLETED intent is received. After running once
+// this receiver will be disabled, so it will not run again.
+public class DisableCameraReceiver extends BroadcastReceiver {
+    private static final String TAG = "DisableCameraReceiver";
+    private static final boolean CHECK_BACK_CAMERA_ONLY = true;
+    private static final String ACTIVITIES[] = {
+        "com.android.camera.CameraLauncher",
+    };
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        // Disable camera-related activities if there is no camera.
+        boolean needCameraActivity = CHECK_BACK_CAMERA_ONLY
+            ? hasBackCamera()
+            : hasCamera();
+
+        if (!needCameraActivity) {
+            Log.i(TAG, "disable all camera activities");
+            for (int i = 0; i < ACTIVITIES.length; i++) {
+                disableComponent(context, ACTIVITIES[i]);
+            }
+        }
+
+        // Disable this receiver so it won't run again.
+        disableComponent(context, "com.android.camera.DisableCameraReceiver");
+    }
+
+    private boolean hasCamera() {
+        int n = android.hardware.Camera.getNumberOfCameras();
+        Log.i(TAG, "number of camera: " + n);
+        return (n > 0);
+    }
+
+    private boolean hasBackCamera() {
+        int n = android.hardware.Camera.getNumberOfCameras();
+        CameraInfo info = new CameraInfo();
+        for (int i = 0; i < n; i++) {
+            android.hardware.Camera.getCameraInfo(i, info);
+            if (info.facing == CameraInfo.CAMERA_FACING_BACK) {
+                Log.i(TAG, "back camera found: " + i);
+                return true;
+            }
+        }
+        Log.i(TAG, "no back camera");
+        return false;
+    }
+
+    private void disableComponent(Context context, String klass) {
+        ComponentName name = new ComponentName(context, klass);
+        PackageManager pm = context.getPackageManager();
+
+        // We need the DONT_KILL_APP flag, otherwise we will be killed
+        // immediately because we are in the same app.
+        pm.setComponentEnabledSetting(name,
+            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+            PackageManager.DONT_KILL_APP);
+    }
+}
diff --git a/src/com/android/camera/EffectsRecorder.java b/src/com/android/camera/EffectsRecorder.java
new file mode 100644
index 0000000..4bf8d41
--- /dev/null
+++ b/src/com/android/camera/EffectsRecorder.java
@@ -0,0 +1,1239 @@
+/*
+ * Copyright (C) 2011 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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.media.CamcorderProfile;
+import android.media.MediaRecorder;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.FileDescriptor;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+
+/**
+ * Encapsulates the mobile filter framework components needed to record video
+ * with effects applied. Modeled after MediaRecorder.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
+public class EffectsRecorder {
+    private static final String TAG = "EffectsRecorder";
+
+    private static Class<?> sClassFilter;
+    private static Method sFilterIsAvailable;
+    private static EffectsRecorder sEffectsRecorder;
+    // The index of the current effects recorder.
+    private static int sEffectsRecorderIndex;
+
+    private static boolean sReflectionInited = false;
+
+    private static Class<?> sClsLearningDoneListener;
+    private static Class<?> sClsOnRunnerDoneListener;
+    private static Class<?> sClsOnRecordingDoneListener;
+    private static Class<?> sClsSurfaceTextureSourceListener;
+
+    private static Method sFilterSetInputValue;
+
+    private static Constructor<?> sCtPoint;
+    private static Constructor<?> sCtQuad;
+
+    private static Method sLearningDoneListenerOnLearningDone;
+
+    private static Method sObjectEquals;
+    private static Method sObjectToString;
+
+    private static Class<?> sClsGraphRunner;
+    private static Method sGraphRunnerGetGraph;
+    private static Method sGraphRunnerSetDoneCallback;
+    private static Method sGraphRunnerRun;
+    private static Method sGraphRunnerGetError;
+    private static Method sGraphRunnerStop;
+
+    private static Method sFilterGraphGetFilter;
+    private static Method sFilterGraphTearDown;
+
+    private static Method sOnRunnerDoneListenerOnRunnerDone;
+
+    private static Class<?> sClsGraphEnvironment;
+    private static Constructor<?> sCtGraphEnvironment;
+    private static Method sGraphEnvironmentCreateGLEnvironment;
+    private static Method sGraphEnvironmentGetRunner;
+    private static Method sGraphEnvironmentAddReferences;
+    private static Method sGraphEnvironmentLoadGraph;
+    private static Method sGraphEnvironmentGetContext;
+
+    private static Method sFilterContextGetGLEnvironment;
+    private static Method sGLEnvironmentIsActive;
+    private static Method sGLEnvironmentActivate;
+    private static Method sGLEnvironmentDeactivate;
+    private static Method sSurfaceTextureTargetDisconnect;
+    private static Method sOnRecordingDoneListenerOnRecordingDone;
+    private static Method sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady;
+
+    private Object mLearningDoneListener;
+    private Object mRunnerDoneCallback;
+    private Object mSourceReadyCallback;
+    // A callback to finalize the media after the recording is done.
+    private Object mRecordingDoneListener;
+
+    static {
+        try {
+            sClassFilter = Class.forName("android.filterfw.core.Filter");
+            sFilterIsAvailable = sClassFilter.getMethod("isAvailable",
+                    String.class);
+        } catch (ClassNotFoundException ex) {
+            Log.v(TAG, "Can't find the class android.filterfw.core.Filter");
+        } catch (NoSuchMethodException e) {
+            Log.v(TAG, "Can't find the method Filter.isAvailable");
+        }
+    }
+
+    public static final int  EFFECT_NONE        = 0;
+    public static final int  EFFECT_GOOFY_FACE  = 1;
+    public static final int  EFFECT_BACKDROPPER = 2;
+
+    public static final int  EFFECT_GF_SQUEEZE     = 0;
+    public static final int  EFFECT_GF_BIG_EYES    = 1;
+    public static final int  EFFECT_GF_BIG_MOUTH   = 2;
+    public static final int  EFFECT_GF_SMALL_MOUTH = 3;
+    public static final int  EFFECT_GF_BIG_NOSE    = 4;
+    public static final int  EFFECT_GF_SMALL_EYES  = 5;
+    public static final int  NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1;
+
+    public static final int  EFFECT_MSG_STARTED_LEARNING = 0;
+    public static final int  EFFECT_MSG_DONE_LEARNING    = 1;
+    public static final int  EFFECT_MSG_SWITCHING_EFFECT = 2;
+    public static final int  EFFECT_MSG_EFFECTS_STOPPED  = 3;
+    public static final int  EFFECT_MSG_RECORDING_DONE   = 4;
+    public static final int  EFFECT_MSG_PREVIEW_RUNNING  = 5;
+
+    private Context mContext;
+    private Handler mHandler;
+
+    private CameraManager.CameraProxy mCameraDevice;
+    private CamcorderProfile mProfile;
+    private double mCaptureRate = 0;
+    private SurfaceTexture mPreviewSurfaceTexture;
+    private int mPreviewWidth;
+    private int mPreviewHeight;
+    private MediaRecorder.OnInfoListener mInfoListener;
+    private MediaRecorder.OnErrorListener mErrorListener;
+
+    private String mOutputFile;
+    private FileDescriptor mFd;
+    private int mOrientationHint = 0;
+    private long mMaxFileSize = 0;
+    private int mMaxDurationMs = 0;
+    private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;
+    private int mCameraDisplayOrientation;
+
+    private int mEffect = EFFECT_NONE;
+    private int mCurrentEffect = EFFECT_NONE;
+    private EffectsListener mEffectsListener;
+
+    private Object mEffectParameter;
+
+    private Object mGraphEnv;
+    private int mGraphId;
+    private Object mRunner = null;
+    private Object mOldRunner = null;
+
+    private SurfaceTexture mTextureSource;
+
+    private static final int STATE_CONFIGURE              = 0;
+    private static final int STATE_WAITING_FOR_SURFACE    = 1;
+    private static final int STATE_STARTING_PREVIEW       = 2;
+    private static final int STATE_PREVIEW                = 3;
+    private static final int STATE_RECORD                 = 4;
+    private static final int STATE_RELEASED               = 5;
+    private int mState = STATE_CONFIGURE;
+
+    private boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
+    private SoundClips.Player mSoundPlayer;
+
+    /** Determine if a given effect is supported at runtime
+     * Some effects require libraries not available on all devices
+     */
+    public static boolean isEffectSupported(int effectId) {
+        if (sFilterIsAvailable == null)  return false;
+
+        try {
+            switch (effectId) {
+                case EFFECT_GOOFY_FACE:
+                    return (Boolean) sFilterIsAvailable.invoke(null,
+                            "com.google.android.filterpacks.facedetect.GoofyRenderFilter");
+                case EFFECT_BACKDROPPER:
+                    return (Boolean) sFilterIsAvailable.invoke(null,
+                            "android.filterpacks.videoproc.BackDropperFilter");
+                default:
+                    return false;
+            }
+        } catch (Exception ex) {
+            Log.e(TAG, "Fail to check filter", ex);
+        }
+        return false;
+    }
+
+    public EffectsRecorder(Context context) {
+        if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")");
+
+        if (!sReflectionInited) {
+            try {
+                sFilterSetInputValue = sClassFilter.getMethod("setInputValue",
+                        new Class[] {String.class, Object.class});
+
+                Class<?> clsPoint = Class.forName("android.filterfw.geometry.Point");
+                sCtPoint = clsPoint.getConstructor(new Class[] {float.class,
+                        float.class});
+
+                Class<?> clsQuad = Class.forName("android.filterfw.geometry.Quad");
+                sCtQuad = clsQuad.getConstructor(new Class[] {clsPoint, clsPoint,
+                        clsPoint, clsPoint});
+
+                Class<?> clsBackDropperFilter = Class.forName(
+                        "android.filterpacks.videoproc.BackDropperFilter");
+                sClsLearningDoneListener = Class.forName(
+                        "android.filterpacks.videoproc.BackDropperFilter$LearningDoneListener");
+                sLearningDoneListenerOnLearningDone = sClsLearningDoneListener
+                        .getMethod("onLearningDone", new Class[] {clsBackDropperFilter});
+
+                sObjectEquals = Object.class.getMethod("equals", new Class[] {Object.class});
+                sObjectToString = Object.class.getMethod("toString");
+
+                sClsOnRunnerDoneListener = Class.forName(
+                        "android.filterfw.core.GraphRunner$OnRunnerDoneListener");
+                sOnRunnerDoneListenerOnRunnerDone = sClsOnRunnerDoneListener.getMethod(
+                        "onRunnerDone", new Class[] {int.class});
+
+                sClsGraphRunner = Class.forName("android.filterfw.core.GraphRunner");
+                sGraphRunnerGetGraph = sClsGraphRunner.getMethod("getGraph");
+                sGraphRunnerSetDoneCallback = sClsGraphRunner.getMethod(
+                        "setDoneCallback", new Class[] {sClsOnRunnerDoneListener});
+                sGraphRunnerRun = sClsGraphRunner.getMethod("run");
+                sGraphRunnerGetError = sClsGraphRunner.getMethod("getError");
+                sGraphRunnerStop = sClsGraphRunner.getMethod("stop");
+
+                Class<?> clsFilterContext = Class.forName("android.filterfw.core.FilterContext");
+                sFilterContextGetGLEnvironment = clsFilterContext.getMethod(
+                        "getGLEnvironment");
+
+                Class<?> clsFilterGraph = Class.forName("android.filterfw.core.FilterGraph");
+                sFilterGraphGetFilter = clsFilterGraph.getMethod("getFilter",
+                        new Class[] {String.class});
+                sFilterGraphTearDown = clsFilterGraph.getMethod("tearDown",
+                        new Class[] {clsFilterContext});
+
+                sClsGraphEnvironment = Class.forName("android.filterfw.GraphEnvironment");
+                sCtGraphEnvironment = sClsGraphEnvironment.getConstructor();
+                sGraphEnvironmentCreateGLEnvironment = sClsGraphEnvironment.getMethod(
+                        "createGLEnvironment");
+                sGraphEnvironmentGetRunner = sClsGraphEnvironment.getMethod(
+                        "getRunner", new Class[] {int.class, int.class});
+                sGraphEnvironmentAddReferences = sClsGraphEnvironment.getMethod(
+                        "addReferences", new Class[] {Object[].class});
+                sGraphEnvironmentLoadGraph = sClsGraphEnvironment.getMethod(
+                        "loadGraph", new Class[] {Context.class, int.class});
+                sGraphEnvironmentGetContext = sClsGraphEnvironment.getMethod(
+                        "getContext");
+
+                Class<?> clsGLEnvironment = Class.forName("android.filterfw.core.GLEnvironment");
+                sGLEnvironmentIsActive = clsGLEnvironment.getMethod("isActive");
+                sGLEnvironmentActivate = clsGLEnvironment.getMethod("activate");
+                sGLEnvironmentDeactivate = clsGLEnvironment.getMethod("deactivate");
+
+                Class<?> clsSurfaceTextureTarget = Class.forName(
+                        "android.filterpacks.videosrc.SurfaceTextureTarget");
+                sSurfaceTextureTargetDisconnect = clsSurfaceTextureTarget.getMethod(
+                        "disconnect", new Class[] {clsFilterContext});
+
+                sClsOnRecordingDoneListener = Class.forName(
+                        "android.filterpacks.videosink.MediaEncoderFilter$OnRecordingDoneListener");
+                sOnRecordingDoneListenerOnRecordingDone =
+                        sClsOnRecordingDoneListener.getMethod("onRecordingDone");
+
+                sClsSurfaceTextureSourceListener = Class.forName(
+                        "android.filterpacks.videosrc.SurfaceTextureSource$SurfaceTextureSourceListener");
+                sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady =
+                        sClsSurfaceTextureSourceListener.getMethod(
+                                "onSurfaceTextureSourceReady",
+                                new Class[] {SurfaceTexture.class});
+            } catch (Exception ex) {
+                throw new RuntimeException(ex);
+            }
+
+            sReflectionInited = true;
+        }
+
+        sEffectsRecorderIndex++;
+        Log.v(TAG, "Current effects recorder index is " + sEffectsRecorderIndex);
+        sEffectsRecorder = this;
+        SerializableInvocationHandler sih = new SerializableInvocationHandler(
+                sEffectsRecorderIndex);
+        mLearningDoneListener = Proxy.newProxyInstance(
+                sClsLearningDoneListener.getClassLoader(),
+                new Class[] {sClsLearningDoneListener}, sih);
+        mRunnerDoneCallback = Proxy.newProxyInstance(
+                sClsOnRunnerDoneListener.getClassLoader(),
+                new Class[] {sClsOnRunnerDoneListener}, sih);
+        mSourceReadyCallback = Proxy.newProxyInstance(
+                sClsSurfaceTextureSourceListener.getClassLoader(),
+                new Class[] {sClsSurfaceTextureSourceListener}, sih);
+        mRecordingDoneListener =  Proxy.newProxyInstance(
+                sClsOnRecordingDoneListener.getClassLoader(),
+                new Class[] {sClsOnRecordingDoneListener}, sih);
+
+        mContext = context;
+        mHandler = new Handler(Looper.getMainLooper());
+        mSoundPlayer = SoundClips.getPlayer(context);
+    }
+
+    public synchronized void setCamera(CameraManager.CameraProxy cameraDevice) {
+        switch (mState) {
+            case STATE_PREVIEW:
+                throw new RuntimeException("setCamera cannot be called while previewing!");
+            case STATE_RECORD:
+                throw new RuntimeException("setCamera cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException("setCamera called on an already released recorder!");
+            default:
+                break;
+        }
+
+        mCameraDevice = cameraDevice;
+    }
+
+    public void setProfile(CamcorderProfile profile) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setProfile cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException("setProfile called on an already released recorder!");
+            default:
+                break;
+        }
+        mProfile = profile;
+    }
+
+    public void setOutputFile(String outputFile) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setOutputFile cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException("setOutputFile called on an already released recorder!");
+            default:
+                break;
+        }
+
+        mOutputFile = outputFile;
+        mFd = null;
+    }
+
+    public void setOutputFile(FileDescriptor fd) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setOutputFile cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException("setOutputFile called on an already released recorder!");
+            default:
+                break;
+        }
+
+        mOutputFile = null;
+        mFd = fd;
+    }
+
+    /**
+     * Sets the maximum filesize (in bytes) of the recording session.
+     * This will be passed on to the MediaEncoderFilter and then to the
+     * MediaRecorder ultimately. If zero or negative, the MediaRecorder will
+     * disable the limit
+    */
+    public synchronized void setMaxFileSize(long maxFileSize) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setMaxFileSize cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "setMaxFileSize called on an already released recorder!");
+            default:
+                break;
+        }
+        mMaxFileSize = maxFileSize;
+    }
+
+    /**
+    * Sets the maximum recording duration (in ms) for the next recording session
+    * Setting it to zero (the default) disables the limit.
+    */
+    public synchronized void setMaxDuration(int maxDurationMs) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setMaxDuration cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "setMaxDuration called on an already released recorder!");
+            default:
+                break;
+        }
+        mMaxDurationMs = maxDurationMs;
+    }
+
+
+    public void setCaptureRate(double fps) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setCaptureRate cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "setCaptureRate called on an already released recorder!");
+            default:
+                break;
+        }
+
+        if (mLogVerbose) Log.v(TAG, "Setting time lapse capture rate to " + fps + " fps");
+        mCaptureRate = fps;
+    }
+
+    public void setPreviewSurfaceTexture(SurfaceTexture previewSurfaceTexture,
+                                  int previewWidth,
+                                  int previewHeight) {
+        if (mLogVerbose) Log.v(TAG, "setPreviewSurfaceTexture(" + this + ")");
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException(
+                    "setPreviewSurfaceTexture cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "setPreviewSurfaceTexture called on an already released recorder!");
+            default:
+                break;
+        }
+
+        mPreviewSurfaceTexture = previewSurfaceTexture;
+        mPreviewWidth = previewWidth;
+        mPreviewHeight = previewHeight;
+
+        switch (mState) {
+            case STATE_WAITING_FOR_SURFACE:
+                startPreview();
+                break;
+            case STATE_STARTING_PREVIEW:
+            case STATE_PREVIEW:
+                initializeEffect(true);
+                break;
+        }
+    }
+
+    public void setEffect(int effect, Object effectParameter) {
+        if (mLogVerbose) Log.v(TAG,
+                               "setEffect: effect ID " + effect +
+                               ", parameter " + effectParameter.toString());
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setEffect cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException("setEffect called on an already released recorder!");
+            default:
+                break;
+        }
+
+        mEffect = effect;
+        mEffectParameter = effectParameter;
+
+        if (mState == STATE_PREVIEW ||
+                mState == STATE_STARTING_PREVIEW) {
+            initializeEffect(false);
+        }
+    }
+
+    public interface EffectsListener {
+        public void onEffectsUpdate(int effectId, int effectMsg);
+        public void onEffectsError(Exception exception, String filePath);
+    }
+
+    public void setEffectsListener(EffectsListener listener) {
+        mEffectsListener = listener;
+    }
+
+    private void setFaceDetectOrientation() {
+        if (mCurrentEffect == EFFECT_GOOFY_FACE) {
+            Object rotateFilter = getGraphFilter(mRunner, "rotate");
+            Object metaRotateFilter = getGraphFilter(mRunner, "metarotate");
+            setInputValue(rotateFilter, "rotation", mOrientationHint);
+            int reverseDegrees = (360 - mOrientationHint) % 360;
+            setInputValue(metaRotateFilter, "rotation", reverseDegrees);
+        }
+    }
+
+    private void setRecordingOrientation() {
+        if (mState != STATE_RECORD && mRunner != null) {
+            Object bl = newInstance(sCtPoint, new Object[] {0, 0});
+            Object br = newInstance(sCtPoint, new Object[] {1, 0});
+            Object tl = newInstance(sCtPoint, new Object[] {0, 1});
+            Object tr = newInstance(sCtPoint, new Object[] {1, 1});
+            Object recordingRegion;
+            if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
+                // The back camera is not mirrored, so use a identity transform
+                recordingRegion = newInstance(sCtQuad, new Object[] {bl, br, tl, tr});
+            } else {
+                // Recording region needs to be tweaked for front cameras, since they
+                // mirror their preview
+                if (mOrientationHint == 0 || mOrientationHint == 180) {
+                    // Horizontal flip in landscape
+                    recordingRegion = newInstance(sCtQuad, new Object[] {br, bl, tr, tl});
+                } else {
+                    // Horizontal flip in portrait
+                    recordingRegion = newInstance(sCtQuad, new Object[] {tl, tr, bl, br});
+                }
+            }
+            Object recorder = getGraphFilter(mRunner, "recorder");
+            setInputValue(recorder, "inputRegion", recordingRegion);
+        }
+    }
+    public void setOrientationHint(int degrees) {
+        switch (mState) {
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                        "setOrientationHint called on an already released recorder!");
+            default:
+                break;
+        }
+        if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees);
+        mOrientationHint = degrees;
+        setFaceDetectOrientation();
+        setRecordingOrientation();
+    }
+
+    public void setCameraDisplayOrientation(int orientation) {
+        if (mState != STATE_CONFIGURE) {
+            throw new RuntimeException(
+                "setCameraDisplayOrientation called after configuration!");
+        }
+        mCameraDisplayOrientation = orientation;
+    }
+
+    public void setCameraFacing(int facing) {
+        switch (mState) {
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "setCameraFacing called on alrady released recorder!");
+            default:
+                break;
+        }
+        mCameraFacing = facing;
+        setRecordingOrientation();
+    }
+
+    public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setInfoListener cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "setInfoListener called on an already released recorder!");
+            default:
+                break;
+        }
+        mInfoListener = infoListener;
+    }
+
+    public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) {
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("setErrorListener cannot be called while recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "setErrorListener called on an already released recorder!");
+            default:
+                break;
+        }
+        mErrorListener = errorListener;
+    }
+
+    private void initializeFilterFramework() {
+        mGraphEnv = newInstance(sCtGraphEnvironment);
+        invoke(mGraphEnv, sGraphEnvironmentCreateGLEnvironment);
+
+        int videoFrameWidth = mProfile.videoFrameWidth;
+        int videoFrameHeight = mProfile.videoFrameHeight;
+        if (mCameraDisplayOrientation == 90 || mCameraDisplayOrientation == 270) {
+            int tmp = videoFrameWidth;
+            videoFrameWidth = videoFrameHeight;
+            videoFrameHeight = tmp;
+        }
+
+        invoke(mGraphEnv, sGraphEnvironmentAddReferences,
+                new Object[] {new Object[] {
+                "textureSourceCallback", mSourceReadyCallback,
+                "recordingWidth", videoFrameWidth,
+                "recordingHeight", videoFrameHeight,
+                "recordingProfile", mProfile,
+                "learningDoneListener", mLearningDoneListener,
+                "recordingDoneListener", mRecordingDoneListener}});
+        mRunner = null;
+        mGraphId = -1;
+        mCurrentEffect = EFFECT_NONE;
+    }
+
+    private synchronized void initializeEffect(boolean forceReset) {
+        if (forceReset ||
+            mCurrentEffect != mEffect ||
+            mCurrentEffect == EFFECT_BACKDROPPER) {
+
+            invoke(mGraphEnv, sGraphEnvironmentAddReferences,
+                    new Object[] {new Object[] {
+                    "previewSurfaceTexture", mPreviewSurfaceTexture,
+                    "previewWidth", mPreviewWidth,
+                    "previewHeight", mPreviewHeight,
+                    "orientation", mOrientationHint}});
+            if (mState == STATE_PREVIEW ||
+                    mState == STATE_STARTING_PREVIEW) {
+                // Switching effects while running. Inform video camera.
+                sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT);
+            }
+
+            switch (mEffect) {
+                case EFFECT_GOOFY_FACE:
+                    mGraphId = (Integer) invoke(mGraphEnv,
+                            sGraphEnvironmentLoadGraph,
+                            new Object[] {mContext, R.raw.goofy_face});
+                    break;
+                case EFFECT_BACKDROPPER:
+                    sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
+                    mGraphId = (Integer) invoke(mGraphEnv,
+                            sGraphEnvironmentLoadGraph,
+                            new Object[] {mContext, R.raw.backdropper});
+                    break;
+                default:
+                    throw new RuntimeException("Unknown effect ID" + mEffect + "!");
+            }
+            mCurrentEffect = mEffect;
+
+            mOldRunner = mRunner;
+            mRunner = invoke(mGraphEnv, sGraphEnvironmentGetRunner,
+                    new Object[] {mGraphId,
+                    getConstant(sClsGraphEnvironment, "MODE_ASYNCHRONOUS")});
+            invoke(mRunner, sGraphRunnerSetDoneCallback, new Object[] {mRunnerDoneCallback});
+            if (mLogVerbose) {
+                Log.v(TAG, "New runner: " + mRunner
+                      + ". Old runner: " + mOldRunner);
+            }
+            if (mState == STATE_PREVIEW ||
+                    mState == STATE_STARTING_PREVIEW) {
+                // Switching effects while running. Stop existing runner.
+                // The stop callback will take care of starting new runner.
+                mCameraDevice.stopPreview();
+                mCameraDevice.setPreviewTexture(null);
+                invoke(mOldRunner, sGraphRunnerStop);
+            }
+        }
+
+        switch (mCurrentEffect) {
+            case EFFECT_GOOFY_FACE:
+                tryEnableVideoStabilization(true);
+                Object goofyFilter = getGraphFilter(mRunner, "goofyrenderer");
+                setInputValue(goofyFilter, "currentEffect",
+                        ((Integer) mEffectParameter).intValue());
+                break;
+            case EFFECT_BACKDROPPER:
+                tryEnableVideoStabilization(false);
+                Object backgroundSrc = getGraphFilter(mRunner, "background");
+                if (ApiHelper.HAS_EFFECTS_RECORDING_CONTEXT_INPUT) {
+                    // Set the context first before setting sourceUrl to
+                    // guarantee the content URI get resolved properly.
+                    setInputValue(backgroundSrc, "context", mContext);
+                }
+                setInputValue(backgroundSrc, "sourceUrl", mEffectParameter);
+                // For front camera, the background video needs to be mirrored in the
+                // backdropper filter
+                if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+                    Object replacer = getGraphFilter(mRunner, "replacer");
+                    setInputValue(replacer, "mirrorBg", true);
+                    if (mLogVerbose) Log.v(TAG, "Setting the background to be mirrored");
+                }
+                break;
+            default:
+                break;
+        }
+        setFaceDetectOrientation();
+        setRecordingOrientation();
+    }
+
+    public synchronized void startPreview() {
+        if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")");
+
+        switch (mState) {
+            case STATE_STARTING_PREVIEW:
+            case STATE_PREVIEW:
+                // Already running preview
+                Log.w(TAG, "startPreview called when already running preview");
+                return;
+            case STATE_RECORD:
+                throw new RuntimeException("Cannot start preview when already recording!");
+            case STATE_RELEASED:
+                throw new RuntimeException("setEffect called on an already released recorder!");
+            default:
+                break;
+        }
+
+        if (mEffect == EFFECT_NONE) {
+            throw new RuntimeException("No effect selected!");
+        }
+        if (mEffectParameter == null) {
+            throw new RuntimeException("No effect parameter provided!");
+        }
+        if (mProfile == null) {
+            throw new RuntimeException("No recording profile provided!");
+        }
+        if (mPreviewSurfaceTexture == null) {
+            if (mLogVerbose) Log.v(TAG, "Passed a null surface; waiting for valid one");
+            mState = STATE_WAITING_FOR_SURFACE;
+            return;
+        }
+        if (mCameraDevice == null) {
+            throw new RuntimeException("No camera to record from!");
+        }
+
+        if (mLogVerbose) Log.v(TAG, "Initializing filter framework and running the graph.");
+        initializeFilterFramework();
+
+        initializeEffect(true);
+
+        mState = STATE_STARTING_PREVIEW;
+        invoke(mRunner, sGraphRunnerRun);
+        // Rest of preview startup handled in mSourceReadyCallback
+    }
+
+    private Object invokeObjectEquals(Object proxy, Object[] args) {
+        return Boolean.valueOf(proxy == args[0]);
+    }
+
+    private Object invokeObjectToString() {
+        return "Proxy-" + toString();
+    }
+
+    private void invokeOnLearningDone() {
+        if (mLogVerbose) Log.v(TAG, "Learning done callback triggered");
+        // Called in a processing thread, so have to post message back to UI
+        // thread
+        sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING);
+        enable3ALocks(true);
+    }
+
+    private void invokeOnRunnerDone(Object[] args) {
+        int runnerDoneResult = (Integer) args[0];
+        synchronized (EffectsRecorder.this) {
+            if (mLogVerbose) {
+                Log.v(TAG,
+                      "Graph runner done (" + EffectsRecorder.this
+                      + ", mRunner " + mRunner
+                      + ", mOldRunner " + mOldRunner + ")");
+            }
+            if (runnerDoneResult ==
+                    (Integer) getConstant(sClsGraphRunner, "RESULT_ERROR")) {
+                // Handle error case
+                Log.e(TAG, "Error running filter graph!");
+                Exception e = null;
+                if (mRunner != null) {
+                    e = (Exception) invoke(mRunner, sGraphRunnerGetError);
+                } else if (mOldRunner != null) {
+                    e = (Exception) invoke(mOldRunner, sGraphRunnerGetError);
+                }
+                raiseError(e);
+            }
+            if (mOldRunner != null) {
+                // Tear down old graph if available
+                if (mLogVerbose) Log.v(TAG, "Tearing down old graph.");
+                Object glEnv = getContextGLEnvironment(mGraphEnv);
+                if (glEnv != null && !(Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
+                    invoke(glEnv, sGLEnvironmentActivate);
+                }
+                getGraphTearDown(mOldRunner,
+                        invoke(mGraphEnv, sGraphEnvironmentGetContext));
+                if (glEnv != null && (Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
+                    invoke(glEnv, sGLEnvironmentDeactivate);
+                }
+                mOldRunner = null;
+            }
+            if (mState == STATE_PREVIEW ||
+                    mState == STATE_STARTING_PREVIEW) {
+                // Switching effects, start up the new runner
+                if (mLogVerbose) {
+                    Log.v(TAG, "Previous effect halted. Running graph again. state: "
+                            + mState);
+                }
+                tryEnable3ALocks(false);
+                // In case of an error, the graph restarts from beginning and in case
+                // of the BACKDROPPER effect, the learner re-learns the background.
+                // Hence, we need to show the learning dialogue to the user
+                // to avoid recording before the learning is done. Else, the user
+                // could start recording before the learning is done and the new
+                // background comes up later leading to an end result video
+                // with a heterogeneous background.
+                // For BACKDROPPER effect, this path is also executed sometimes at
+                // the end of a normal recording session. In such a case, the graph
+                // does not restart and hence the learner does not re-learn. So we
+                // do not want to show the learning dialogue then.
+                if (runnerDoneResult == (Integer) getConstant(
+                        sClsGraphRunner, "RESULT_ERROR")
+                        && mCurrentEffect == EFFECT_BACKDROPPER) {
+                    sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
+                }
+                invoke(mRunner, sGraphRunnerRun);
+            } else if (mState != STATE_RELEASED) {
+                // Shutting down effects
+                if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview");
+                tryEnable3ALocks(false);
+                sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED);
+            } else {
+                // STATE_RELEASED - camera will be/has been released as well, do nothing.
+            }
+        }
+    }
+
+    private void invokeOnSurfaceTextureSourceReady(Object[] args) {
+        SurfaceTexture source = (SurfaceTexture) args[0];
+        if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received");
+        synchronized (EffectsRecorder.this) {
+            mTextureSource = source;
+
+            if (mState == STATE_CONFIGURE) {
+                // Stop preview happened while the runner was doing startup tasks
+                // Since we haven't started anything up, don't do anything
+                // Rest of cleanup will happen in onRunnerDone
+                if (mLogVerbose) Log.v(TAG, "Ready callback: Already stopped, skipping.");
+                return;
+            }
+            if (mState == STATE_RELEASED) {
+                // EffectsRecorder has been released, so don't touch the camera device
+                // or anything else
+                if (mLogVerbose) Log.v(TAG, "Ready callback: Already released, skipping.");
+                return;
+            }
+            if (source == null) {
+                if (mLogVerbose) {
+                    Log.v(TAG, "Ready callback: source null! Looks like graph was closed!");
+                }
+                if (mState == STATE_PREVIEW ||
+                        mState == STATE_STARTING_PREVIEW ||
+                        mState == STATE_RECORD) {
+                    // A null source here means the graph is shutting down
+                    // unexpectedly, so we need to turn off preview before
+                    // the surface texture goes away.
+                    if (mLogVerbose) {
+                        Log.v(TAG, "Ready callback: State: " + mState
+                                + ". stopCameraPreview");
+                    }
+
+                    stopCameraPreview();
+                }
+                return;
+            }
+
+            // Lock AE/AWB to reduce transition flicker
+            tryEnable3ALocks(true);
+
+            mCameraDevice.stopPreview();
+            if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview");
+            mCameraDevice.setPreviewTexture(mTextureSource);
+
+            mCameraDevice.startPreview();
+
+            // Unlock AE/AWB after preview started
+            tryEnable3ALocks(false);
+
+            mState = STATE_PREVIEW;
+
+            if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete");
+
+            // Sending a message to listener that preview is complete
+            sendMessage(mCurrentEffect, EFFECT_MSG_PREVIEW_RUNNING);
+        }
+    }
+
+    private void invokeOnRecordingDone() {
+        // Forward the callback to the VideoModule object (as an asynchronous event).
+        if (mLogVerbose) Log.v(TAG, "Recording done callback triggered");
+        sendMessage(EFFECT_NONE, EFFECT_MSG_RECORDING_DONE);
+    }
+
+    public synchronized void startRecording() {
+        if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")");
+
+        switch (mState) {
+            case STATE_RECORD:
+                throw new RuntimeException("Already recording, cannot begin anew!");
+            case STATE_RELEASED:
+                throw new RuntimeException(
+                    "startRecording called on an already released recorder!");
+            default:
+                break;
+        }
+
+        if ((mOutputFile == null) && (mFd == null)) {
+            throw new RuntimeException("No output file name or descriptor provided!");
+        }
+
+        if (mState == STATE_CONFIGURE) {
+            startPreview();
+        }
+
+        Object recorder = getGraphFilter(mRunner, "recorder");
+        if (mFd != null) {
+            setInputValue(recorder, "outputFileDescriptor", mFd);
+        } else {
+            setInputValue(recorder, "outputFile", mOutputFile);
+        }
+        // It is ok to set the audiosource without checking for timelapse here
+        // since that check will be done in the MediaEncoderFilter itself
+        setInputValue(recorder, "audioSource", MediaRecorder.AudioSource.CAMCORDER);
+        setInputValue(recorder, "recordingProfile", mProfile);
+        setInputValue(recorder, "orientationHint", mOrientationHint);
+        // Important to set the timelapseinterval to 0 if the capture rate is not >0
+        // since the recorder does not get created every time the recording starts.
+        // The recorder infers whether the capture is timelapsed based on the value of
+        // this interval
+        boolean captureTimeLapse = mCaptureRate > 0;
+        if (captureTimeLapse) {
+            double timeBetweenFrameCapture = 1 / mCaptureRate;
+            setInputValue(recorder, "timelapseRecordingIntervalUs",
+                    (long) (1000000 * timeBetweenFrameCapture));
+
+        } else {
+            setInputValue(recorder, "timelapseRecordingIntervalUs", 0L);
+        }
+
+        if (mInfoListener != null) {
+            setInputValue(recorder, "infoListener", mInfoListener);
+        }
+        if (mErrorListener != null) {
+            setInputValue(recorder, "errorListener", mErrorListener);
+        }
+        setInputValue(recorder, "maxFileSize", mMaxFileSize);
+        setInputValue(recorder, "maxDurationMs", mMaxDurationMs);
+        setInputValue(recorder, "recording", true);
+        mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
+        mState = STATE_RECORD;
+    }
+
+    public synchronized void stopRecording() {
+        if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")");
+
+        switch (mState) {
+            case STATE_CONFIGURE:
+            case STATE_STARTING_PREVIEW:
+            case STATE_PREVIEW:
+                Log.w(TAG, "StopRecording called when recording not active!");
+                return;
+            case STATE_RELEASED:
+                throw new RuntimeException("stopRecording called on released EffectsRecorder!");
+            default:
+                break;
+        }
+        Object recorder = getGraphFilter(mRunner, "recorder");
+        setInputValue(recorder, "recording", false);
+        mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
+        mState = STATE_PREVIEW;
+    }
+
+    // Called to tell the filter graph that the display surfacetexture is not valid anymore.
+    // So the filter graph should not hold any reference to the surface created with that.
+    public synchronized void disconnectDisplay() {
+        if (mLogVerbose) Log.v(TAG, "Disconnecting the graph from the " +
+            "SurfaceTexture");
+        Object display = getGraphFilter(mRunner, "display");
+        invoke(display, sSurfaceTextureTargetDisconnect, new Object[] {
+                invoke(mGraphEnv, sGraphEnvironmentGetContext)});
+    }
+
+    // The VideoModule will call this to notify that the camera is being
+    // released to the outside world. This call should happen after the
+    // stopRecording call. Else, the effects may throw an exception.
+    // With the recording stopped, the stopPreview call will not try to
+    // release the camera again.
+    // This must be called in onPause() if the effects are ON.
+    public synchronized void disconnectCamera() {
+        if (mLogVerbose) Log.v(TAG, "Disconnecting the effects from Camera");
+        stopCameraPreview();
+        mCameraDevice = null;
+    }
+
+    // In a normal case, when the disconnect is not called, we should not
+    // set the camera device to null, since on return callback, we try to
+    // enable 3A locks, which need the cameradevice.
+    public synchronized void stopCameraPreview() {
+        if (mLogVerbose) Log.v(TAG, "Stopping camera preview.");
+        if (mCameraDevice == null) {
+            Log.d(TAG, "Camera already null. Nothing to disconnect");
+            return;
+        }
+        mCameraDevice.stopPreview();
+        mCameraDevice.setPreviewTexture(null);
+    }
+
+    // Stop and release effect resources
+    public synchronized void stopPreview() {
+        if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")");
+        switch (mState) {
+            case STATE_CONFIGURE:
+                Log.w(TAG, "StopPreview called when preview not active!");
+                return;
+            case STATE_RELEASED:
+                throw new RuntimeException("stopPreview called on released EffectsRecorder!");
+            default:
+                break;
+        }
+
+        if (mState == STATE_RECORD) {
+            stopRecording();
+        }
+
+        mCurrentEffect = EFFECT_NONE;
+
+        // This will not do anything if the camera has already been disconnected.
+        stopCameraPreview();
+
+        mState = STATE_CONFIGURE;
+        mOldRunner = mRunner;
+        invoke(mRunner, sGraphRunnerStop);
+        mRunner = null;
+        // Rest of stop and release handled in mRunnerDoneCallback
+    }
+
+    // Try to enable/disable video stabilization if supported; otherwise return false
+    // It is called from a synchronized block.
+    boolean tryEnableVideoStabilization(boolean toggle) {
+        if (mLogVerbose) Log.v(TAG, "tryEnableVideoStabilization.");
+        if (mCameraDevice == null) {
+            Log.d(TAG, "Camera already null. Not enabling video stabilization.");
+            return false;
+        }
+        Camera.Parameters params = mCameraDevice.getParameters();
+
+        String vstabSupported = params.get("video-stabilization-supported");
+        if ("true".equals(vstabSupported)) {
+            if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle);
+            params.set("video-stabilization", toggle ? "true" : "false");
+            mCameraDevice.setParameters(params);
+            return true;
+        }
+        if (mLogVerbose) Log.v(TAG, "Video stabilization not supported");
+        return false;
+    }
+
+    // Try to enable/disable 3A locks if supported; otherwise return false
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    synchronized boolean tryEnable3ALocks(boolean toggle) {
+        if (mLogVerbose) Log.v(TAG, "tryEnable3ALocks");
+        if (mCameraDevice == null) {
+            Log.d(TAG, "Camera already null. Not tryenabling 3A locks.");
+            return false;
+        }
+        Camera.Parameters params = mCameraDevice.getParameters();
+        if (Util.isAutoExposureLockSupported(params) &&
+            Util.isAutoWhiteBalanceLockSupported(params)) {
+            params.setAutoExposureLock(toggle);
+            params.setAutoWhiteBalanceLock(toggle);
+            mCameraDevice.setParameters(params);
+            return true;
+        }
+        return false;
+    }
+
+    // Try to enable/disable 3A locks if supported; otherwise, throw error
+    // Use this when locks are essential to success
+    synchronized void enable3ALocks(boolean toggle) {
+        if (mLogVerbose) Log.v(TAG, "Enable3ALocks");
+        if (mCameraDevice == null) {
+            Log.d(TAG, "Camera already null. Not enabling 3A locks.");
+            return;
+        }
+        Camera.Parameters params = mCameraDevice.getParameters();
+        if (!tryEnable3ALocks(toggle)) {
+            throw new RuntimeException("Attempt to lock 3A on camera with no locking support!");
+        }
+    }
+
+    static class SerializableInvocationHandler
+            implements InvocationHandler, Serializable {
+        private final int mEffectsRecorderIndex;
+        public SerializableInvocationHandler(int index) {
+            mEffectsRecorderIndex = index;
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args)
+                throws Throwable {
+            if (sEffectsRecorder == null) return null;
+            if (mEffectsRecorderIndex != sEffectsRecorderIndex) {
+                Log.v(TAG, "Ignore old callback " + mEffectsRecorderIndex);
+                return null;
+            }
+            if (method.equals(sObjectEquals)) {
+                return sEffectsRecorder.invokeObjectEquals(proxy, args);
+            } else if (method.equals(sObjectToString)) {
+                return sEffectsRecorder.invokeObjectToString();
+            } else if (method.equals(sLearningDoneListenerOnLearningDone)) {
+                sEffectsRecorder.invokeOnLearningDone();
+            } else if (method.equals(sOnRunnerDoneListenerOnRunnerDone)) {
+                sEffectsRecorder.invokeOnRunnerDone(args);
+            } else if (method.equals(
+                    sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady)) {
+                sEffectsRecorder.invokeOnSurfaceTextureSourceReady(args);
+            } else if (method.equals(sOnRecordingDoneListenerOnRecordingDone)) {
+                sEffectsRecorder.invokeOnRecordingDone();
+            }
+            return null;
+        }
+    }
+
+    // Indicates that all camera/recording activity needs to halt
+    public synchronized void release() {
+        if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")");
+
+        switch (mState) {
+            case STATE_RECORD:
+            case STATE_STARTING_PREVIEW:
+            case STATE_PREVIEW:
+                stopPreview();
+                // Fall-through
+            default:
+                if (mSoundPlayer != null) {
+                    mSoundPlayer.release();
+                    mSoundPlayer = null;
+                }
+                mState = STATE_RELEASED;
+                break;
+        }
+        sEffectsRecorder = null;
+    }
+
+    private void sendMessage(final int effect, final int msg) {
+        if (mEffectsListener != null) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mEffectsListener.onEffectsUpdate(effect, msg);
+                }
+            });
+        }
+    }
+
+    private void raiseError(final Exception exception) {
+        if (mEffectsListener != null) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mFd != null) {
+                        mEffectsListener.onEffectsError(exception, null);
+                    } else {
+                        mEffectsListener.onEffectsError(exception, mOutputFile);
+                    }
+                }
+            });
+        }
+    }
+
+    // invoke method on receiver with no arguments
+    private Object invoke(Object receiver, Method method) {
+        try {
+            return method.invoke(receiver);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    // invoke method on receiver with arguments
+    private Object invoke(Object receiver, Method method, Object[] args) {
+        try {
+            return method.invoke(receiver, args);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private void setInputValue(Object receiver, String key, Object value) {
+        try {
+            sFilterSetInputValue.invoke(receiver, new Object[] {key, value});
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private Object newInstance(Constructor<?> ct, Object[] initArgs) {
+        try {
+            return ct.newInstance(initArgs);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private Object newInstance(Constructor<?> ct) {
+        try {
+            return ct.newInstance();
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private Object getGraphFilter(Object receiver, String name) {
+        try {
+            return sFilterGraphGetFilter.invoke(sGraphRunnerGetGraph
+                    .invoke(receiver), new Object[] {name});
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private Object getContextGLEnvironment(Object receiver) {
+        try {
+            return sFilterContextGetGLEnvironment
+                    .invoke(sGraphEnvironmentGetContext.invoke(receiver));
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private void getGraphTearDown(Object receiver, Object filterContext) {
+        try {
+            sFilterGraphTearDown.invoke(sGraphRunnerGetGraph.invoke(receiver),
+                    new Object[]{filterContext});
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private Object getConstant(Class<?> cls, String name) {
+        try {
+            return cls.getDeclaredField(name).get(null);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+}
diff --git a/src/com/android/camera/Exif.java b/src/com/android/camera/Exif.java
new file mode 100644
index 0000000..c6ec6af
--- /dev/null
+++ b/src/com/android/camera/Exif.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 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.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.IOException;
+
+public class Exif {
+    private static final String TAG = "CameraExif";
+
+    public static ExifInterface getExif(byte[] jpegData) {
+        ExifInterface exif = new ExifInterface();
+        try {
+            exif.readExif(jpegData);
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to read EXIF data", e);
+        }
+        return exif;
+    }
+
+    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+    public static int getOrientation(ExifInterface exif) {
+        Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+        if (val == null) {
+            return 0;
+        } else {
+            return ExifInterface.getRotationForOrientationValue(val.shortValue());
+        }
+    }
+
+    public static int getOrientation(byte[] jpegData) {
+        if (jpegData == null) return 0;
+
+        ExifInterface exif = getExif(jpegData);
+        return getOrientation(exif);
+    }
+}
diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java
new file mode 100644
index 0000000..8bcb52f
--- /dev/null
+++ b/src/com/android/camera/FocusOverlayManager.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera.Area;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/* A class that handles everything about focus in still picture mode.
+ * This also handles the metering area because it is the same as focus area.
+ *
+ * The test cases:
+ * (1) The camera has continuous autofocus. Move the camera. Take a picture when
+ *     CAF is not in progress.
+ * (2) The camera has continuous autofocus. Move the camera. Take a picture when
+ *     CAF is in progress.
+ * (3) The camera has face detection. Point the camera at some faces. Hold the
+ *     shutter. Release to take a picture.
+ * (4) The camera has face detection. Point the camera at some faces. Single tap
+ *     the shutter to take a picture.
+ * (5) The camera has autofocus. Single tap the shutter to take a picture.
+ * (6) The camera has autofocus. Hold the shutter. Release to take a picture.
+ * (7) The camera has no autofocus. Single tap the shutter and take a picture.
+ * (8) The camera has autofocus and supports focus area. Touch the screen to
+ *     trigger autofocus. Take a picture.
+ * (9) The camera has autofocus and supports focus area. Touch the screen to
+ *     trigger autofocus. Wait until it times out.
+ * (10) The camera has no autofocus and supports metering area. Touch the screen
+ *     to change metering area.
+ */
+public class FocusOverlayManager {
+    private static final String TAG = "CAM_FocusManager";
+
+    private static final int RESET_TOUCH_FOCUS = 0;
+    private static final int RESET_TOUCH_FOCUS_DELAY = 3000;
+
+    private int mState = STATE_IDLE;
+    private static final int STATE_IDLE = 0; // Focus is not active.
+    private static final int STATE_FOCUSING = 1; // Focus is in progress.
+    // Focus is in progress and the camera should take a picture after focus finishes.
+    private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
+    private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
+    private static final int STATE_FAIL = 4; // Focus finishes and fails.
+
+    private boolean mInitialized;
+    private boolean mFocusAreaSupported;
+    private boolean mMeteringAreaSupported;
+    private boolean mLockAeAwbNeeded;
+    private boolean mAeAwbLock;
+    private Matrix mMatrix;
+
+    private int mPreviewWidth; // The width of the preview frame layout.
+    private int mPreviewHeight; // The height of the preview frame layout.
+    private boolean mMirror; // true if the camera is front-facing.
+    private int mDisplayOrientation;
+    private List<Object> mFocusArea; // focus area in driver format
+    private List<Object> mMeteringArea; // metering area in driver format
+    private String mFocusMode;
+    private String[] mDefaultFocusModes;
+    private String mOverrideFocusMode;
+    private Parameters mParameters;
+    private ComboPreferences mPreferences;
+    private Handler mHandler;
+    Listener mListener;
+    private boolean mPreviousMoving;
+    private boolean mFocusDefault;
+
+    private FocusUI mUI;
+
+    public  interface FocusUI {
+        public boolean hasFaces();
+        public void clearFocus();
+        public void setFocusPosition(int x, int y);
+        public void onFocusStarted();
+        public void onFocusSucceeded(boolean timeOut);
+        public void onFocusFailed(boolean timeOut);
+        public void pauseFaceDetection();
+        public void resumeFaceDetection();
+    }
+
+    public interface Listener {
+        public void autoFocus();
+        public void cancelAutoFocus();
+        public boolean capture();
+        public void startFaceDetection();
+        public void stopFaceDetection();
+        public void setFocusParameters();
+    }
+
+    private class MainHandler extends Handler {
+        public MainHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case RESET_TOUCH_FOCUS: {
+                    cancelAutoFocus();
+                    mListener.startFaceDetection();
+                    break;
+                }
+            }
+        }
+    }
+
+    public FocusOverlayManager(ComboPreferences preferences, String[] defaultFocusModes,
+            Parameters parameters, Listener listener,
+            boolean mirror, Looper looper, FocusUI ui) {
+        mHandler = new MainHandler(looper);
+        mMatrix = new Matrix();
+        mPreferences = preferences;
+        mDefaultFocusModes = defaultFocusModes;
+        setParameters(parameters);
+        mListener = listener;
+        setMirror(mirror);
+        mFocusDefault = true;
+        mUI = ui;
+    }
+
+    public void setParameters(Parameters parameters) {
+        // parameters can only be null when onConfigurationChanged is called
+        // before camera is open. We will just return in this case, because
+        // parameters will be set again later with the right parameters after
+        // camera is open.
+        if (parameters == null) return;
+        mParameters = parameters;
+        mFocusAreaSupported = Util.isFocusAreaSupported(parameters);
+        mMeteringAreaSupported = Util.isMeteringAreaSupported(parameters);
+        mLockAeAwbNeeded = (Util.isAutoExposureLockSupported(mParameters) ||
+                Util.isAutoWhiteBalanceLockSupported(mParameters));
+    }
+
+    public void setPreviewSize(int previewWidth, int previewHeight) {
+        if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) {
+            mPreviewWidth = previewWidth;
+            mPreviewHeight = previewHeight;
+            setMatrix();
+        }
+    }
+
+    public void setMirror(boolean mirror) {
+        mMirror = mirror;
+        setMatrix();
+    }
+
+    public void setDisplayOrientation(int displayOrientation) {
+        mDisplayOrientation = displayOrientation;
+        setMatrix();
+    }
+
+    private void setMatrix() {
+        if (mPreviewWidth != 0 && mPreviewHeight != 0) {
+            Matrix matrix = new Matrix();
+            Util.prepareMatrix(matrix, mMirror, mDisplayOrientation,
+                    mPreviewWidth, mPreviewHeight);
+            // In face detection, the matrix converts the driver coordinates to UI
+            // coordinates. In tap focus, the inverted matrix converts the UI
+            // coordinates to driver coordinates.
+            matrix.invert(mMatrix);
+            mInitialized = true;
+        }
+    }
+
+    private void lockAeAwbIfNeeded() {
+        if (mLockAeAwbNeeded && !mAeAwbLock) {
+            mAeAwbLock = true;
+            mListener.setFocusParameters();
+        }
+    }
+
+    private void unlockAeAwbIfNeeded() {
+        if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) {
+            mAeAwbLock = false;
+            mListener.setFocusParameters();
+        }
+    }
+
+    public void onShutterDown() {
+        if (!mInitialized) return;
+
+        boolean autoFocusCalled = false;
+        if (needAutoFocusCall()) {
+            // Do not focus if touch focus has been triggered.
+            if (mState != STATE_SUCCESS && mState != STATE_FAIL) {
+                autoFocus();
+                autoFocusCalled = true;
+            }
+        }
+
+        if (!autoFocusCalled) lockAeAwbIfNeeded();
+    }
+
+    public void onShutterUp() {
+        if (!mInitialized) return;
+
+        if (needAutoFocusCall()) {
+            // User releases half-pressed focus key.
+            if (mState == STATE_FOCUSING || mState == STATE_SUCCESS
+                    || mState == STATE_FAIL) {
+                cancelAutoFocus();
+            }
+        }
+
+        // Unlock AE and AWB after cancelAutoFocus. Camera API does not
+        // guarantee setParameters can be called during autofocus.
+        unlockAeAwbIfNeeded();
+    }
+
+    public void doSnap() {
+        if (!mInitialized) return;
+
+        // If the user has half-pressed the shutter and focus is completed, we
+        // can take the photo right away. If the focus mode is infinity, we can
+        // also take the photo.
+        if (!needAutoFocusCall() || (mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+            capture();
+        } else if (mState == STATE_FOCUSING) {
+            // Half pressing the shutter (i.e. the focus button event) will
+            // already have requested AF for us, so just request capture on
+            // focus here.
+            mState = STATE_FOCUSING_SNAP_ON_FINISH;
+        } else if (mState == STATE_IDLE) {
+            // We didn't do focus. This can happen if the user press focus key
+            // while the snapshot is still in progress. The user probably wants
+            // the next snapshot as soon as possible, so we just do a snapshot
+            // without focusing again.
+            capture();
+        }
+    }
+
+    public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
+        if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+            // Take the picture no matter focus succeeds or fails. No need
+            // to play the AF sound if we're about to play the shutter
+            // sound.
+            if (focused) {
+                mState = STATE_SUCCESS;
+            } else {
+                mState = STATE_FAIL;
+            }
+            updateFocusUI();
+            capture();
+        } else if (mState == STATE_FOCUSING) {
+            // This happens when (1) user is half-pressing the focus key or
+            // (2) touch focus is triggered. Play the focus tone. Do not
+            // take the picture now.
+            if (focused) {
+                mState = STATE_SUCCESS;
+            } else {
+                mState = STATE_FAIL;
+            }
+            updateFocusUI();
+            // If this is triggered by touch focus, cancel focus after a
+            // while.
+            if (!mFocusDefault) {
+                mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+            }
+            if (shutterButtonPressed) {
+                // Lock AE & AWB so users can half-press shutter and recompose.
+                lockAeAwbIfNeeded();
+            }
+        } else if (mState == STATE_IDLE) {
+            // User has released the focus key before focus completes.
+            // Do nothing.
+        }
+    }
+
+    public void onAutoFocusMoving(boolean moving) {
+        if (!mInitialized) return;
+
+
+        // Ignore if the camera has detected some faces.
+        if (mUI.hasFaces()) {
+            mUI.clearFocus();
+            return;
+        }
+
+        // Ignore if we have requested autofocus. This method only handles
+        // continuous autofocus.
+        if (mState != STATE_IDLE) return;
+
+        // animate on false->true trasition only b/8219520
+        if (moving && !mPreviousMoving) {
+            mUI.onFocusStarted();
+        } else if (!moving) {
+            mUI.onFocusSucceeded(true);
+        }
+        mPreviousMoving = moving;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void initializeFocusAreas(int x, int y) {
+        if (mFocusArea == null) {
+            mFocusArea = new ArrayList<Object>();
+            mFocusArea.add(new Area(new Rect(), 1));
+        }
+
+        // Convert the coordinates to driver format.
+        calculateTapArea(x, y, 1f, ((Area) mFocusArea.get(0)).rect);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void initializeMeteringAreas(int x, int y) {
+        if (mMeteringArea == null) {
+            mMeteringArea = new ArrayList<Object>();
+            mMeteringArea.add(new Area(new Rect(), 1));
+        }
+
+        // Convert the coordinates to driver format.
+        // AE area is bigger because exposure is sensitive and
+        // easy to over- or underexposure if area is too small.
+        calculateTapArea(x, y, 1.5f, ((Area) mMeteringArea.get(0)).rect);
+    }
+
+    public void onSingleTapUp(int x, int y) {
+        if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) return;
+
+        // Let users be able to cancel previous touch focus.
+        if ((!mFocusDefault) && (mState == STATE_FOCUSING ||
+                    mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+            cancelAutoFocus();
+        }
+        if (mPreviewWidth == 0 || mPreviewHeight == 0) return;
+        mFocusDefault = false;
+        // Initialize mFocusArea.
+        if (mFocusAreaSupported) {
+            initializeFocusAreas(x, y);
+        }
+        // Initialize mMeteringArea.
+        if (mMeteringAreaSupported) {
+            initializeMeteringAreas(x, y);
+        }
+
+        // Use margin to set the focus indicator to the touched area.
+        mUI.setFocusPosition(x, y);
+
+        // Stop face detection because we want to specify focus and metering area.
+        mListener.stopFaceDetection();
+
+        // Set the focus area and metering area.
+        mListener.setFocusParameters();
+        if (mFocusAreaSupported) {
+            autoFocus();
+        } else {  // Just show the indicator in all other cases.
+            updateFocusUI();
+            // Reset the metering area in 3 seconds.
+            mHandler.removeMessages(RESET_TOUCH_FOCUS);
+            mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+        }
+    }
+
+    public void onPreviewStarted() {
+        mState = STATE_IDLE;
+    }
+
+    public void onPreviewStopped() {
+        // If auto focus was in progress, it would have been stopped.
+        mState = STATE_IDLE;
+        resetTouchFocus();
+        updateFocusUI();
+    }
+
+    public void onCameraReleased() {
+        onPreviewStopped();
+    }
+
+    private void autoFocus() {
+        Log.v(TAG, "Start autofocus.");
+        mListener.autoFocus();
+        mState = STATE_FOCUSING;
+        // Pause the face view because the driver will keep sending face
+        // callbacks after the focus completes.
+        mUI.pauseFaceDetection();
+        updateFocusUI();
+        mHandler.removeMessages(RESET_TOUCH_FOCUS);
+    }
+
+    private void cancelAutoFocus() {
+        Log.v(TAG, "Cancel autofocus.");
+
+        // Reset the tap area before calling mListener.cancelAutofocus.
+        // Otherwise, focus mode stays at auto and the tap area passed to the
+        // driver is not reset.
+        resetTouchFocus();
+        mListener.cancelAutoFocus();
+        mUI.resumeFaceDetection();
+        mState = STATE_IDLE;
+        updateFocusUI();
+        mHandler.removeMessages(RESET_TOUCH_FOCUS);
+    }
+
+    private void capture() {
+        if (mListener.capture()) {
+            mState = STATE_IDLE;
+            mHandler.removeMessages(RESET_TOUCH_FOCUS);
+        }
+    }
+
+    public String getFocusMode() {
+        if (mOverrideFocusMode != null) return mOverrideFocusMode;
+        if (mParameters == null) return Parameters.FOCUS_MODE_AUTO;
+        List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
+
+        if (mFocusAreaSupported && !mFocusDefault) {
+            // Always use autofocus in tap-to-focus.
+            mFocusMode = Parameters.FOCUS_MODE_AUTO;
+        } else {
+            // The default is continuous autofocus.
+            mFocusMode = mPreferences.getString(
+                    CameraSettings.KEY_FOCUS_MODE, null);
+
+            // Try to find a supported focus mode from the default list.
+            if (mFocusMode == null) {
+                for (int i = 0; i < mDefaultFocusModes.length; i++) {
+                    String mode = mDefaultFocusModes[i];
+                    if (Util.isSupported(mode, supportedFocusModes)) {
+                        mFocusMode = mode;
+                        break;
+                    }
+                }
+            }
+        }
+        if (!Util.isSupported(mFocusMode, supportedFocusModes)) {
+            // For some reasons, the driver does not support the current
+            // focus mode. Fall back to auto.
+            if (Util.isSupported(Parameters.FOCUS_MODE_AUTO,
+                    mParameters.getSupportedFocusModes())) {
+                mFocusMode = Parameters.FOCUS_MODE_AUTO;
+            } else {
+                mFocusMode = mParameters.getFocusMode();
+            }
+        }
+        return mFocusMode;
+    }
+
+    public List getFocusAreas() {
+        return mFocusArea;
+    }
+
+    public List getMeteringAreas() {
+        return mMeteringArea;
+    }
+
+    public void updateFocusUI() {
+        if (!mInitialized) return;
+        // Show only focus indicator or face indicator.
+
+        if (mState == STATE_IDLE) {
+            if (mFocusDefault) {
+                mUI.clearFocus();
+            } else {
+                // Users touch on the preview and the indicator represents the
+                // metering area. Either focus area is not supported or
+                // autoFocus call is not required.
+                mUI.onFocusStarted();
+            }
+        } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+            mUI.onFocusStarted();
+        } else {
+            if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
+                // TODO: check HAL behavior and decide if this can be removed.
+                mUI.onFocusSucceeded(false);
+            } else if (mState == STATE_SUCCESS) {
+                mUI.onFocusSucceeded(false);
+            } else if (mState == STATE_FAIL) {
+                mUI.onFocusFailed(false);
+            }
+        }
+    }
+
+    public void resetTouchFocus() {
+        if (!mInitialized) return;
+
+        // Put focus indicator to the center. clear reset position
+        mUI.clearFocus();
+        // Initialize mFocusArea.
+        if (mFocusAreaSupported) {
+            initializeFocusAreas(mPreviewWidth / 2, mPreviewHeight / 2);
+        }
+        // Initialize mMeteringArea.
+        if (mMeteringAreaSupported) {
+            initializeMeteringAreas(mPreviewWidth / 2, mPreviewHeight / 2);
+        }
+        mFocusDefault = true;
+    }
+
+    private void calculateTapArea(int x, int y, float areaMultiple, Rect rect) {
+        int areaSize = (int) (Math.min(mPreviewWidth, mPreviewHeight) * areaMultiple / 20);
+        int left = Util.clamp(x - areaSize, 0, mPreviewWidth - 2 * areaSize);
+        int top = Util.clamp(y - areaSize, 0, mPreviewHeight - 2 * areaSize);
+
+        RectF rectF = new RectF(left, top, left + 2 * areaSize, top + 2 * areaSize);
+        mMatrix.mapRect(rectF);
+        Util.rectFToRect(rectF, rect);
+    }
+
+    /* package */ int getFocusState() {
+        return mState;
+    }
+
+    public boolean isFocusCompleted() {
+        return mState == STATE_SUCCESS || mState == STATE_FAIL;
+    }
+
+    public boolean isFocusingSnapOnFinish() {
+        return mState == STATE_FOCUSING_SNAP_ON_FINISH;
+    }
+
+    public void removeMessages() {
+        mHandler.removeMessages(RESET_TOUCH_FOCUS);
+    }
+
+    public void overrideFocusMode(String focusMode) {
+        mOverrideFocusMode = focusMode;
+    }
+
+    public void setAeAwbLock(boolean lock) {
+        mAeAwbLock = lock;
+    }
+
+    public boolean getAeAwbLock() {
+        return mAeAwbLock;
+    }
+
+    private boolean needAutoFocusCall() {
+        String focusMode = getFocusMode();
+        return !(focusMode.equals(Parameters.FOCUS_MODE_INFINITY)
+                || focusMode.equals(Parameters.FOCUS_MODE_FIXED)
+                || focusMode.equals(Parameters.FOCUS_MODE_EDOF));
+    }
+}
diff --git a/src/com/android/camera/IconListPreference.java b/src/com/android/camera/IconListPreference.java
new file mode 100644
index 0000000..e5f75d3
--- /dev/null
+++ b/src/com/android/camera/IconListPreference.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.R;
+
+import java.util.List;
+
+/** A {@code ListPreference} where each entry has a corresponding icon. */
+public class IconListPreference extends ListPreference {
+    private int mSingleIconId;
+    private int mIconIds[];
+    private int mLargeIconIds[];
+    private int mImageIds[];
+    private boolean mUseSingleIcon;
+
+    public IconListPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.IconListPreference, 0, 0);
+        Resources res = context.getResources();
+        mSingleIconId = a.getResourceId(
+                R.styleable.IconListPreference_singleIcon, 0);
+        mIconIds = getIds(res, a.getResourceId(
+                R.styleable.IconListPreference_icons, 0));
+        mLargeIconIds = getIds(res, a.getResourceId(
+                R.styleable.IconListPreference_largeIcons, 0));
+        mImageIds = getIds(res, a.getResourceId(
+                R.styleable.IconListPreference_images, 0));
+        a.recycle();
+    }
+
+    public int getSingleIcon() {
+        return mSingleIconId;
+    }
+
+    public int[] getIconIds() {
+        return mIconIds;
+    }
+
+    public int[] getLargeIconIds() {
+        return mLargeIconIds;
+    }
+
+    public int[] getImageIds() {
+        return mImageIds;
+    }
+
+    public boolean getUseSingleIcon() {
+        return mUseSingleIcon;
+    }
+
+    public void setIconIds(int[] iconIds) {
+        mIconIds = iconIds;
+    }
+
+    public void setLargeIconIds(int[] largeIconIds) {
+        mLargeIconIds = largeIconIds;
+    }
+
+    public void setUseSingleIcon(boolean useSingle) {
+        mUseSingleIcon = useSingle;
+    }
+
+    private int[] getIds(Resources res, int iconsRes) {
+        if (iconsRes == 0) return null;
+        TypedArray array = res.obtainTypedArray(iconsRes);
+        int n = array.length();
+        int ids[] = new int[n];
+        for (int i = 0; i < n; ++i) {
+            ids[i] = array.getResourceId(i, 0);
+        }
+        array.recycle();
+        return ids;
+    }
+
+    @Override
+    public void filterUnsupported(List<String> supported) {
+        CharSequence entryValues[] = getEntryValues();
+        IntArray iconIds = new IntArray();
+        IntArray largeIconIds = new IntArray();
+        IntArray imageIds = new IntArray();
+
+        for (int i = 0, len = entryValues.length; i < len; i++) {
+            if (supported.indexOf(entryValues[i].toString()) >= 0) {
+                if (mIconIds != null) iconIds.add(mIconIds[i]);
+                if (mLargeIconIds != null) largeIconIds.add(mLargeIconIds[i]);
+                if (mImageIds != null) imageIds.add(mImageIds[i]);
+            }
+        }
+        if (mIconIds != null) mIconIds = iconIds.toArray(new int[iconIds.size()]);
+        if (mLargeIconIds != null) {
+            mLargeIconIds = largeIconIds.toArray(new int[largeIconIds.size()]);
+        }
+        if (mImageIds != null) mImageIds = imageIds.toArray(new int[imageIds.size()]);
+        super.filterUnsupported(supported);
+    }
+}
diff --git a/src/com/android/camera/ImageTaskManager.java b/src/com/android/camera/ImageTaskManager.java
new file mode 100644
index 0000000..1324942
--- /dev/null
+++ b/src/com/android/camera/ImageTaskManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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;
+
+/**
+ * The interface for background image processing task manager.
+ */
+interface ImageTaskManager {
+
+    /**
+     * Callback interface for task events.
+     */
+    public interface TaskListener {
+        public void onTaskQueued(String filePath, Uri imageUri);
+        public void onTaskDone(String filePath, Uri imageUri);
+        public void onTaskProgress(
+                String filePath, Uri imageUri, int progress);
+    }
+
+    public void addTaskListener(TaskListener l);
+
+    public void removeTaskListener(TaskListener l);
+
+    /**
+     * Get task progress by Uri.
+     *
+     * @param uri         The Uri of the final image file to identify the task.
+     * @return            Integer from 0 to 100, or -1. The percentage of the task done
+     *                    so far. -1 means not found.
+     */
+    public int getTaskProgress(Uri uri);
+}
diff --git a/src/com/android/camera/IntArray.java b/src/com/android/camera/IntArray.java
new file mode 100644
index 0000000..a2550db
--- /dev/null
+++ b/src/com/android/camera/IntArray.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 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;
+
+public class IntArray {
+    private static final int INIT_CAPACITY = 8;
+
+    private int mData[] = new int[INIT_CAPACITY];
+    private int mSize = 0;
+
+    public void add(int value) {
+        if (mData.length == mSize) {
+            int temp[] = new int[mSize + mSize];
+            System.arraycopy(mData, 0, temp, 0, mSize);
+            mData = temp;
+        }
+        mData[mSize++] = value;
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    public int[] toArray(int[] result) {
+        if (result == null || result.length < mSize) {
+            result = new int[mSize];
+        }
+        System.arraycopy(mData, 0, result, 0, mSize);
+        return result;
+    }
+}
diff --git a/src/com/android/camera/ListPreference.java b/src/com/android/camera/ListPreference.java
new file mode 100644
index 0000000..38866de
--- /dev/null
+++ b/src/com/android/camera/ListPreference.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A type of <code>CameraPreference</code> whose number of possible values
+ * is limited.
+ */
+public class ListPreference extends CameraPreference {
+    private static final String TAG = "ListPreference";
+    private final String mKey;
+    private String mValue;
+    private final CharSequence[] mDefaultValues;
+
+    private CharSequence[] mEntries;
+    private CharSequence[] mEntryValues;
+    private CharSequence[] mLabels;
+    private boolean mLoaded = false;
+
+    public ListPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.ListPreference, 0, 0);
+
+        mKey = Util.checkNotNull(
+                a.getString(R.styleable.ListPreference_key));
+
+        // We allow the defaultValue attribute to be a string or an array of
+        // strings. The reason we need multiple default values is that some
+        // of them may be unsupported on a specific platform (for example,
+        // continuous auto-focus). In that case the first supported value
+        // in the array will be used.
+        int attrDefaultValue = R.styleable.ListPreference_defaultValue;
+        TypedValue tv = a.peekValue(attrDefaultValue);
+        if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) {
+            mDefaultValues = a.getTextArray(attrDefaultValue);
+        } else {
+            mDefaultValues = new CharSequence[1];
+            mDefaultValues[0] = a.getString(attrDefaultValue);
+        }
+
+        setEntries(a.getTextArray(R.styleable.ListPreference_entries));
+        setEntryValues(a.getTextArray(
+                R.styleable.ListPreference_entryValues));
+        setLabels(a.getTextArray(
+                R.styleable.ListPreference_labelList));
+        a.recycle();
+    }
+
+    public String getKey() {
+        return mKey;
+    }
+
+    public CharSequence[] getEntries() {
+        return mEntries;
+    }
+
+    public CharSequence[] getEntryValues() {
+        return mEntryValues;
+    }
+
+    public CharSequence[] getLabels() {
+        return mLabels;
+    }
+
+    public void setEntries(CharSequence entries[]) {
+        mEntries = entries == null ? new CharSequence[0] : entries;
+    }
+
+    public void setEntryValues(CharSequence values[]) {
+        mEntryValues = values == null ? new CharSequence[0] : values;
+    }
+
+    public void setLabels(CharSequence labels[]) {
+        mLabels = labels == null ? new CharSequence[0] : labels;
+    }
+
+    public String getValue() {
+        if (!mLoaded) {
+            mValue = getSharedPreferences().getString(mKey,
+                    findSupportedDefaultValue());
+            mLoaded = true;
+        }
+        return mValue;
+    }
+
+    // Find the first value in mDefaultValues which is supported.
+    private String findSupportedDefaultValue() {
+        for (int i = 0; i < mDefaultValues.length; i++) {
+            for (int j = 0; j < mEntryValues.length; j++) {
+                // Note that mDefaultValues[i] may be null (if unspecified
+                // in the xml file).
+                if (mEntryValues[j].equals(mDefaultValues[i])) {
+                    return mDefaultValues[i].toString();
+                }
+            }
+        }
+        return null;
+    }
+
+    public void setValue(String value) {
+        if (findIndexOfValue(value) < 0) throw new IllegalArgumentException();
+        mValue = value;
+        persistStringValue(value);
+    }
+
+    public void setValueIndex(int index) {
+        setValue(mEntryValues[index].toString());
+    }
+
+    public int findIndexOfValue(String value) {
+        for (int i = 0, n = mEntryValues.length; i < n; ++i) {
+            if (Util.equals(mEntryValues[i], value)) return i;
+        }
+        return -1;
+    }
+
+    public int getCurrentIndex() {
+        return findIndexOfValue(getValue());
+    }
+
+    public String getEntry() {
+        return mEntries[findIndexOfValue(getValue())].toString();
+    }
+
+    public String getLabel() {
+        return mLabels[findIndexOfValue(getValue())].toString();
+    }
+
+    protected void persistStringValue(String value) {
+        SharedPreferences.Editor editor = getSharedPreferences().edit();
+        editor.putString(mKey, value);
+        editor.apply();
+    }
+
+    @Override
+    public void reloadValue() {
+        this.mLoaded = false;
+    }
+
+    public void filterUnsupported(List<String> supported) {
+        ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+        ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+        for (int i = 0, len = mEntryValues.length; i < len; i++) {
+            if (supported.indexOf(mEntryValues[i].toString()) >= 0) {
+                entries.add(mEntries[i]);
+                entryValues.add(mEntryValues[i]);
+            }
+        }
+        int size = entries.size();
+        mEntries = entries.toArray(new CharSequence[size]);
+        mEntryValues = entryValues.toArray(new CharSequence[size]);
+    }
+
+    public void filterDuplicated() {
+        ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+        ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+        for (int i = 0, len = mEntryValues.length; i < len; i++) {
+            if (!entries.contains(mEntries[i])) {
+                entries.add(mEntries[i]);
+                entryValues.add(mEntryValues[i]);
+            }
+        }
+        int size = entries.size();
+        mEntries = entries.toArray(new CharSequence[size]);
+        mEntryValues = entryValues.toArray(new CharSequence[size]);
+    }
+
+    public void print() {
+        Log.v(TAG, "Preference key=" + getKey() + ". value=" + getValue());
+        for (int i = 0; i < mEntryValues.length; i++) {
+            Log.v(TAG, "entryValues[" + i + "]=" + mEntryValues[i]);
+        }
+    }
+}
diff --git a/src/com/android/camera/LocationManager.java b/src/com/android/camera/LocationManager.java
new file mode 100644
index 0000000..fcf21b6
--- /dev/null
+++ b/src/com/android/camera/LocationManager.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2011 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.location.Location;
+import android.location.LocationProvider;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * A class that handles everything about location.
+ */
+public class LocationManager {
+    private static final String TAG = "LocationManager";
+
+    private Context mContext;
+    private Listener mListener;
+    private android.location.LocationManager mLocationManager;
+    private boolean mRecordLocation;
+
+    LocationListener [] mLocationListeners = new LocationListener[] {
+            new LocationListener(android.location.LocationManager.GPS_PROVIDER),
+            new LocationListener(android.location.LocationManager.NETWORK_PROVIDER)
+    };
+
+    public interface Listener {
+        public void showGpsOnScreenIndicator(boolean hasSignal);
+        public void hideGpsOnScreenIndicator();
+   }
+
+    public LocationManager(Context context, Listener listener) {
+        mContext = context;
+        mListener = listener;
+    }
+
+    public Location getCurrentLocation() {
+        if (!mRecordLocation) return null;
+
+        // go in best to worst order
+        for (int i = 0; i < mLocationListeners.length; i++) {
+            Location l = mLocationListeners[i].current();
+            if (l != null) return l;
+        }
+        Log.d(TAG, "No location received yet.");
+        return null;
+    }
+
+    public void recordLocation(boolean recordLocation) {
+        if (mRecordLocation != recordLocation) {
+            mRecordLocation = recordLocation;
+            if (recordLocation) {
+                startReceivingLocationUpdates();
+            } else {
+                stopReceivingLocationUpdates();
+            }
+        }
+    }
+
+    private void startReceivingLocationUpdates() {
+        if (mLocationManager == null) {
+            mLocationManager = (android.location.LocationManager)
+                    mContext.getSystemService(Context.LOCATION_SERVICE);
+        }
+        if (mLocationManager != null) {
+            try {
+                mLocationManager.requestLocationUpdates(
+                        android.location.LocationManager.NETWORK_PROVIDER,
+                        1000,
+                        0F,
+                        mLocationListeners[1]);
+            } catch (SecurityException ex) {
+                Log.i(TAG, "fail to request location update, ignore", ex);
+            } catch (IllegalArgumentException ex) {
+                Log.d(TAG, "provider does not exist " + ex.getMessage());
+            }
+            try {
+                mLocationManager.requestLocationUpdates(
+                        android.location.LocationManager.GPS_PROVIDER,
+                        1000,
+                        0F,
+                        mLocationListeners[0]);
+                if (mListener != null) mListener.showGpsOnScreenIndicator(false);
+            } catch (SecurityException ex) {
+                Log.i(TAG, "fail to request location update, ignore", ex);
+            } catch (IllegalArgumentException ex) {
+                Log.d(TAG, "provider does not exist " + ex.getMessage());
+            }
+            Log.d(TAG, "startReceivingLocationUpdates");
+        }
+    }
+
+    private void stopReceivingLocationUpdates() {
+        if (mLocationManager != null) {
+            for (int i = 0; i < mLocationListeners.length; i++) {
+                try {
+                    mLocationManager.removeUpdates(mLocationListeners[i]);
+                } catch (Exception ex) {
+                    Log.i(TAG, "fail to remove location listners, ignore", ex);
+                }
+            }
+            Log.d(TAG, "stopReceivingLocationUpdates");
+        }
+        if (mListener != null) mListener.hideGpsOnScreenIndicator();
+    }
+
+    private class LocationListener
+            implements android.location.LocationListener {
+        Location mLastLocation;
+        boolean mValid = false;
+        String mProvider;
+
+        public LocationListener(String provider) {
+            mProvider = provider;
+            mLastLocation = new Location(mProvider);
+        }
+
+        @Override
+        public void onLocationChanged(Location newLocation) {
+            if (newLocation.getLatitude() == 0.0
+                    && newLocation.getLongitude() == 0.0) {
+                // Hack to filter out 0.0,0.0 locations
+                return;
+            }
+            // If GPS is available before start camera, we won't get status
+            // update so update GPS indicator when we receive data.
+            if (mListener != null && mRecordLocation &&
+                    android.location.LocationManager.GPS_PROVIDER.equals(mProvider)) {
+                mListener.showGpsOnScreenIndicator(true);
+            }
+            if (!mValid) {
+                Log.d(TAG, "Got first location.");
+            }
+            mLastLocation.set(newLocation);
+            mValid = true;
+        }
+
+        @Override
+        public void onProviderEnabled(String provider) {
+        }
+
+        @Override
+        public void onProviderDisabled(String provider) {
+            mValid = false;
+        }
+
+        @Override
+        public void onStatusChanged(
+                String provider, int status, Bundle extras) {
+            switch(status) {
+                case LocationProvider.OUT_OF_SERVICE:
+                case LocationProvider.TEMPORARILY_UNAVAILABLE: {
+                    mValid = false;
+                    if (mListener != null && mRecordLocation &&
+                            android.location.LocationManager.GPS_PROVIDER.equals(provider)) {
+                        mListener.showGpsOnScreenIndicator(false);
+                    }
+                    break;
+                }
+            }
+        }
+
+        public Location current() {
+            return mValid ? mLastLocation : null;
+        }
+    }
+}
diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java
new file mode 100644
index 0000000..40675b8
--- /dev/null
+++ b/src/com/android/camera/MediaSaveService.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2013 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.Service;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.location.Location;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.IBinder;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.File;
+
+/*
+ * Service for saving images in the background thread.
+ */
+public class MediaSaveService extends Service {
+    // The memory limit for unsaved image is 20MB.
+    private static final int SAVE_TASK_MEMORY_LIMIT = 20 * 1024 * 1024;
+    private static final String TAG = "CAM_" + MediaSaveService.class.getSimpleName();
+
+    private final IBinder mBinder = new LocalBinder();
+    private Listener mListener;
+    // Memory used by the total queued save request, in bytes.
+    private long mMemoryUse;
+
+    interface Listener {
+        public void onQueueStatus(boolean full);
+    }
+
+    interface OnMediaSavedListener {
+        public void onMediaSaved(Uri uri);
+    }
+
+    class LocalBinder extends Binder {
+        public MediaSaveService getService() {
+            return MediaSaveService.this;
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flag, int startId) {
+        return START_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+    }
+
+    @Override
+    public void onCreate() {
+        mMemoryUse = 0;
+    }
+
+    public boolean isQueueFull() {
+        return (mMemoryUse >= SAVE_TASK_MEMORY_LIMIT);
+    }
+
+    public void addImage(final byte[] data, String title, long date, Location loc,
+            int width, int height, int orientation, ExifInterface exif,
+            OnMediaSavedListener l, ContentResolver resolver) {
+        if (isQueueFull()) {
+            Log.e(TAG, "Cannot add image when the queue is full");
+            return;
+        }
+        ImageSaveTask t = new ImageSaveTask(data, title, date,
+                (loc == null) ? null : new Location(loc),
+                width, height, orientation, exif, resolver, l);
+
+        mMemoryUse += data.length;
+        if (isQueueFull()) {
+            onQueueFull();
+        }
+        t.execute();
+    }
+
+    public void addImage(final byte[] data, String title, Location loc,
+            int width, int height, int orientation, ExifInterface exif,
+            OnMediaSavedListener l, ContentResolver resolver) {
+        addImage(data, title, System.currentTimeMillis(), loc, width, height,
+                orientation, exif, l, resolver);
+    }
+
+    public void addVideo(String path, long duration, ContentValues values,
+            OnMediaSavedListener l, ContentResolver resolver) {
+        // We don't set a queue limit for video saving because the file
+        // is already in the storage. Only updating the database.
+        new VideoSaveTask(path, duration, values, l, resolver).execute();
+    }
+
+    public void setListener(Listener l) {
+        mListener = l;
+        if (l == null) return;
+        l.onQueueStatus(isQueueFull());
+    }
+
+    private void onQueueFull() {
+        if (mListener != null) mListener.onQueueStatus(true);
+    }
+
+    private void onQueueAvailable() {
+        if (mListener != null) mListener.onQueueStatus(false);
+    }
+
+    private class ImageSaveTask extends AsyncTask <Void, Void, Uri> {
+        private byte[] data;
+        private String title;
+        private long date;
+        private Location loc;
+        private int width, height;
+        private int orientation;
+        private ExifInterface exif;
+        private ContentResolver resolver;
+        private OnMediaSavedListener listener;
+
+        public ImageSaveTask(byte[] data, String title, long date, Location loc,
+                             int width, int height, int orientation, ExifInterface exif,
+                             ContentResolver resolver, OnMediaSavedListener listener) {
+            this.data = data;
+            this.title = title;
+            this.date = date;
+            this.loc = loc;
+            this.width = width;
+            this.height = height;
+            this.orientation = orientation;
+            this.exif = exif;
+            this.resolver = resolver;
+            this.listener = listener;
+        }
+
+        @Override
+        protected void onPreExecute() {
+            // do nothing.
+        }
+
+        @Override
+        protected Uri doInBackground(Void... v) {
+            return Storage.addImage(
+                    resolver, title, date, loc, orientation, exif, data, width, height);
+        }
+
+        @Override
+        protected void onPostExecute(Uri uri) {
+            if (listener != null) listener.onMediaSaved(uri);
+            boolean previouslyFull = isQueueFull();
+            mMemoryUse -= data.length;
+            if (isQueueFull() != previouslyFull) onQueueAvailable();
+        }
+    }
+
+    private class VideoSaveTask extends AsyncTask <Void, Void, Uri> {
+        private String path;
+        private long duration;
+        private ContentValues values;
+        private OnMediaSavedListener listener;
+        private ContentResolver resolver;
+
+        public VideoSaveTask(String path, long duration, ContentValues values,
+                OnMediaSavedListener l, ContentResolver r) {
+            this.path = path;
+            this.duration = duration;
+            this.values = new ContentValues(values);
+            this.listener = l;
+            this.resolver = r;
+        }
+
+        @Override
+        protected void onPreExecute() {
+            // do nothing.
+        }
+
+        @Override
+        protected Uri doInBackground(Void... v) {
+            values.put(Video.Media.SIZE, new File(path).length());
+            values.put(Video.Media.DURATION, duration);
+            Uri uri = null;
+            try {
+                Uri videoTable = Uri.parse("content://media/external/video/media");
+                uri = resolver.insert(videoTable, values);
+
+                // Rename the video file to the final name. This avoids other
+                // apps reading incomplete data.  We need to do it after we are
+                // certain that the previous insert to MediaProvider is completed.
+                String finalName = values.getAsString(
+                        Video.Media.DATA);
+                if (new File(path).renameTo(new File(finalName))) {
+                    path = finalName;
+                }
+
+                resolver.update(uri, values, null, null);
+            } catch (Exception e) {
+                // We failed to insert into the database. This can happen if
+                // the SD card is unmounted.
+                Log.e(TAG, "failed to add video to media store", e);
+                uri = null;
+            } finally {
+                Log.v(TAG, "Current video URI: " + uri);
+            }
+            return uri;
+        }
+
+        @Override
+        protected void onPostExecute(Uri uri) {
+            if (listener != null) listener.onMediaSaved(uri);
+        }
+    }
+}
diff --git a/src/com/android/camera/OnClickAttr.java b/src/com/android/camera/OnClickAttr.java
new file mode 100644
index 0000000..07a1063
--- /dev/null
+++ b/src/com/android/camera/OnClickAttr.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 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.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * Interface for OnClickAttr annotation.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnClickAttr {
+}
diff --git a/src/com/android/camera/OnScreenHint.java b/src/com/android/camera/OnScreenHint.java
new file mode 100644
index 0000000..4d7fa70
--- /dev/null
+++ b/src/com/android/camera/OnScreenHint.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+/**
+ * A on-screen hint is a view containing a little message for the user and will
+ * be shown on the screen continuously.  This class helps you create and show
+ * those.
+ *
+ * <p>
+ * When the view is shown to the user, appears as a floating view over the
+ * application.
+ * <p>
+ * The easiest way to use this class is to call one of the static methods that
+ * constructs everything you need and returns a new {@code OnScreenHint} object.
+ */
+public class OnScreenHint {
+    static final String TAG = "OnScreenHint";
+
+    int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+    int mX, mY;
+    float mHorizontalMargin;
+    float mVerticalMargin;
+    View mView;
+    View mNextView;
+
+    private final WindowManager.LayoutParams mParams =
+            new WindowManager.LayoutParams();
+    private final WindowManager mWM;
+    private final Handler mHandler = new Handler();
+
+    /**
+     * Construct an empty OnScreenHint object.
+     *
+     * @param context  The context to use.  Usually your
+     *                 {@link android.app.Application} or
+     *                 {@link android.app.Activity} object.
+     */
+    private OnScreenHint(Context context) {
+        mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        mY = context.getResources().getDimensionPixelSize(
+                R.dimen.hint_y_offset);
+
+        mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+        mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+        mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+        mParams.format = PixelFormat.TRANSLUCENT;
+        mParams.windowAnimations = R.style.Animation_OnScreenHint;
+        mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+        mParams.setTitle("OnScreenHint");
+    }
+
+    /**
+     * Show the view on the screen.
+     */
+    public void show() {
+        if (mNextView == null) {
+            throw new RuntimeException("View is not initialized");
+        }
+        mHandler.post(mShow);
+    }
+
+    /**
+     * Close the view if it's showing.
+     */
+    public void cancel() {
+        mHandler.post(mHide);
+    }
+
+    /**
+     * Make a standard hint that just contains a text view.
+     *
+     * @param context  The context to use.  Usually your
+     *                 {@link android.app.Application} or
+     *                 {@link android.app.Activity} object.
+     * @param text     The text to show.  Can be formatted text.
+     *
+     */
+    public static OnScreenHint makeText(Context context, CharSequence text) {
+        OnScreenHint result = new OnScreenHint(context);
+
+        LayoutInflater inflate =
+                (LayoutInflater) context.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        View v = inflate.inflate(R.layout.on_screen_hint, null);
+        TextView tv = (TextView) v.findViewById(R.id.message);
+        tv.setText(text);
+
+        result.mNextView = v;
+
+        return result;
+    }
+
+    /**
+     * Update the text in a OnScreenHint that was previously created using one
+     * of the makeText() methods.
+     * @param s The new text for the OnScreenHint.
+     */
+    public void setText(CharSequence s) {
+        if (mNextView == null) {
+            throw new RuntimeException("This OnScreenHint was not "
+                    + "created with OnScreenHint.makeText()");
+        }
+        TextView tv = (TextView) mNextView.findViewById(R.id.message);
+        if (tv == null) {
+            throw new RuntimeException("This OnScreenHint was not "
+                    + "created with OnScreenHint.makeText()");
+        }
+        tv.setText(s);
+    }
+
+    private synchronized void handleShow() {
+        if (mView != mNextView) {
+            // remove the old view if necessary
+            handleHide();
+            mView = mNextView;
+            final int gravity = mGravity;
+            mParams.gravity = gravity;
+            if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK)
+                    == Gravity.FILL_HORIZONTAL) {
+                mParams.horizontalWeight = 1.0f;
+            }
+            if ((gravity & Gravity.VERTICAL_GRAVITY_MASK)
+                    == Gravity.FILL_VERTICAL) {
+                mParams.verticalWeight = 1.0f;
+            }
+            mParams.x = mX;
+            mParams.y = mY;
+            mParams.verticalMargin = mVerticalMargin;
+            mParams.horizontalMargin = mHorizontalMargin;
+            if (mView.getParent() != null) {
+                mWM.removeView(mView);
+            }
+            mWM.addView(mView, mParams);
+        }
+    }
+
+    private synchronized void handleHide() {
+        if (mView != null) {
+            // note: checking parent() just to make sure the view has
+            // been added...  i have seen cases where we get here when
+            // the view isn't yet added, so let's try not to crash.
+            if (mView.getParent() != null) {
+                mWM.removeView(mView);
+            }
+            mView = null;
+        }
+    }
+
+    private final Runnable mShow = new Runnable() {
+        @Override
+        public void run() {
+            handleShow();
+        }
+    };
+
+    private final Runnable mHide = new Runnable() {
+        @Override
+        public void run() {
+            handleHide();
+        }
+    };
+}
+
diff --git a/src/com/android/camera/OnScreenIndicators.java b/src/com/android/camera/OnScreenIndicators.java
new file mode 100644
index 0000000..77c8faf
--- /dev/null
+++ b/src/com/android/camera/OnScreenIndicators.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.res.TypedArray;
+import android.hardware.Camera;
+import android.hardware.Camera.Parameters;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+
+/**
+ * The on-screen indicators of the pie menu button. They show the camera
+ * settings in the viewfinder.
+ */
+public class OnScreenIndicators {
+    private final int[] mWBArray;
+    private final View mOnScreenIndicators;
+    private final ImageView mExposureIndicator;
+    private final ImageView mFlashIndicator;
+    private final ImageView mSceneIndicator;
+    private final ImageView mLocationIndicator;
+    private final ImageView mTimerIndicator;
+    private final ImageView mWBIndicator;
+
+    public OnScreenIndicators(Context ctx, View onScreenIndicatorsView) {
+        TypedArray iconIds = ctx.getResources().obtainTypedArray(
+                R.array.camera_wb_indicators);
+        final int n = iconIds.length();
+        mWBArray = new int[n];
+        for (int i = 0; i < n; i++) {
+            mWBArray[i] = iconIds.getResourceId(i, R.drawable.ic_indicator_wb_off);
+        }
+        mOnScreenIndicators = onScreenIndicatorsView;
+        mExposureIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+                R.id.menu_exposure_indicator);
+        mFlashIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+                R.id.menu_flash_indicator);
+        mSceneIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+                R.id.menu_scenemode_indicator);
+        mLocationIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+                R.id.menu_location_indicator);
+        mTimerIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+                R.id.menu_timer_indicator);
+        mWBIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+                R.id.menu_wb_indicator);
+    }
+
+    /**
+     * Resets all indicators to show the default values.
+     */
+    public void resetToDefault() {
+        updateExposureOnScreenIndicator(0);
+        updateFlashOnScreenIndicator(Parameters.FLASH_MODE_OFF);
+        updateSceneOnScreenIndicator(Parameters.SCENE_MODE_AUTO);
+        updateWBIndicator(2);
+        updateTimerIndicator(false);
+        updateLocationIndicator(false);
+    }
+
+    /**
+     * Sets the exposure indicator using exposure compensations step rounding.
+     */
+    public void updateExposureOnScreenIndicator(Camera.Parameters params, int value) {
+        if (mExposureIndicator == null) {
+            return;
+        }
+        float step = params.getExposureCompensationStep();
+        value = Math.round(value * step);
+        updateExposureOnScreenIndicator(value);
+    }
+
+    /**
+     * Set the exposure indicator to the given value.
+     *
+     * @param value Value between -3 and 3. If outside this range, 0 is used by
+     *            default.
+     */
+    public void updateExposureOnScreenIndicator(int value) {
+        int id = 0;
+        switch(value) {
+        case -3:
+            id = R.drawable.ic_indicator_ev_n3;
+            break;
+        case -2:
+            id = R.drawable.ic_indicator_ev_n2;
+            break;
+        case -1:
+            id = R.drawable.ic_indicator_ev_n1;
+            break;
+        case 0:
+            id = R.drawable.ic_indicator_ev_0;
+            break;
+        case 1:
+            id = R.drawable.ic_indicator_ev_p1;
+            break;
+        case 2:
+            id = R.drawable.ic_indicator_ev_p2;
+            break;
+        case 3:
+            id = R.drawable.ic_indicator_ev_p3;
+            break;
+        }
+        mExposureIndicator.setImageResource(id);
+    }
+
+    public void updateWBIndicator(int wbIndex) {
+        if (mWBIndicator == null) return;
+        mWBIndicator.setImageResource(mWBArray[wbIndex]);
+    }
+
+    public void updateTimerIndicator(boolean on) {
+        if (mTimerIndicator == null) return;
+        mTimerIndicator.setImageResource(on ? R.drawable.ic_indicator_timer_on
+                : R.drawable.ic_indicator_timer_off);
+    }
+
+    public void updateLocationIndicator(boolean on) {
+        if (mLocationIndicator == null) return;
+        mLocationIndicator.setImageResource(on ? R.drawable.ic_indicator_loc_on
+                : R.drawable.ic_indicator_loc_off);
+    }
+
+    /**
+     * Set the flash indicator to the given value.
+     *
+     * @param value One of Parameters.FLASH_MODE_OFF,
+     *            Parameters.FLASH_MODE_AUTO, Parameters.FLASH_MODE_ON.
+     */
+    public void updateFlashOnScreenIndicator(String value) {
+        if (mFlashIndicator == null) {
+            return;
+        }
+        if (value == null || Parameters.FLASH_MODE_OFF.equals(value)) {
+            mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off);
+        } else {
+            if (Parameters.FLASH_MODE_AUTO.equals(value)) {
+                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_auto);
+            } else if (Parameters.FLASH_MODE_ON.equals(value)
+                    || Parameters.FLASH_MODE_TORCH.equals(value)) {
+                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_on);
+            } else {
+                mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off);
+            }
+        }
+    }
+
+    /**
+     * Set the scene indicator depending on the given scene mode.
+     *
+     * @param value the current Parameters.SCENE_MODE_* value.
+     */
+    public void updateSceneOnScreenIndicator(String value) {
+        if (mSceneIndicator == null) {
+            return;
+        }
+        if ((value == null) || Parameters.SCENE_MODE_AUTO.equals(value)) {
+            mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_off);
+        } else if (Parameters.SCENE_MODE_HDR.equals(value)) {
+            mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_hdr);
+        } else {
+            mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_on);
+        }
+    }
+
+    /**
+     * Sets the visibility of all indicators.
+     *
+     * @param visibility View.VISIBLE, View.GONE etc.
+     */
+    public void setVisibility(int visibility) {
+        mOnScreenIndicators.setVisibility(visibility);
+    }
+}
diff --git a/src/com/android/camera/PhotoController.java b/src/com/android/camera/PhotoController.java
new file mode 100644
index 0000000..bc824d9
--- /dev/null
+++ b/src/com/android/camera/PhotoController.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 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.view.SurfaceHolder;
+import android.view.View;
+
+import com.android.camera.ShutterButton.OnShutterButtonListener;
+
+
+public interface PhotoController extends OnShutterButtonListener {
+
+    public static final int PREVIEW_STOPPED = 0;
+    public static final int IDLE = 1;  // preview is active
+    // Focus is in progress. The exact focus state is in Focus.java.
+    public static final int FOCUSING = 2;
+    public static final int SNAPSHOT_IN_PROGRESS = 3;
+    // Switching between cameras.
+    public static final int SWITCHING_CAMERA = 4;
+
+    // returns the actual set zoom value
+    public int onZoomChanged(int requestedZoom);
+
+    public boolean isImageCaptureIntent();
+
+    public boolean isCameraIdle();
+
+    public void onCaptureDone();
+
+    public void onCaptureCancelled();
+
+    public void onCaptureRetake();
+
+    public void cancelAutoFocus();
+
+    public void stopPreview();
+
+    public int getCameraState();
+
+    public void onSingleTapUp(View view, int x, int y);
+
+    public void onSurfaceCreated(SurfaceHolder holder);
+
+    public void onCountDownFinished();
+
+    public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight);
+
+    public void updateCameraOrientation();
+
+    public void enableRecordingLocation(boolean enable);
+}
diff --git a/src/com/android/camera/PhotoMenu.java b/src/com/android/camera/PhotoMenu.java
new file mode 100644
index 0000000..6c1e2d0
--- /dev/null
+++ b/src/com/android/camera/PhotoMenu.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012 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.res.Resources;
+import android.hardware.Camera.Parameters;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CountdownTimerPopup;
+import com.android.camera.ui.ListPrefSettingPopup;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.gallery3d.R;
+
+import java.util.Locale;
+
+public class PhotoMenu extends PieController
+        implements CountdownTimerPopup.Listener,
+        ListPrefSettingPopup.Listener {
+    private static String TAG = "CAM_photomenu";
+
+    private final String mSettingOff;
+
+    private PhotoUI mUI;
+    private AbstractSettingPopup mPopup;
+    private CameraActivity mActivity;
+
+    public PhotoMenu(CameraActivity activity, PhotoUI ui, PieRenderer pie) {
+        super(activity, pie);
+        mUI = ui;
+        mSettingOff = activity.getString(R.string.setting_off_value);
+        mActivity = activity;
+    }
+
+    public void initialize(PreferenceGroup group) {
+        super.initialize(group);
+        mPopup = null;
+        PieItem item = null;
+        final Resources res = mActivity.getResources();
+        Locale locale = res.getConfiguration().locale;
+        // the order is from left to right in the menu
+
+        // hdr
+        if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) {
+            item = makeSwitchItem(CameraSettings.KEY_CAMERA_HDR, true);
+            mRenderer.addItem(item);
+        }
+        // exposure compensation
+        if (group.findPreference(CameraSettings.KEY_EXPOSURE) != null) {
+            item = makeItem(CameraSettings.KEY_EXPOSURE);
+            item.setLabel(res.getString(R.string.pref_exposure_label));
+            mRenderer.addItem(item);
+        }
+        // more settings
+        PieItem more = makeItem(R.drawable.ic_settings_holo_light);
+        more.setLabel(res.getString(R.string.camera_menu_more_label));
+        mRenderer.addItem(more);
+        // flash
+        if (group.findPreference(CameraSettings.KEY_FLASH_MODE) != null) {
+            item = makeItem(CameraSettings.KEY_FLASH_MODE);
+            item.setLabel(res.getString(R.string.pref_camera_flashmode_label));
+            mRenderer.addItem(item);
+        }
+        // camera switcher
+        if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+            item = makeSwitchItem(CameraSettings.KEY_CAMERA_ID, false);
+            final PieItem fitem = item;
+            item.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(PieItem item) {
+                    // Find the index of next camera.
+                    ListPreference pref = mPreferenceGroup
+                            .findPreference(CameraSettings.KEY_CAMERA_ID);
+                    if (pref != null) {
+                        int index = pref.findIndexOfValue(pref.getValue());
+                        CharSequence[] values = pref.getEntryValues();
+                        index = (index + 1) % values.length;
+                        pref.setValueIndex(index);
+                        mListener.onCameraPickerClicked(index);
+                    }
+                    updateItem(fitem, CameraSettings.KEY_CAMERA_ID);
+                }
+            });
+            mRenderer.addItem(item);
+        }
+        // location
+        if (group.findPreference(CameraSettings.KEY_RECORD_LOCATION) != null) {
+            item = makeSwitchItem(CameraSettings.KEY_RECORD_LOCATION, true);
+            more.addItem(item);
+            if (mActivity.isSecureCamera()) {
+                // Prevent location preference from getting changed in secure camera mode
+                item.setEnabled(false);
+            }
+        }
+        // countdown timer
+        final ListPreference ctpref = group.findPreference(CameraSettings.KEY_TIMER);
+        final ListPreference beeppref = group.findPreference(CameraSettings.KEY_TIMER_SOUND_EFFECTS);
+        item = makeItem(R.drawable.ic_timer);
+        item.setLabel(res.getString(R.string.pref_camera_timer_title).toUpperCase(locale));
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                CountdownTimerPopup timerPopup = (CountdownTimerPopup) mActivity.getLayoutInflater().inflate(
+                        R.layout.countdown_setting_popup, null, false);
+                timerPopup.initialize(ctpref, beeppref);
+                timerPopup.setSettingChangedListener(PhotoMenu.this);
+                mUI.dismissPopup();
+                mPopup = timerPopup;
+                mUI.showPopup(mPopup);
+            }
+        });
+        more.addItem(item);
+        // image size
+        item = makeItem(R.drawable.ic_imagesize);
+        final ListPreference sizePref = group.findPreference(CameraSettings.KEY_PICTURE_SIZE);
+        item.setLabel(res.getString(R.string.pref_camera_picturesize_title).toUpperCase(locale));
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                ListPrefSettingPopup popup = (ListPrefSettingPopup) mActivity.getLayoutInflater().inflate(
+                        R.layout.list_pref_setting_popup, null, false);
+                popup.initialize(sizePref);
+                popup.setSettingChangedListener(PhotoMenu.this);
+                mUI.dismissPopup();
+                mPopup = popup;
+                mUI.showPopup(mPopup);
+            }
+        });
+        more.addItem(item);
+        // white balance
+        if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+            item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+            item.setLabel(res.getString(R.string.pref_camera_whitebalance_label));
+            more.addItem(item);
+        }
+        // scene mode
+        if (group.findPreference(CameraSettings.KEY_SCENE_MODE) != null) {
+            IconListPreference pref = (IconListPreference) group.findPreference(
+                    CameraSettings.KEY_SCENE_MODE);
+            pref.setUseSingleIcon(true);
+            item = makeItem(CameraSettings.KEY_SCENE_MODE);
+            more.addItem(item);
+        }
+    }
+
+    @Override
+    // Hit when an item in a popup gets selected
+    public void onListPrefChanged(ListPreference pref) {
+        if (mPopup != null) {
+            mUI.dismissPopup();
+        }
+        onSettingChanged(pref);
+    }
+
+    public void popupDismissed() {
+        if (mPopup != null) {
+            mPopup = null;
+        }
+    }
+
+    // Return true if the preference has the specified key but not the value.
+    private static boolean notSame(ListPreference pref, String key, String value) {
+        return (key.equals(pref.getKey()) && !value.equals(pref.getValue()));
+    }
+
+    private void setPreference(String key, String value) {
+        ListPreference pref = mPreferenceGroup.findPreference(key);
+        if (pref != null && !value.equals(pref.getValue())) {
+            pref.setValue(value);
+            reloadPreferences();
+        }
+    }
+
+    @Override
+    public void onSettingChanged(ListPreference pref) {
+        // Reset the scene mode if HDR is set to on. Reset HDR if scene mode is
+        // set to non-auto.
+        if (notSame(pref, CameraSettings.KEY_CAMERA_HDR, mSettingOff)) {
+            setPreference(CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO);
+        } else if (notSame(pref, CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO)) {
+            setPreference(CameraSettings.KEY_CAMERA_HDR, mSettingOff);
+        }
+        super.onSettingChanged(pref);
+    }
+}
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
new file mode 100644
index 0000000..c65a49e
--- /dev/null
+++ b/src/com/android/camera/PhotoModule.java
@@ -0,0 +1,2006 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.Location;
+import android.media.CameraProfile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.OrientationEventListener;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.camera.CameraManager.CameraAFCallback;
+import com.android.camera.CameraManager.CameraAFMoveCallback;
+import com.android.camera.CameraManager.CameraPictureCallback;
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.CameraManager.CameraShutterCallback;
+import com.android.camera.ui.CountDownView.OnCountDownFinishedListener;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.RotateTextToast;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.List;
+
+public class PhotoModule
+    implements CameraModule,
+    PhotoController,
+    FocusOverlayManager.Listener,
+    CameraPreference.OnPreferenceChangedListener,
+    ShutterButton.OnShutterButtonListener,
+    MediaSaveService.Listener,
+    OnCountDownFinishedListener,
+    SensorEventListener {
+
+    private static final String TAG = "CAM_PhotoModule";
+
+    // We number the request code from 1000 to avoid collision with Gallery.
+    private static final int REQUEST_CROP = 1000;
+
+    private static final int SETUP_PREVIEW = 1;
+    private static final int FIRST_TIME_INIT = 2;
+    private static final int CLEAR_SCREEN_DELAY = 3;
+    private static final int SET_CAMERA_PARAMETERS_WHEN_IDLE = 4;
+    private static final int CHECK_DISPLAY_ROTATION = 5;
+    private static final int SHOW_TAP_TO_FOCUS_TOAST = 6;
+    private static final int SWITCH_CAMERA = 7;
+    private static final int SWITCH_CAMERA_START_ANIMATION = 8;
+    private static final int CAMERA_OPEN_DONE = 9;
+    private static final int START_PREVIEW_DONE = 10;
+    private static final int OPEN_CAMERA_FAIL = 11;
+    private static final int CAMERA_DISABLED = 12;
+    private static final int CAPTURE_ANIMATION_DONE = 13;
+
+    // The subset of parameters we need to update in setCameraParameters().
+    private static final int UPDATE_PARAM_INITIALIZE = 1;
+    private static final int UPDATE_PARAM_ZOOM = 2;
+    private static final int UPDATE_PARAM_PREFERENCE = 4;
+    private static final int UPDATE_PARAM_ALL = -1;
+
+    // This is the timeout to keep the camera in onPause for the first time
+    // after screen on if the activity is started from secure lock screen.
+    private static final int KEEP_CAMERA_TIMEOUT = 1000; // ms
+
+    // copied from Camera hierarchy
+    private CameraActivity mActivity;
+    private CameraProxy mCameraDevice;
+    private int mCameraId;
+    private Parameters mParameters;
+    private boolean mPaused;
+
+    private PhotoUI mUI;
+
+    // The activity is going to switch to the specified camera id. This is
+    // needed because texture copy is done in GL thread. -1 means camera is not
+    // switching.
+    protected int mPendingSwitchCameraId = -1;
+    private boolean mOpenCameraFail;
+    private boolean mCameraDisabled;
+
+    // When setCameraParametersWhenIdle() is called, we accumulate the subsets
+    // needed to be updated in mUpdateSet.
+    private int mUpdateSet;
+
+    private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+    private int mZoomValue;  // The current zoom value.
+
+    private Parameters mInitialParams;
+    private boolean mFocusAreaSupported;
+    private boolean mMeteringAreaSupported;
+    private boolean mAeLockSupported;
+    private boolean mAwbLockSupported;
+    private boolean mContinousFocusSupported;
+
+    // The degrees of the device rotated clockwise from its natural orientation.
+    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+    private ComboPreferences mPreferences;
+
+    private static final String sTempCropFilename = "crop-temp";
+
+    private ContentProviderClient mMediaProviderClient;
+    private boolean mFaceDetectionStarted = false;
+
+    // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true.
+    private String mCropValue;
+    private Uri mSaveUri;
+
+    // We use a queue to generated names of the images to be used later
+    // when the image is ready to be saved.
+    private NamedImages mNamedImages;
+
+    private Runnable mDoSnapRunnable = new Runnable() {
+        @Override
+        public void run() {
+            onShutterButtonClick();
+        }
+    };
+
+    private Runnable mFlashRunnable = new Runnable() {
+        @Override
+        public void run() {
+            animateFlash();
+        }
+    };
+
+    private final StringBuilder mBuilder = new StringBuilder();
+    private final Formatter mFormatter = new Formatter(mBuilder);
+    private final Object[] mFormatterArgs = new Object[1];
+
+    /**
+     * An unpublished intent flag requesting to return as soon as capturing
+     * is completed.
+     *
+     * TODO: consider publishing by moving into MediaStore.
+     */
+    private static final String EXTRA_QUICK_CAPTURE =
+            "android.intent.extra.quickCapture";
+
+    // The display rotation in degrees. This is only valid when mCameraState is
+    // not PREVIEW_STOPPED.
+    private int mDisplayRotation;
+    // The value for android.hardware.Camera.setDisplayOrientation.
+    private int mCameraDisplayOrientation;
+    // The value for UI components like indicators.
+    private int mDisplayOrientation;
+    // The value for android.hardware.Camera.Parameters.setRotation.
+    private int mJpegRotation;
+    private boolean mFirstTimeInitialized;
+    private boolean mIsImageCaptureIntent;
+
+    private int mCameraState = PREVIEW_STOPPED;
+    private boolean mSnapshotOnIdle = false;
+
+    private ContentResolver mContentResolver;
+
+    private LocationManager mLocationManager;
+
+    private final PostViewPictureCallback mPostViewPictureCallback =
+            new PostViewPictureCallback();
+    private final RawPictureCallback mRawPictureCallback =
+            new RawPictureCallback();
+    private final AutoFocusCallback mAutoFocusCallback =
+            new AutoFocusCallback();
+    private final Object mAutoFocusMoveCallback =
+            ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK
+            ? new AutoFocusMoveCallback()
+            : null;
+
+    private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
+
+    private long mFocusStartTime;
+    private long mShutterCallbackTime;
+    private long mPostViewPictureCallbackTime;
+    private long mRawPictureCallbackTime;
+    private long mJpegPictureCallbackTime;
+    private long mOnResumeTime;
+    private byte[] mJpegImageData;
+
+    // These latency time are for the CameraLatency test.
+    public long mAutoFocusTime;
+    public long mShutterLag;
+    public long mShutterToPictureDisplayedTime;
+    public long mPictureDisplayedToJpegCallbackTime;
+    public long mJpegCallbackFinishTime;
+    public long mCaptureStartTime;
+
+    // This handles everything about focus.
+    private FocusOverlayManager mFocusManager;
+
+    private String mSceneMode;
+
+    private final Handler mHandler = new MainHandler();
+    private PreferenceGroup mPreferenceGroup;
+
+    private boolean mQuickCapture;
+    private SensorManager mSensorManager;
+    private float[] mGData = new float[3];
+    private float[] mMData = new float[3];
+    private float[] mR = new float[16];
+    private int mHeading = -1;
+
+    CameraStartUpThread mCameraStartUpThread;
+    ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable();
+
+    private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener =
+            new MediaSaveService.OnMediaSavedListener() {
+                @Override
+                public void onMediaSaved(Uri uri) {
+                    if (uri != null) {
+                        mActivity.notifyNewMedia(uri);
+                    }
+                }
+            };
+
+    // The purpose is not to block the main thread in onCreate and onResume.
+    private class CameraStartUpThread extends Thread {
+        private volatile boolean mCancelled;
+
+        public void cancel() {
+            mCancelled = true;
+            interrupt();
+        }
+
+        public boolean isCanceled() {
+            return mCancelled;
+        }
+
+        @Override
+        public void run() {
+            try {
+                // We need to check whether the activity is paused before long
+                // operations to ensure that onPause() can be done ASAP.
+                if (mCancelled) return;
+                mCameraDevice = Util.openCamera(mActivity, mCameraId);
+                mParameters = mCameraDevice.getParameters();
+                // Wait until all the initialization needed by startPreview are
+                // done.
+                mStartPreviewPrerequisiteReady.block();
+
+                initializeCapabilities();
+                if (mFocusManager == null) initializeFocusManager();
+                if (mCancelled) return;
+                setCameraParameters(UPDATE_PARAM_ALL);
+                mHandler.sendEmptyMessage(CAMERA_OPEN_DONE);
+                if (mCancelled) return;
+                startPreview();
+                mHandler.sendEmptyMessage(START_PREVIEW_DONE);
+                mOnResumeTime = SystemClock.uptimeMillis();
+                mHandler.sendEmptyMessage(CHECK_DISPLAY_ROTATION);
+            } catch (CameraHardwareException e) {
+                mHandler.sendEmptyMessage(OPEN_CAMERA_FAIL);
+            } catch (CameraDisabledException e) {
+                mHandler.sendEmptyMessage(CAMERA_DISABLED);
+            }
+        }
+    }
+
+    /**
+     * 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 SETUP_PREVIEW: {
+                    setupPreview();
+                    break;
+                }
+
+                case CLEAR_SCREEN_DELAY: {
+                    mActivity.getWindow().clearFlags(
+                            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                    break;
+                }
+
+                case FIRST_TIME_INIT: {
+                    initializeFirstTime();
+                    break;
+                }
+
+                case SET_CAMERA_PARAMETERS_WHEN_IDLE: {
+                    setCameraParametersWhenIdle(0);
+                    break;
+                }
+
+                case CHECK_DISPLAY_ROTATION: {
+                    // Set the display orientation if display rotation has changed.
+                    // Sometimes this happens when the device is held upside
+                    // down and camera app is opened. Rotation animation will
+                    // take some time and the rotation value we have got may be
+                    // wrong. Framework does not have a callback for this now.
+                    if (Util.getDisplayRotation(mActivity) != mDisplayRotation) {
+                        setDisplayOrientation();
+                    }
+                    if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) {
+                        mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+                    }
+                    break;
+                }
+
+                case SHOW_TAP_TO_FOCUS_TOAST: {
+                    showTapToFocusToast();
+                    break;
+                }
+
+                case SWITCH_CAMERA: {
+                    switchCamera();
+                    break;
+                }
+
+                case SWITCH_CAMERA_START_ANIMATION: {
+                   // TODO: Need to revisit
+                   // ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+                    break;
+                }
+
+                case CAMERA_OPEN_DONE: {
+                    onCameraOpened();
+                    break;
+                }
+
+                case START_PREVIEW_DONE: {
+                    onPreviewStarted();
+                    break;
+                }
+
+                case OPEN_CAMERA_FAIL: {
+                    mCameraStartUpThread = null;
+                    mOpenCameraFail = true;
+                    Util.showErrorAndFinish(mActivity,
+                            R.string.cannot_connect_camera);
+                    break;
+                }
+
+                case CAMERA_DISABLED: {
+                    mCameraStartUpThread = null;
+                    mCameraDisabled = true;
+                    Util.showErrorAndFinish(mActivity,
+                            R.string.camera_disabled);
+                    break;
+                }
+                case CAPTURE_ANIMATION_DONE: {
+                    mUI.enablePreviewThumb(false);
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void init(CameraActivity activity, View parent) {
+        mActivity = activity;
+        mUI = new PhotoUI(activity, this, parent);
+        mPreferences = new ComboPreferences(mActivity);
+        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+        mCameraId = getPreferredCameraId(mPreferences);
+
+        mContentResolver = mActivity.getContentResolver();
+
+        // To reduce startup time, open the camera and start the preview in
+        // another thread.
+        mCameraStartUpThread = new CameraStartUpThread();
+        mCameraStartUpThread.start();
+
+        // Surface texture is from camera screen nail and startPreview needs it.
+        // This must be done before startPreview.
+        mIsImageCaptureIntent = isImageCaptureIntent();
+
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+        // we need to reset exposure for the preview
+        resetExposureCompensation();
+        // Starting the preview needs preferences, camera screen nail, and
+        // focus area indicator.
+        mStartPreviewPrerequisiteReady.open();
+
+        initializeControlByIntent();
+        mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
+        mLocationManager = new LocationManager(mActivity, mUI);
+        mSensorManager = (SensorManager)(mActivity.getSystemService(Context.SENSOR_SERVICE));
+    }
+
+    private void initializeControlByIntent() {
+        mUI.initializeControlByIntent();
+        if (mIsImageCaptureIntent) {
+            setupCaptureParams();
+        }
+    }
+
+    private void onPreviewStarted() {
+        mCameraStartUpThread = null;
+        setCameraState(IDLE);
+        startFaceDetection();
+        locationFirstRun();
+    }
+
+    // Prompt the user to pick to record location for the very first run of
+    // camera only
+    private void locationFirstRun() {
+        if (RecordLocationPreference.isSet(mPreferences)) {
+            return;
+        }
+        if (mActivity.isSecureCamera()) return;
+        // Check if the back camera exists
+        int backCameraId = CameraHolder.instance().getBackCameraId();
+        if (backCameraId == -1) {
+            // If there is no back camera, do not show the prompt.
+            return;
+        }
+        mUI.showLocationDialog();
+    }
+
+    public void enableRecordingLocation(boolean enable) {
+        setLocationPreference(enable ? RecordLocationPreference.VALUE_ON
+                : RecordLocationPreference.VALUE_OFF);
+    }
+
+    private void setLocationPreference(String value) {
+        mPreferences.edit()
+            .putString(CameraSettings.KEY_RECORD_LOCATION, value)
+            .apply();
+        // TODO: Fix this to use the actual onSharedPreferencesChanged listener
+        // instead of invoking manually
+        onSharedPreferenceChanged();
+    }
+
+    private void onCameraOpened() {
+        View root = mUI.getRootView();
+        // These depend on camera parameters.
+
+        int width = root.getWidth();
+        int height = root.getHeight();
+        mFocusManager.setPreviewSize(width, height);
+        openCameraCommon();
+    }
+
+    private void switchCamera() {
+        if (mPaused) return;
+
+        Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId);
+        mCameraId = mPendingSwitchCameraId;
+        mPendingSwitchCameraId = -1;
+        setCameraId(mCameraId);
+
+        // from onPause
+        closeCamera();
+        mUI.collapseCameraControls();
+        mUI.clearFaces();
+        if (mFocusManager != null) mFocusManager.removeMessages();
+
+        // Restart the camera and initialize the UI. From onCreate.
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+        try {
+            mCameraDevice = Util.openCamera(mActivity, mCameraId);
+            mParameters = mCameraDevice.getParameters();
+        } catch (CameraHardwareException e) {
+            Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+            return;
+        } catch (CameraDisabledException e) {
+            Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+            return;
+        }
+        initializeCapabilities();
+        CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+        boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT);
+        mFocusManager.setMirror(mirror);
+        mFocusManager.setParameters(mInitialParams);
+        setupPreview();
+
+        // reset zoom value index
+        mZoomValue = 0;
+        openCameraCommon();
+
+        if (ApiHelper.HAS_SURFACE_TEXTURE) {
+            // Start switch camera animation. Post a message because
+            // onFrameAvailable from the old camera may already exist.
+            mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION);
+        }
+    }
+
+    protected void setCameraId(int cameraId) {
+        ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+        pref.setValue("" + cameraId);
+    }
+
+    // either open a new camera or switch cameras
+    private void openCameraCommon() {
+        loadCameraPreferences();
+
+        mUI.onCameraOpened(mPreferenceGroup, mPreferences, mParameters, this);
+        updateSceneMode();
+        showTapToFocusToastIfNeeded();
+
+
+    }
+
+    public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+        if (mFocusManager != null) mFocusManager.setPreviewSize(width, height);
+    }
+
+    private void resetExposureCompensation() {
+        String value = mPreferences.getString(CameraSettings.KEY_EXPOSURE,
+                CameraSettings.EXPOSURE_DEFAULT_VALUE);
+        if (!CameraSettings.EXPOSURE_DEFAULT_VALUE.equals(value)) {
+            Editor editor = mPreferences.edit();
+            editor.putString(CameraSettings.KEY_EXPOSURE, "0");
+            editor.apply();
+        }
+    }
+
+    private void keepMediaProviderInstance() {
+        // We want to keep a reference to MediaProvider in camera's lifecycle.
+        // TODO: Utilize mMediaProviderClient instance to replace
+        // ContentResolver calls.
+        if (mMediaProviderClient == null) {
+            mMediaProviderClient = mContentResolver
+                    .acquireContentProviderClient(MediaStore.AUTHORITY);
+        }
+    }
+
+    // Snapshots can only be taken after this is called. It should be called
+    // once only. We could have done these things in onCreate() but we want to
+    // make preview screen appear as soon as possible.
+    private void initializeFirstTime() {
+        if (mFirstTimeInitialized) return;
+
+        // Initialize location service.
+        boolean recordLocation = RecordLocationPreference.get(
+                mPreferences, mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+
+        keepMediaProviderInstance();
+
+        mUI.initializeFirstTime();
+        MediaSaveService s = mActivity.getMediaSaveService();
+        // We set the listener only when both service and shutterbutton
+        // are initialized.
+        if (s != null) {
+            s.setListener(this);
+        }
+
+        mNamedImages = new NamedImages();
+
+        mFirstTimeInitialized = true;
+        addIdleHandler();
+
+        mActivity.updateStorageSpaceAndHint();
+    }
+
+    // If the activity is paused and resumed, this method will be called in
+    // onResume.
+    private void initializeSecondTime() {
+        // Start location update if needed.
+        boolean recordLocation = RecordLocationPreference.get(
+                mPreferences, mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+        MediaSaveService s = mActivity.getMediaSaveService();
+        if (s != null) {
+            s.setListener(this);
+        }
+        mNamedImages = new NamedImages();
+        mUI.initializeSecondTime(mParameters);
+        keepMediaProviderInstance();
+    }
+
+    @Override
+    public void onSurfaceCreated(SurfaceHolder holder) {
+        // Do not access the camera if camera start up thread is not finished.
+        if (mCameraDevice == null || mCameraStartUpThread != null)
+            return;
+
+        mCameraDevice.setPreviewDisplay(holder);
+        // This happens when onConfigurationChanged arrives, surface has been
+        // destroyed, and there is no onFullScreenChanged.
+        if (mCameraState == PREVIEW_STOPPED) {
+            setupPreview();
+        }
+    }
+
+    private void showTapToFocusToastIfNeeded() {
+        // Show the tap to focus toast if this is the first start.
+        if (mFocusAreaSupported &&
+                mPreferences.getBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, true)) {
+            // Delay the toast for one second to wait for orientation.
+            mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_FOCUS_TOAST, 1000);
+        }
+    }
+
+    private void addIdleHandler() {
+        MessageQueue queue = Looper.myQueue();
+        queue.addIdleHandler(new MessageQueue.IdleHandler() {
+            @Override
+            public boolean queueIdle() {
+                Storage.ensureOSXCompatible();
+                return false;
+            }
+        });
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    @Override
+    public void startFaceDetection() {
+        if (!ApiHelper.HAS_FACE_DETECTION) return;
+        if (mFaceDetectionStarted) return;
+        if (mParameters.getMaxNumDetectedFaces() > 0) {
+            mFaceDetectionStarted = true;
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            mUI.onStartFaceDetection(mDisplayOrientation,
+                    (info.facing == CameraInfo.CAMERA_FACING_FRONT));
+            mCameraDevice.setFaceDetectionCallback(mHandler, mUI);
+            mCameraDevice.startFaceDetection();
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    @Override
+    public void stopFaceDetection() {
+        if (!ApiHelper.HAS_FACE_DETECTION) return;
+        if (!mFaceDetectionStarted) return;
+        if (mParameters.getMaxNumDetectedFaces() > 0) {
+            mFaceDetectionStarted = false;
+            mCameraDevice.setFaceDetectionCallback(null, null);
+            mCameraDevice.stopFaceDetection();
+            mUI.clearFaces();
+        }
+    }
+
+    private final class ShutterCallback
+            implements CameraShutterCallback {
+
+        private boolean mAnimateFlash;
+
+        public ShutterCallback(boolean animateFlash) {
+            mAnimateFlash = animateFlash;
+        }
+
+        @Override
+        public void onShutter(CameraProxy camera) {
+            mShutterCallbackTime = System.currentTimeMillis();
+            mShutterLag = mShutterCallbackTime - mCaptureStartTime;
+            Log.v(TAG, "mShutterLag = " + mShutterLag + "ms");
+            if (mAnimateFlash) {
+                mActivity.runOnUiThread(mFlashRunnable);
+            }
+        }
+    }
+
+    private final class PostViewPictureCallback
+            implements CameraPictureCallback {
+        @Override
+        public void onPictureTaken(byte [] data, CameraProxy camera) {
+            mPostViewPictureCallbackTime = System.currentTimeMillis();
+            Log.v(TAG, "mShutterToPostViewCallbackTime = "
+                    + (mPostViewPictureCallbackTime - mShutterCallbackTime)
+                    + "ms");
+        }
+    }
+
+    private final class RawPictureCallback
+            implements CameraPictureCallback {
+        @Override
+        public void onPictureTaken(byte [] rawData, CameraProxy camera) {
+            mRawPictureCallbackTime = System.currentTimeMillis();
+            Log.v(TAG, "mShutterToRawCallbackTime = "
+                    + (mRawPictureCallbackTime - mShutterCallbackTime) + "ms");
+        }
+    }
+
+    private final class JpegPictureCallback
+            implements CameraPictureCallback {
+        Location mLocation;
+
+        public JpegPictureCallback(Location loc) {
+            mLocation = loc;
+        }
+
+        @Override
+        public void onPictureTaken(final byte [] jpegData, CameraProxy camera) {
+            if (mPaused) {
+                return;
+            }
+            //TODO: We should show the picture taken rather than frozen preview here
+            if (mIsImageCaptureIntent) {
+                stopPreview();
+            }
+            if (mSceneMode == Util.SCENE_MODE_HDR) {
+                mUI.showSwitcher();
+                mUI.setSwipingEnabled(true);
+            }
+
+            mJpegPictureCallbackTime = System.currentTimeMillis();
+            // If postview callback has arrived, the captured image is displayed
+            // in postview callback. If not, the captured image is displayed in
+            // raw picture callback.
+            if (mPostViewPictureCallbackTime != 0) {
+                mShutterToPictureDisplayedTime =
+                        mPostViewPictureCallbackTime - mShutterCallbackTime;
+                mPictureDisplayedToJpegCallbackTime =
+                        mJpegPictureCallbackTime - mPostViewPictureCallbackTime;
+            } else {
+                mShutterToPictureDisplayedTime =
+                        mRawPictureCallbackTime - mShutterCallbackTime;
+                mPictureDisplayedToJpegCallbackTime =
+                        mJpegPictureCallbackTime - mRawPictureCallbackTime;
+            }
+            Log.v(TAG, "mPictureDisplayedToJpegCallbackTime = "
+                    + mPictureDisplayedToJpegCallbackTime + "ms");
+
+             /*TODO:
+            // Only animate when in full screen capture mode
+            // i.e. If monkey/a user swipes to the gallery during picture taking,
+            // don't show animation
+            if (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent
+                    && mActivity.mShowCameraAppView) {
+                // Finish capture animation
+                mHandler.removeMessages(CAPTURE_ANIMATION_DONE);
+                ((CameraScreenNail) mActivity.mCameraScreenNail).animateSlide();
+                mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+                        CaptureAnimManager.getAnimationDuration());
+            } */
+            mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden.
+            if (!mIsImageCaptureIntent) {
+                if (ApiHelper.CAN_START_PREVIEW_IN_JPEG_CALLBACK) {
+                    setupPreview();
+                } else {
+                    // Camera HAL of some devices have a bug. Starting preview
+                    // immediately after taking a picture will fail. Wait some
+                    // time before starting the preview.
+                    mHandler.sendEmptyMessageDelayed(SETUP_PREVIEW, 300);
+                }
+            }
+
+            if (!mIsImageCaptureIntent) {
+                // Calculate the width and the height of the jpeg.
+                Size s = mParameters.getPictureSize();
+                ExifInterface exif = Exif.getExif(jpegData);
+                int orientation = Exif.getOrientation(exif);
+                int width, height;
+                if ((mJpegRotation + orientation) % 180 == 0) {
+                    width = s.width;
+                    height = s.height;
+                } else {
+                    width = s.height;
+                    height = s.width;
+                }
+                String title = mNamedImages.getTitle();
+                long date = mNamedImages.getDate();
+                if (title == null) {
+                    Log.e(TAG, "Unbalanced name/data pair");
+                } else {
+                    if (date == -1) date = mCaptureStartTime;
+                    if (mHeading >= 0) {
+                        // heading direction has been updated by the sensor.
+                        ExifTag directionRefTag = exif.buildTag(
+                                ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+                                ExifInterface.GpsTrackRef.MAGNETIC_DIRECTION);
+                        ExifTag directionTag = exif.buildTag(
+                                ExifInterface.TAG_GPS_IMG_DIRECTION,
+                                new Rational(mHeading, 1));
+                        exif.setTag(directionRefTag);
+                        exif.setTag(directionTag);
+                    }
+                    mActivity.getMediaSaveService().addImage(
+                            jpegData, title, date, mLocation, width, height,
+                            orientation, exif, mOnMediaSavedListener, mContentResolver);
+                }
+            } else {
+                mJpegImageData = jpegData;
+                if (!mQuickCapture) {
+                    mUI.showPostCaptureAlert();
+                } else {
+                    onCaptureDone();
+                }
+            }
+
+            // Check 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.
+            mActivity.updateStorageSpaceAndHint();
+
+            long now = System.currentTimeMillis();
+            mJpegCallbackFinishTime = now - mJpegPictureCallbackTime;
+            Log.v(TAG, "mJpegCallbackFinishTime = "
+                    + mJpegCallbackFinishTime + "ms");
+            mJpegPictureCallbackTime = 0;
+        }
+    }
+
+    private final class AutoFocusCallback implements CameraAFCallback {
+        @Override
+        public void onAutoFocus(
+                boolean focused, CameraProxy camera) {
+            if (mPaused) return;
+
+            mAutoFocusTime = System.currentTimeMillis() - mFocusStartTime;
+            Log.v(TAG, "mAutoFocusTime = " + mAutoFocusTime + "ms");
+            setCameraState(IDLE);
+            mFocusManager.onAutoFocus(focused, mUI.isShutterPressed());
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private final class AutoFocusMoveCallback
+            implements CameraAFMoveCallback {
+        @Override
+        public void onAutoFocusMoving(
+            boolean moving, CameraProxy camera) {
+                mFocusManager.onAutoFocusMoving(moving);
+        }
+    }
+
+    private static class NamedImages {
+        private ArrayList<NamedEntity> mQueue;
+        private boolean mStop;
+        private NamedEntity mNamedEntity;
+
+        public NamedImages() {
+            mQueue = new ArrayList<NamedEntity>();
+        }
+
+        public void nameNewImage(ContentResolver resolver, long date) {
+            NamedEntity r = new NamedEntity();
+            r.title = Util.createJpegName(date);
+            r.date = date;
+            mQueue.add(r);
+        }
+
+        public String getTitle() {
+            if (mQueue.isEmpty()) {
+                mNamedEntity = null;
+                return null;
+            }
+            mNamedEntity = mQueue.get(0);
+            mQueue.remove(0);
+
+            return mNamedEntity.title;
+        }
+
+        // Must be called after getTitle().
+        public long getDate() {
+            if (mNamedEntity == null) return -1;
+            return mNamedEntity.date;
+        }
+
+        private static class NamedEntity {
+            String title;
+            long date;
+        }
+    }
+
+    private void setCameraState(int state) {
+        mCameraState = state;
+        switch (state) {
+        case PhotoController.PREVIEW_STOPPED:
+        case PhotoController.SNAPSHOT_IN_PROGRESS:
+        case PhotoController.SWITCHING_CAMERA:
+            mUI.enableGestures(false);
+            break;
+        case PhotoController.IDLE:
+            mUI.enableGestures(true);
+            break;
+        }
+    }
+
+    private void animateFlash() {
+        // Only animate when in full screen capture mode
+        // i.e. If monkey/a user swipes to the gallery during picture taking,
+        // don't show animation
+        if (!mIsImageCaptureIntent) {
+            mUI.animateFlash();
+
+            // TODO: mUI.enablePreviewThumb(true);
+            // mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+            //        CaptureAnimManager.getAnimationDuration());
+        }
+    }
+
+    @Override
+    public boolean capture() {
+        // If we are already in the middle of taking a snapshot or the image save request
+        // is full then ignore.
+        if (mCameraDevice == null || mCameraState == SNAPSHOT_IN_PROGRESS
+                || mCameraState == SWITCHING_CAMERA
+                || mActivity.getMediaSaveService().isQueueFull()) {
+            return false;
+        }
+        mCaptureStartTime = System.currentTimeMillis();
+        mPostViewPictureCallbackTime = 0;
+        mJpegImageData = null;
+
+        final boolean animateBefore = (mSceneMode == Util.SCENE_MODE_HDR);
+
+        if (animateBefore) {
+            animateFlash();
+        }
+
+        // Set rotation and gps data.
+        int orientation;
+        // We need to be consistent with the framework orientation (i.e. the
+        // orientation of the UI.) when the auto-rotate screen setting is on.
+        if (mActivity.isAutoRotateScreen()) {
+            orientation = (360 - mDisplayRotation) % 360;
+        } else {
+            orientation = mOrientation;
+        }
+        mJpegRotation = Util.getJpegRotation(mCameraId, orientation);
+        mParameters.setRotation(mJpegRotation);
+        Location loc = mLocationManager.getCurrentLocation();
+        Util.setGpsParameters(mParameters, loc);
+        mCameraDevice.setParameters(mParameters);
+
+        mCameraDevice.takePicture(mHandler,
+                new ShutterCallback(!animateBefore),
+                mRawPictureCallback, mPostViewPictureCallback,
+                new JpegPictureCallback(loc));
+
+        mNamedImages.nameNewImage(mContentResolver, mCaptureStartTime);
+
+        mFaceDetectionStarted = false;
+        setCameraState(SNAPSHOT_IN_PROGRESS);
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+                UsageStatistics.ACTION_CAPTURE_DONE, "Photo");
+        return true;
+    }
+
+    @Override
+    public void setFocusParameters() {
+        setCameraParameters(UPDATE_PARAM_PREFERENCE);
+    }
+
+    private int getPreferredCameraId(ComboPreferences preferences) {
+        int intentCameraId = Util.getCameraFacingIntentExtras(mActivity);
+        if (intentCameraId != -1) {
+            // Testing purpose. Launch a specific camera through the intent
+            // extras.
+            return intentCameraId;
+        } else {
+            return CameraSettings.readPreferredCameraId(preferences);
+        }
+    }
+
+    private void updateSceneMode() {
+        // If scene mode is set, we cannot set flash mode, white balance, and
+        // focus mode, instead, we read it from driver
+        if (!Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) {
+            overrideCameraSettings(mParameters.getFlashMode(),
+                    mParameters.getWhiteBalance(), mParameters.getFocusMode());
+        } else {
+            overrideCameraSettings(null, null, null);
+        }
+    }
+
+    private void overrideCameraSettings(final String flashMode,
+            final String whiteBalance, final String focusMode) {
+        mUI.overrideSettings(
+                CameraSettings.KEY_FLASH_MODE, flashMode,
+                CameraSettings.KEY_WHITE_BALANCE, whiteBalance,
+                CameraSettings.KEY_FOCUS_MODE, focusMode);
+    }
+
+    private void loadCameraPreferences() {
+        CameraSettings settings = new CameraSettings(mActivity, mInitialParams,
+                mCameraId, CameraHolder.instance().getCameraInfo());
+        mPreferenceGroup = settings.getPreferenceGroup(R.xml.camera_preferences);
+    }
+
+    @Override
+    public void onOrientationChanged(int orientation) {
+        // We keep the last known orientation. So if the user first orient
+        // the camera then point the camera to floor or sky, we still have
+        // the correct orientation.
+        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;
+        mOrientation = Util.roundOrientation(orientation, mOrientation);
+
+        // Show the toast after getting the first orientation changed.
+        if (mHandler.hasMessages(SHOW_TAP_TO_FOCUS_TOAST)) {
+            mHandler.removeMessages(SHOW_TAP_TO_FOCUS_TOAST);
+            showTapToFocusToast();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        if (mMediaProviderClient != null) {
+            mMediaProviderClient.release();
+            mMediaProviderClient = null;
+        }
+    }
+
+    @Override
+    public void onCaptureCancelled() {
+        mActivity.setResultEx(Activity.RESULT_CANCELED, new Intent());
+        mActivity.finish();
+    }
+
+    @Override
+    public void onCaptureRetake() {
+        if (mPaused)
+            return;
+        mUI.hidePostCaptureAlert();
+        setupPreview();
+    }
+
+    @Override
+    public void onCaptureDone() {
+        if (mPaused) {
+            return;
+        }
+
+        byte[] data = mJpegImageData;
+
+        if (mCropValue == null) {
+            // First handle the no crop case -- just return the value.  If the
+            // caller specifies a "save uri" then write the data to its
+            // stream. Otherwise, pass back a scaled down version of the bitmap
+            // directly in the extras.
+            if (mSaveUri != null) {
+                OutputStream outputStream = null;
+                try {
+                    outputStream = mContentResolver.openOutputStream(mSaveUri);
+                    outputStream.write(data);
+                    outputStream.close();
+
+                    mActivity.setResultEx(Activity.RESULT_OK);
+                    mActivity.finish();
+                } catch (IOException ex) {
+                    // ignore exception
+                } finally {
+                    Util.closeSilently(outputStream);
+                }
+            } else {
+                ExifInterface exif = Exif.getExif(data);
+                int orientation = Exif.getOrientation(exif);
+                Bitmap bitmap = Util.makeBitmap(data, 50 * 1024);
+                bitmap = Util.rotate(bitmap, orientation);
+                mActivity.setResultEx(Activity.RESULT_OK,
+                        new Intent("inline-data").putExtra("data", bitmap));
+                mActivity.finish();
+            }
+        } else {
+            // Save the image to a temp file and invoke the cropper
+            Uri tempUri = null;
+            FileOutputStream tempStream = null;
+            try {
+                File path = mActivity.getFileStreamPath(sTempCropFilename);
+                path.delete();
+                tempStream = mActivity.openFileOutput(sTempCropFilename, 0);
+                tempStream.write(data);
+                tempStream.close();
+                tempUri = Uri.fromFile(path);
+            } catch (FileNotFoundException ex) {
+                mActivity.setResultEx(Activity.RESULT_CANCELED);
+                mActivity.finish();
+                return;
+            } catch (IOException ex) {
+                mActivity.setResultEx(Activity.RESULT_CANCELED);
+                mActivity.finish();
+                return;
+            } finally {
+                Util.closeSilently(tempStream);
+            }
+
+            Bundle newExtras = new Bundle();
+            if (mCropValue.equals("circle")) {
+                newExtras.putString("circleCrop", "true");
+            }
+            if (mSaveUri != null) {
+                newExtras.putParcelable(MediaStore.EXTRA_OUTPUT, mSaveUri);
+            } else {
+                newExtras.putBoolean(CropExtras.KEY_RETURN_DATA, true);
+            }
+            if (mActivity.isSecureCamera()) {
+                newExtras.putBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, true);
+            }
+
+            Intent cropIntent = new Intent(CropActivity.CROP_ACTION);
+
+            cropIntent.setData(tempUri);
+            cropIntent.putExtras(newExtras);
+
+            mActivity.startActivityForResult(cropIntent, REQUEST_CROP);
+        }
+    }
+
+    @Override
+    public void onShutterButtonFocus(boolean pressed) {
+        if (mPaused || mUI.collapseCameraControls()
+                || (mCameraState == SNAPSHOT_IN_PROGRESS)
+                || (mCameraState == PREVIEW_STOPPED)) return;
+
+        // Do not do focus if there is not enough storage.
+        if (pressed && !canTakePicture()) return;
+
+        if (pressed) {
+            mFocusManager.onShutterDown();
+        } else {
+            // for countdown mode, we need to postpone the shutter release
+            // i.e. lock the focus during countdown.
+            if (!mUI.isCountingDown()) {
+                mFocusManager.onShutterUp();
+            }
+        }
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        if (mPaused || mUI.collapseCameraControls()
+                || (mCameraState == SWITCHING_CAMERA)
+                || (mCameraState == PREVIEW_STOPPED)) return;
+
+        // Do not take the picture if there is not enough storage.
+        if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) {
+            Log.i(TAG, "Not enough space or storage not ready. remaining="
+                    + mActivity.getStorageSpace());
+            return;
+        }
+        Log.v(TAG, "onShutterButtonClick: mCameraState=" + mCameraState);
+
+        if (mSceneMode == Util.SCENE_MODE_HDR) {
+            mUI.hideSwitcher();
+            mUI.setSwipingEnabled(false);
+        }
+        // If the user wants to do a snapshot while the previous one is still
+        // in progress, remember the fact and do it after we finish the previous
+        // one and re-start the preview. Snapshot in progress also includes the
+        // state that autofocus is focusing and a picture will be taken when
+        // focus callback arrives.
+        if ((mFocusManager.isFocusingSnapOnFinish() || mCameraState == SNAPSHOT_IN_PROGRESS)
+                && !mIsImageCaptureIntent) {
+            mSnapshotOnIdle = true;
+            return;
+        }
+
+        String timer = mPreferences.getString(
+                CameraSettings.KEY_TIMER,
+                mActivity.getString(R.string.pref_camera_timer_default));
+        boolean playSound = mPreferences.getString(CameraSettings.KEY_TIMER_SOUND_EFFECTS,
+                mActivity.getString(R.string.pref_camera_timer_sound_default))
+                .equals(mActivity.getString(R.string.setting_on_value));
+
+        int seconds = Integer.parseInt(timer);
+        // When shutter button is pressed, check whether the previous countdown is
+        // finished. If not, cancel the previous countdown and start a new one.
+        if (mUI.isCountingDown()) {
+            mUI.cancelCountDown();
+        }
+        if (seconds > 0) {
+            mUI.startCountDown(seconds, playSound);
+        } else {
+           mSnapshotOnIdle = false;
+           mFocusManager.doSnap();
+        }
+    }
+
+    @Override
+    public void installIntentFilter() {
+    }
+
+    @Override
+    public boolean updateStorageHintOnResume() {
+        return mFirstTimeInitialized;
+    }
+
+    @Override
+    public void updateCameraAppView() {
+    }
+
+    @Override
+    public void onResumeBeforeSuper() {
+        mPaused = false;
+    }
+
+    @Override
+    public void onResumeAfterSuper() {
+        if (mOpenCameraFail || mCameraDisabled) return;
+
+        mJpegPictureCallbackTime = 0;
+        mZoomValue = 0;
+        // Start the preview if it is not started.
+        if (mCameraState == PREVIEW_STOPPED && mCameraStartUpThread == null) {
+            resetExposureCompensation();
+            mCameraStartUpThread = new CameraStartUpThread();
+            mCameraStartUpThread.start();
+        }
+
+        // If first time initialization is not finished, put it in the
+        // message queue.
+        if (!mFirstTimeInitialized) {
+            mHandler.sendEmptyMessage(FIRST_TIME_INIT);
+        } else {
+            initializeSecondTime();
+        }
+        keepScreenOnAwhile();
+
+        // Dismiss open menu if exists.
+        PopupManager.getInstance(mActivity).notifyShowPopup(null);
+        UsageStatistics.onContentViewChanged(
+                UsageStatistics.COMPONENT_CAMERA, "PhotoModule");
+
+        Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        if (gsensor != null) {
+            mSensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_NORMAL);
+        }
+
+        Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        if (msensor != null) {
+            mSensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_NORMAL);
+        }
+    }
+
+    void waitCameraStartUpThread() {
+        try {
+            if (mCameraStartUpThread != null) {
+                mCameraStartUpThread.cancel();
+                mCameraStartUpThread.join();
+                mCameraStartUpThread = null;
+                setCameraState(IDLE);
+            }
+        } catch (InterruptedException e) {
+            // ignore
+        }
+    }
+
+    @Override
+    public void onPauseBeforeSuper() {
+        mPaused = true;
+        Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        if (gsensor != null) {
+            mSensorManager.unregisterListener(this, gsensor);
+        }
+
+        Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+        if (msensor != null) {
+            mSensorManager.unregisterListener(this, msensor);
+        }
+    }
+
+    @Override
+    public void onPauseAfterSuper() {
+        // Wait the camera start up thread to finish.
+        waitCameraStartUpThread();
+
+        // When camera is started from secure lock screen for the first time
+        // after screen on, the activity gets onCreate->onResume->onPause->onResume.
+        // To reduce the latency, keep the camera for a short time so it does
+        // not need to be opened again.
+        if (mCameraDevice != null && mActivity.isSecureCamera()
+                && CameraActivity.isFirstStartAfterScreenOn()) {
+            CameraActivity.resetFirstStartAfterScreenOn();
+            CameraHolder.instance().keep(KEEP_CAMERA_TIMEOUT);
+        }
+        // Reset the focus first. Camera CTS does not guarantee that
+        // cancelAutoFocus is allowed after preview stops.
+        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
+            mCameraDevice.cancelAutoFocus();
+        }
+        stopPreview();
+
+        mNamedImages = null;
+
+        if (mLocationManager != null) mLocationManager.recordLocation(false);
+
+        // If we are in an image capture intent and has taken
+        // a picture, we just clear it in onPause.
+        mJpegImageData = null;
+
+        // Remove the messages in the event queue.
+        mHandler.removeMessages(SETUP_PREVIEW);
+        mHandler.removeMessages(FIRST_TIME_INIT);
+        mHandler.removeMessages(CHECK_DISPLAY_ROTATION);
+        mHandler.removeMessages(SWITCH_CAMERA);
+        mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION);
+        mHandler.removeMessages(CAMERA_OPEN_DONE);
+        mHandler.removeMessages(START_PREVIEW_DONE);
+        mHandler.removeMessages(OPEN_CAMERA_FAIL);
+        mHandler.removeMessages(CAMERA_DISABLED);
+
+        closeCamera();
+
+        resetScreenOn();
+        mUI.onPause();
+
+        mPendingSwitchCameraId = -1;
+        if (mFocusManager != null) mFocusManager.removeMessages();
+        MediaSaveService s = mActivity.getMediaSaveService();
+        if (s != null) {
+            s.setListener(null);
+        }
+    }
+
+    /**
+     * The focus manager is the first UI related element to get initialized,
+     * and it requires the RenderOverlay, so initialize it here
+     */
+    private void initializeFocusManager() {
+        // Create FocusManager object. startPreview needs it.
+        // if mFocusManager not null, reuse it
+        // otherwise create a new instance
+        if (mFocusManager != null) {
+            mFocusManager.removeMessages();
+        } else {
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT);
+            String[] defaultFocusModes = mActivity.getResources().getStringArray(
+                    R.array.pref_camera_focusmode_default_array);
+            mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes,
+                    mInitialParams, this, mirror,
+                    mActivity.getMainLooper(), mUI);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        Log.v(TAG, "onConfigurationChanged");
+        setDisplayOrientation();
+    }
+
+    @Override
+    public void updateCameraOrientation() {
+        if (mDisplayRotation != Util.getDisplayRotation(mActivity)) {
+            setDisplayOrientation();
+        }
+    }
+
+    @Override
+    public void onActivityResult(
+            int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_CROP: {
+                Intent intent = new Intent();
+                if (data != null) {
+                    Bundle extras = data.getExtras();
+                    if (extras != null) {
+                        intent.putExtras(extras);
+                    }
+                }
+                mActivity.setResultEx(resultCode, intent);
+                mActivity.finish();
+
+                File path = mActivity.getFileStreamPath(sTempCropFilename);
+                path.delete();
+
+                break;
+            }
+        }
+    }
+
+    private boolean canTakePicture() {
+        return isCameraIdle() && (mActivity.getStorageSpace() > Storage.LOW_STORAGE_THRESHOLD);
+    }
+
+    @Override
+    public void autoFocus() {
+        mFocusStartTime = System.currentTimeMillis();
+        mCameraDevice.autoFocus(mHandler, mAutoFocusCallback);
+        setCameraState(FOCUSING);
+    }
+
+    @Override
+    public void cancelAutoFocus() {
+        mCameraDevice.cancelAutoFocus();
+        setCameraState(IDLE);
+        setCameraParameters(UPDATE_PARAM_PREFERENCE);
+    }
+
+    // Preview area is touched. Handle touch focus.
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+        if (mPaused || mCameraDevice == null || !mFirstTimeInitialized
+                || mCameraState == SNAPSHOT_IN_PROGRESS
+                || mCameraState == SWITCHING_CAMERA
+                || mCameraState == PREVIEW_STOPPED) {
+            return;
+        }
+
+        // Do not trigger touch focus if popup window is opened.
+        if (mUI.removeTopLevelPopup()) return;
+
+        // Check if metering area or focus area is supported.
+        if (!mFocusAreaSupported && !mMeteringAreaSupported) return;
+        mFocusManager.onSingleTapUp(x, y);
+    }
+
+    @Override
+    public boolean onBackPressed() {
+        return mUI.onBackPressed();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+        case KeyEvent.KEYCODE_VOLUME_UP:
+        case KeyEvent.KEYCODE_VOLUME_DOWN:
+        case KeyEvent.KEYCODE_FOCUS:
+            if (/*TODO: mActivity.isInCameraApp() &&*/ mFirstTimeInitialized) {
+                if (event.getRepeatCount() == 0) {
+                    onShutterButtonFocus(true);
+                }
+                return true;
+            }
+            return false;
+        case KeyEvent.KEYCODE_CAMERA:
+            if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
+                onShutterButtonClick();
+            }
+            return true;
+        case KeyEvent.KEYCODE_DPAD_CENTER:
+            // If we get a dpad center event without any focused view, move
+            // the focus to the shutter button and press it.
+            if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
+                // Start auto-focus immediately to reduce shutter lag. After
+                // the shutter button gets the focus, onShutterButtonFocus()
+                // will be called again but it is fine.
+                if (mUI.removeTopLevelPopup()) return true;
+                onShutterButtonFocus(true);
+                mUI.pressShutterButton();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+        case KeyEvent.KEYCODE_VOLUME_UP:
+        case KeyEvent.KEYCODE_VOLUME_DOWN:
+            if (/*mActivity.isInCameraApp() && */ mFirstTimeInitialized) {
+                onShutterButtonClick();
+                return true;
+            }
+            return false;
+        case KeyEvent.KEYCODE_FOCUS:
+            if (mFirstTimeInitialized) {
+                onShutterButtonFocus(false);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void closeCamera() {
+        if (mCameraDevice != null) {
+            mCameraDevice.setZoomChangeListener(null);
+            if(ApiHelper.HAS_FACE_DETECTION) {
+                mCameraDevice.setFaceDetectionCallback(null, null);
+            }
+            mCameraDevice.setErrorCallback(null);
+            CameraHolder.instance().release();
+            mFaceDetectionStarted = false;
+            mCameraDevice = null;
+            setCameraState(PREVIEW_STOPPED);
+            mFocusManager.onCameraReleased();
+        }
+    }
+
+    private void setDisplayOrientation() {
+        mDisplayRotation = Util.getDisplayRotation(mActivity);
+        mDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
+        mCameraDisplayOrientation = mDisplayOrientation;
+        mUI.setDisplayOrientation(mDisplayOrientation);
+        if (mFocusManager != null) {
+            mFocusManager.setDisplayOrientation(mDisplayOrientation);
+        }
+        // Change the camera display orientation
+        if (mCameraDevice != null) {
+            mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+        }
+    }
+
+    // Only called by UI thread.
+    private void setupPreview() {
+        mFocusManager.resetTouchFocus();
+        startPreview();
+        setCameraState(IDLE);
+        startFaceDetection();
+    }
+
+    // This can be called by UI Thread or CameraStartUpThread. So this should
+    // not modify the views.
+    private void startPreview() {
+        mCameraDevice.setErrorCallback(mErrorCallback);
+
+        // ICS camera frameworks has a bug. Face detection state is not cleared
+        // after taking a picture. Stop the preview to work around it. The bug
+        // was fixed in JB.
+        if (mCameraState != PREVIEW_STOPPED) stopPreview();
+
+        setDisplayOrientation();
+
+        if (!mSnapshotOnIdle) {
+            // If the focus mode is continuous autofocus, call cancelAutoFocus to
+            // resume it because it may have been paused by autoFocus call.
+            if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusManager.getFocusMode())) {
+                mCameraDevice.cancelAutoFocus();
+            }
+            mFocusManager.setAeAwbLock(false); // Unlock AE and AWB.
+        }
+        setCameraParameters(UPDATE_PARAM_ALL);
+        // Let UI set its expected aspect ratio
+        mUI.setPreviewSize(mParameters.getPreviewSize());
+        Object st = mUI.getSurfaceTexture();
+        if (st != null) {
+           mCameraDevice.setPreviewTexture((SurfaceTexture) st);
+        }
+
+        Log.v(TAG, "startPreview");
+        mCameraDevice.startPreview();
+        mFocusManager.onPreviewStarted();
+
+        if (mSnapshotOnIdle) {
+            mHandler.post(mDoSnapRunnable);
+        }
+    }
+
+    @Override
+    public void stopPreview() {
+        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
+            Log.v(TAG, "stopPreview");
+            mCameraDevice.stopPreview();
+            mFaceDetectionStarted = false;
+        }
+        setCameraState(PREVIEW_STOPPED);
+        if (mFocusManager != null) mFocusManager.onPreviewStopped();
+    }
+
+    @SuppressWarnings("deprecation")
+    private void updateCameraParametersInitialize() {
+        // Reset preview frame rate to the maximum because it may be lowered by
+        // video camera application.
+        int[] fpsRange = Util.getMaxPreviewFpsRange(mParameters);
+        if (fpsRange.length > 0) {
+            mParameters.setPreviewFpsRange(
+                    fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX],
+                    fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]);
+        }
+
+        mParameters.set(Util.RECORDING_HINT, Util.FALSE);
+
+        // Disable video stabilization. Convenience methods not available in API
+        // level <= 14
+        String vstabSupported = mParameters.get("video-stabilization-supported");
+        if ("true".equals(vstabSupported)) {
+            mParameters.set("video-stabilization", "false");
+        }
+    }
+
+    private void updateCameraParametersZoom() {
+        // Set zoom.
+        if (mParameters.isZoomSupported()) {
+            mParameters.setZoom(mZoomValue);
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private void setAutoExposureLockIfSupported() {
+        if (mAeLockSupported) {
+            mParameters.setAutoExposureLock(mFocusManager.getAeAwbLock());
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private void setAutoWhiteBalanceLockIfSupported() {
+        if (mAwbLockSupported) {
+            mParameters.setAutoWhiteBalanceLock(mFocusManager.getAeAwbLock());
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void setFocusAreasIfSupported() {
+        if (mFocusAreaSupported) {
+            mParameters.setFocusAreas(mFocusManager.getFocusAreas());
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void setMeteringAreasIfSupported() {
+        if (mMeteringAreaSupported) {
+            // Use the same area for focus and metering.
+            mParameters.setMeteringAreas(mFocusManager.getMeteringAreas());
+        }
+    }
+
+    private void updateCameraParametersPreference() {
+        setAutoExposureLockIfSupported();
+        setAutoWhiteBalanceLockIfSupported();
+        setFocusAreasIfSupported();
+        setMeteringAreasIfSupported();
+
+        // Set picture size.
+        String pictureSize = mPreferences.getString(
+                CameraSettings.KEY_PICTURE_SIZE, null);
+        if (pictureSize == null) {
+            CameraSettings.initialCameraPictureSize(mActivity, mParameters);
+        } else {
+            List<Size> supported = mParameters.getSupportedPictureSizes();
+            CameraSettings.setCameraPictureSize(
+                    pictureSize, supported, mParameters);
+        }
+        Size size = mParameters.getPictureSize();
+
+        // Set a preview size that is closest to the viewfinder height and has
+        // the right aspect ratio.
+        List<Size> sizes = mParameters.getSupportedPreviewSizes();
+        Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes,
+                (double) size.width / size.height);
+        Size original = mParameters.getPreviewSize();
+        if (!original.equals(optimalSize)) {
+            mParameters.setPreviewSize(optimalSize.width, optimalSize.height);
+
+            // Zoom related settings will be changed for different preview
+            // sizes, so set and read the parameters to get latest values
+            if (mHandler.getLooper() == Looper.myLooper()) {
+                // On UI thread only, not when camera starts up
+                setupPreview();
+            } else {
+                mCameraDevice.setParameters(mParameters);
+            }
+            mParameters = mCameraDevice.getParameters();
+        }
+        Log.v(TAG, "Preview size is " + optimalSize.width + "x" + optimalSize.height);
+
+        // Since changing scene mode may change supported values, set scene mode
+        // first. HDR is a scene mode. To promote it in UI, it is stored in a
+        // separate preference.
+        String hdr = mPreferences.getString(CameraSettings.KEY_CAMERA_HDR,
+                mActivity.getString(R.string.pref_camera_hdr_default));
+        if (mActivity.getString(R.string.setting_on_value).equals(hdr)) {
+            mSceneMode = Util.SCENE_MODE_HDR;
+        } else {
+            mSceneMode = mPreferences.getString(
+                CameraSettings.KEY_SCENE_MODE,
+                mActivity.getString(R.string.pref_camera_scenemode_default));
+        }
+        if (Util.isSupported(mSceneMode, mParameters.getSupportedSceneModes())) {
+            if (!mParameters.getSceneMode().equals(mSceneMode)) {
+                mParameters.setSceneMode(mSceneMode);
+
+                // Setting scene mode will change the settings of flash mode,
+                // white balance, and focus mode. Here we read back the
+                // parameters, so we can know those settings.
+                mCameraDevice.setParameters(mParameters);
+                mParameters = mCameraDevice.getParameters();
+            }
+        } else {
+            mSceneMode = mParameters.getSceneMode();
+            if (mSceneMode == null) {
+                mSceneMode = Parameters.SCENE_MODE_AUTO;
+            }
+        }
+
+        // Set JPEG quality.
+        int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
+                CameraProfile.QUALITY_HIGH);
+        mParameters.setJpegQuality(jpegQuality);
+
+        // For the following settings, we need to check if the settings are
+        // still supported by latest driver, if not, ignore the settings.
+
+        // Set exposure compensation
+        int value = CameraSettings.readExposure(mPreferences);
+        int max = mParameters.getMaxExposureCompensation();
+        int min = mParameters.getMinExposureCompensation();
+        if (value >= min && value <= max) {
+            mParameters.setExposureCompensation(value);
+        } else {
+            Log.w(TAG, "invalid exposure range: " + value);
+        }
+
+        if (Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) {
+            // Set flash mode.
+            String flashMode = mPreferences.getString(
+                    CameraSettings.KEY_FLASH_MODE,
+                    mActivity.getString(R.string.pref_camera_flashmode_default));
+            List<String> supportedFlash = mParameters.getSupportedFlashModes();
+            if (Util.isSupported(flashMode, supportedFlash)) {
+                mParameters.setFlashMode(flashMode);
+            } else {
+                flashMode = mParameters.getFlashMode();
+                if (flashMode == null) {
+                    flashMode = mActivity.getString(
+                            R.string.pref_camera_flashmode_no_flash);
+                }
+            }
+
+            // Set white balance parameter.
+            String whiteBalance = mPreferences.getString(
+                    CameraSettings.KEY_WHITE_BALANCE,
+                    mActivity.getString(R.string.pref_camera_whitebalance_default));
+            if (Util.isSupported(whiteBalance,
+                    mParameters.getSupportedWhiteBalance())) {
+                mParameters.setWhiteBalance(whiteBalance);
+            } else {
+                whiteBalance = mParameters.getWhiteBalance();
+                if (whiteBalance == null) {
+                    whiteBalance = Parameters.WHITE_BALANCE_AUTO;
+                }
+            }
+
+            // Set focus mode.
+            mFocusManager.overrideFocusMode(null);
+            mParameters.setFocusMode(mFocusManager.getFocusMode());
+        } else {
+            mFocusManager.overrideFocusMode(mParameters.getFocusMode());
+        }
+
+        if (mContinousFocusSupported && ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK) {
+            updateAutoFocusMoveCallback();
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private void updateAutoFocusMoveCallback() {
+        if (mParameters.getFocusMode().equals(Util.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+            mCameraDevice.setAutoFocusMoveCallback(mHandler,
+                    (CameraManager.CameraAFMoveCallback) mAutoFocusMoveCallback);
+        } else {
+            mCameraDevice.setAutoFocusMoveCallback(null, null);
+        }
+    }
+
+    // We separate the parameters into several subsets, so we can update only
+    // the subsets actually need updating. The PREFERENCE set needs extra
+    // locking because the preference can be changed from GLThread as well.
+    private void setCameraParameters(int updateSet) {
+        if ((updateSet & UPDATE_PARAM_INITIALIZE) != 0) {
+            updateCameraParametersInitialize();
+        }
+
+        if ((updateSet & UPDATE_PARAM_ZOOM) != 0) {
+            updateCameraParametersZoom();
+        }
+
+        if ((updateSet & UPDATE_PARAM_PREFERENCE) != 0) {
+            updateCameraParametersPreference();
+        }
+
+        mCameraDevice.setParameters(mParameters);
+    }
+
+    // If the Camera is idle, update the parameters immediately, otherwise
+    // accumulate them in mUpdateSet and update later.
+    private void setCameraParametersWhenIdle(int additionalUpdateSet) {
+        mUpdateSet |= additionalUpdateSet;
+        if (mCameraDevice == null) {
+            // We will update all the parameters when we open the device, so
+            // we don't need to do anything now.
+            mUpdateSet = 0;
+            return;
+        } else if (isCameraIdle()) {
+            setCameraParameters(mUpdateSet);
+            updateSceneMode();
+            mUpdateSet = 0;
+        } else {
+            if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) {
+                mHandler.sendEmptyMessageDelayed(
+                        SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000);
+            }
+        }
+    }
+
+    public boolean isCameraIdle() {
+        return (mCameraState == IDLE) ||
+                (mCameraState == PREVIEW_STOPPED) ||
+                ((mFocusManager != null) && mFocusManager.isFocusCompleted()
+                        && (mCameraState != SWITCHING_CAMERA));
+    }
+
+    public boolean isImageCaptureIntent() {
+        String action = mActivity.getIntent().getAction();
+        return (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
+                || CameraActivity.ACTION_IMAGE_CAPTURE_SECURE.equals(action));
+    }
+
+    private void setupCaptureParams() {
+        Bundle myExtras = mActivity.getIntent().getExtras();
+        if (myExtras != null) {
+            mSaveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+            mCropValue = myExtras.getString("crop");
+        }
+    }
+
+    @Override
+    public void onSharedPreferenceChanged() {
+        // ignore the events after "onPause()"
+        if (mPaused) return;
+
+        boolean recordLocation = RecordLocationPreference.get(
+                mPreferences, mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+
+        setCameraParametersWhenIdle(UPDATE_PARAM_PREFERENCE);
+        mUI.updateOnScreenIndicators(mParameters, mPreferenceGroup, mPreferences);
+    }
+
+    @Override
+    public void onCameraPickerClicked(int cameraId) {
+        if (mPaused || mPendingSwitchCameraId != -1) return;
+
+        mPendingSwitchCameraId = cameraId;
+
+        Log.v(TAG, "Start to switch camera. cameraId=" + cameraId);
+        // We need to keep a preview frame for the animation before
+        // releasing the camera. This will trigger onPreviewTextureCopied.
+        //TODO: Need to animate the camera switch
+        switchCamera();
+    }
+
+    // Preview texture has been copied. Now camera can be released and the
+    // animation can be started.
+    @Override
+    public void onPreviewTextureCopied() {
+        mHandler.sendEmptyMessage(SWITCH_CAMERA);
+    }
+
+    @Override
+    public void onCaptureTextureCopied() {
+    }
+
+    @Override
+    public void onUserInteraction() {
+      if (!mActivity.isFinishing()) keepScreenOnAwhile();
+    }
+
+    private void resetScreenOn() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private void keepScreenOnAwhile() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+    }
+
+    @Override
+    public void onOverriddenPreferencesClicked() {
+        if (mPaused) return;
+        mUI.showPreferencesToast();
+    }
+
+    private void showTapToFocusToast() {
+        // TODO: Use a toast?
+        new RotateTextToast(mActivity, R.string.tap_to_focus, 0).show();
+        // Clear the preference.
+        Editor editor = mPreferences.edit();
+        editor.putBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, false);
+        editor.apply();
+    }
+
+    private void initializeCapabilities() {
+        mInitialParams = mCameraDevice.getParameters();
+        mFocusAreaSupported = Util.isFocusAreaSupported(mInitialParams);
+        mMeteringAreaSupported = Util.isMeteringAreaSupported(mInitialParams);
+        mAeLockSupported = Util.isAutoExposureLockSupported(mInitialParams);
+        mAwbLockSupported = Util.isAutoWhiteBalanceLockSupported(mInitialParams);
+        mContinousFocusSupported = mInitialParams.getSupportedFocusModes().contains(
+                Util.FOCUS_MODE_CONTINUOUS_PICTURE);
+    }
+
+    @Override
+    public void onCountDownFinished() {
+        mSnapshotOnIdle = false;
+        mFocusManager.doSnap();
+        mFocusManager.onShutterUp();
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+        mUI.onShowSwitcherPopup();
+    }
+
+    @Override
+    public int onZoomChanged(int index) {
+        // Not useful to change zoom value when the activity is paused.
+        if (mPaused) return index;
+        mZoomValue = index;
+        if (mParameters == null || mCameraDevice == null) return index;
+        // Set zoom parameters asynchronously
+        mParameters.setZoom(mZoomValue);
+        mCameraDevice.setParameters(mParameters);
+        Parameters p = mCameraDevice.getParameters();
+        if (p != null) return p.getZoom();
+        return index;
+    }
+
+    @Override
+    public int getCameraState() {
+        return mCameraState;
+    }
+
+    @Override
+    public void onQueueStatus(boolean full) {
+        mUI.enableShutter(!full);
+    }
+
+    @Override
+    public void onMediaSaveServiceConnected(MediaSaveService s) {
+        // We set the listener only when both service and shutterbutton
+        // are initialized.
+        if (mFirstTimeInitialized) {
+            s.setListener(this);
+        }
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        int type = event.sensor.getType();
+        float[] data;
+        if (type == Sensor.TYPE_ACCELEROMETER) {
+            data = mGData;
+        } else if (type == Sensor.TYPE_MAGNETIC_FIELD) {
+            data = mMData;
+        } else {
+            // we should not be here.
+            return;
+        }
+        for (int i = 0; i < 3 ; i++) {
+            data[i] = event.values[i];
+        }
+        float[] orientation = new float[3];
+        SensorManager.getRotationMatrix(mR, null, mGData, mMData);
+        SensorManager.getOrientation(mR, orientation);
+        mHeading = (int) (orientation[0] * 180f / Math.PI) % 360;
+        if (mHeading < 0) {
+            mHeading += 360;
+        }
+    }
+
+    @Override
+    public void onSwitchMode(boolean toCamera) {
+        mUI.onSwitchMode(toCamera);
+    }
+
+/* Below is no longer needed, except to get rid of compile error
+ * TODO: Remove these
+ */
+
+    // TODO: Delete this function after old camera code is removed
+    @Override
+    public void onRestorePreferencesClicked() {}
+
+}
diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java
new file mode 100644
index 0000000..d58ed7f
--- /dev/null
+++ b/src/com/android/camera/PhotoUI.java
@@ -0,0 +1,864 @@
+/*
+ * Copyright (C) 2012 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.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.Face;
+import android.hardware.Camera.FaceDetectionListener;
+import android.hardware.Camera.Size;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.TextureView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.Toast;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.FocusOverlayManager.FocusUI;
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CameraControls;
+import com.android.camera.ui.CameraRootView;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.CountDownView;
+import com.android.camera.ui.CountDownView.OnCountDownFinishedListener;
+import com.android.camera.ui.FaceView;
+import com.android.camera.ui.FocusIndicator;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.PieRenderer.PieListener;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.List;
+
+public class PhotoUI implements PieListener,
+    PreviewGestures.SingleTapListener,
+    FocusUI, TextureView.SurfaceTextureListener,
+    LocationManager.Listener, CameraRootView.MyDisplayListener,
+    CameraManager.CameraFaceDetectionCallback {
+
+    private static final String TAG = "CAM_UI";
+    private static final int UPDATE_TRANSFORM_MATRIX = 1;
+    private CameraActivity mActivity;
+    private PhotoController mController;
+    private PreviewGestures mGestures;
+
+    private View mRootView;
+    private Object mSurfaceTexture;
+
+    private AbstractSettingPopup mPopup;
+    private ShutterButton mShutterButton;
+    private CountDownView mCountDownView;
+
+    private FaceView mFaceView;
+    private RenderOverlay mRenderOverlay;
+    private View mReviewCancelButton;
+    private View mReviewDoneButton;
+    private View mReviewRetakeButton;
+
+    private View mMenuButton;
+    private View mBlocker;
+    private PhotoMenu mMenu;
+    private CameraSwitcher mSwitcher;
+    private CameraControls mCameraControls;
+    private AlertDialog mLocationDialog;
+
+    // Small indicators which show the camera settings in the viewfinder.
+    private OnScreenIndicators mOnScreenIndicators;
+
+    private PieRenderer mPieRenderer;
+    private ZoomRenderer mZoomRenderer;
+    private Toast mNotSelectableToast;
+
+    private int mZoomMax;
+    private List<Integer> mZoomRatios;
+
+    private int mPreviewWidth = 0;
+    private int mPreviewHeight = 0;
+    private float mSurfaceTextureUncroppedWidth;
+    private float mSurfaceTextureUncroppedHeight;
+
+    private View mPreviewThumb;
+    private ObjectAnimator mFlashAnim;
+    private View mFlashOverlay;
+
+    private SurfaceTextureSizeChangedListener mSurfaceTextureSizeListener;
+    private TextureView mTextureView;
+    private Matrix mMatrix = null;
+    private float mAspectRatio = 4f / 3f;
+    private final Object mLock = new Object();
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case UPDATE_TRANSFORM_MATRIX:
+                    setTransformMatrix(mPreviewWidth, mPreviewHeight);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    public interface SurfaceTextureSizeChangedListener {
+        public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight);
+    }
+
+    private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right,
+                int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+            int width = right - left;
+            int height = bottom - top;
+            // Full-screen screennail
+            int w = width;
+            int h = height;
+            if (Util.getDisplayRotation(mActivity) % 180 != 0) {
+                w = height;
+                h = width;
+            }
+            if (mPreviewWidth != width || mPreviewHeight != height) {
+                mPreviewWidth = width;
+                mPreviewHeight = height;
+                onScreenSizeChanged(width, height, w, h);
+                mController.onScreenSizeChanged(width, height, w, h);
+            }
+        }
+    };
+
+    private ValueAnimator.AnimatorListener mAnimatorListener =
+            new ValueAnimator.AnimatorListener() {
+
+        @Override
+        public void onAnimationCancel(Animator arg0) {}
+
+        @Override
+        public void onAnimationEnd(Animator arg0) {
+            mFlashOverlay.setAlpha(0f);
+            mFlashOverlay.setVisibility(View.GONE);
+            mFlashAnim.removeListener(this);
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator arg0) {}
+
+        @Override
+        public void onAnimationStart(Animator arg0) {}
+    };
+
+    public PhotoUI(CameraActivity activity, PhotoController controller, View parent) {
+        mActivity = activity;
+        mController = controller;
+        mRootView = parent;
+
+        mActivity.getLayoutInflater().inflate(R.layout.photo_module,
+                (ViewGroup) mRootView, true);
+        mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+        mFlashOverlay = mRootView.findViewById(R.id.flash_overlay);
+        // display the view
+        mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content);
+        mTextureView.setSurfaceTextureListener(this);
+        mTextureView.addOnLayoutChangeListener(mLayoutListener);
+        initIndicators();
+
+        mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
+        mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
+        mSwitcher.setCurrentIndex(CameraSwitcher.PHOTO_MODULE_INDEX);
+        mSwitcher.setSwitchListener((CameraSwitchListener) mActivity);
+        mMenuButton = mRootView.findViewById(R.id.menu);
+        if (ApiHelper.HAS_FACE_DETECTION) {
+            ViewStub faceViewStub = (ViewStub) mRootView
+                    .findViewById(R.id.face_view_stub);
+            if (faceViewStub != null) {
+                faceViewStub.inflate();
+                mFaceView = (FaceView) mRootView.findViewById(R.id.face_view);
+                setSurfaceTextureSizeChangedListener(
+                        (SurfaceTextureSizeChangedListener) mFaceView);
+            }
+        }
+        mCameraControls = (CameraControls) mRootView.findViewById(R.id.camera_controls);
+        ((CameraRootView) mRootView).setDisplayChangeListener(this);
+    }
+
+    public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+        setTransformMatrix(width, height);
+    }
+
+    public void setSurfaceTextureSizeChangedListener(SurfaceTextureSizeChangedListener listener) {
+        mSurfaceTextureSizeListener = listener;
+    }
+
+    public void setPreviewSize(Size size) {
+        int width = size.width;
+        int height = size.height;
+        if (width == 0 || height == 0) {
+            Log.w(TAG, "Preview size should not be 0.");
+            return;
+        }
+        if (width > height) {
+            mAspectRatio = (float) width / height;
+        } else {
+            mAspectRatio = (float) height / width;
+        }
+        mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX);
+    }
+
+    private void setTransformMatrix(int width, int height) {
+        mMatrix = mTextureView.getTransform(mMatrix);
+        int orientation = Util.getDisplayRotation(mActivity);
+        float scaleX = 1f, scaleY = 1f;
+        float scaledTextureWidth, scaledTextureHeight;
+        if (width > height) {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height * mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int)(width / mAspectRatio));
+        } else {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height / mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int) (width * mAspectRatio));
+        }
+
+        if (mSurfaceTextureUncroppedWidth != scaledTextureWidth ||
+                mSurfaceTextureUncroppedHeight != scaledTextureHeight) {
+            mSurfaceTextureUncroppedWidth = scaledTextureWidth;
+            mSurfaceTextureUncroppedHeight = scaledTextureHeight;
+            if (mSurfaceTextureSizeListener != null) {
+                mSurfaceTextureSizeListener.onSurfaceTextureSizeChanged(
+                        (int) mSurfaceTextureUncroppedWidth, (int) mSurfaceTextureUncroppedHeight);
+            }
+        }
+        scaleX = scaledTextureWidth / width;
+        scaleY = scaledTextureHeight / height;
+        mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2);
+        mTextureView.setTransform(mMatrix);
+    }
+
+    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+        synchronized (mLock) {
+            mSurfaceTexture = surface;
+            mLock.notifyAll();
+        }
+    }
+
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+        // Ignored, Camera does all the work for us
+    }
+
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        mSurfaceTexture = null;
+        mController.stopPreview();
+        Log.w(TAG, "surfaceTexture is destroyed");
+        return true;
+    }
+
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+        // Invoked every time there's a new Camera preview frame
+    }
+
+    public View getRootView() {
+        return mRootView;
+    }
+
+    private void initIndicators() {
+        mOnScreenIndicators = new OnScreenIndicators(mActivity,
+                mRootView.findViewById(R.id.on_screen_indicators));
+    }
+
+    public void onCameraOpened(PreferenceGroup prefGroup, ComboPreferences prefs,
+            Camera.Parameters params, OnPreferenceChangedListener listener) {
+        if (mPieRenderer == null) {
+            mPieRenderer = new PieRenderer(mActivity);
+            mPieRenderer.setPieListener(this);
+            mRenderOverlay.addRenderer(mPieRenderer);
+        }
+
+        if (mMenu == null) {
+            mMenu = new PhotoMenu(mActivity, this, mPieRenderer);
+            mMenu.setListener(listener);
+        }
+        mMenu.initialize(prefGroup);
+
+        if (mZoomRenderer == null) {
+            mZoomRenderer = new ZoomRenderer(mActivity);
+            mRenderOverlay.addRenderer(mZoomRenderer);
+        }
+
+        if (mGestures == null) {
+            // this will handle gesture disambiguation and dispatching
+            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
+            mRenderOverlay.setGestures(mGestures);
+        }
+        mGestures.setZoomEnabled(params.isZoomSupported());
+        mGestures.setRenderOverlay(mRenderOverlay);
+        mRenderOverlay.requestLayout();
+
+        initializeZoom(params);
+        updateOnScreenIndicators(params, prefGroup, prefs);
+    }
+
+    private void openMenu() {
+        if (mPieRenderer != null) {
+            // If autofocus is not finished, cancel autofocus so that the
+            // subsequent touch can be handled by PreviewGestures
+            if (mController.getCameraState() == PhotoController.FOCUSING) {
+                    mController.cancelAutoFocus();
+            }
+            mPieRenderer.showInCenter();
+        }
+    }
+
+    public void initializeControlByIntent() {
+        mBlocker = mRootView.findViewById(R.id.blocker);
+        mPreviewThumb = mRootView.findViewById(R.id.preview_thumb);
+        mPreviewThumb.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // TODO: go to filmstrip
+                // mActivity.gotoGallery();
+            }
+        });
+        mMenuButton = mRootView.findViewById(R.id.menu);
+        mMenuButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                openMenu();
+            }
+        });
+        if (mController.isImageCaptureIntent()) {
+            hideSwitcher();
+            ViewGroup cameraControls = (ViewGroup) mRootView.findViewById(R.id.camera_controls);
+            mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls);
+
+            mReviewDoneButton = mRootView.findViewById(R.id.btn_done);
+            mReviewCancelButton = mRootView.findViewById(R.id.btn_cancel);
+            mReviewRetakeButton = mRootView.findViewById(R.id.btn_retake);
+            mReviewCancelButton.setVisibility(View.VISIBLE);
+
+            mReviewDoneButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onCaptureDone();
+                }
+            });
+            mReviewCancelButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onCaptureCancelled();
+                }
+            });
+
+            mReviewRetakeButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onCaptureRetake();
+                }
+            });
+        }
+    }
+
+    public void hideUI() {
+        mCameraControls.setVisibility(View.INVISIBLE);
+        mSwitcher.closePopup();
+    }
+
+    public void showUI() {
+        mCameraControls.setVisibility(View.VISIBLE);
+    }
+
+    public void hideSwitcher() {
+        mSwitcher.closePopup();
+        mSwitcher.setVisibility(View.INVISIBLE);
+    }
+
+    public void showSwitcher() {
+        mSwitcher.setVisibility(View.VISIBLE);
+    }
+    // called from onResume but only the first time
+    public  void initializeFirstTime() {
+        // Initialize shutter button.
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+        mShutterButton.setOnShutterButtonListener(mController);
+        mShutterButton.setVisibility(View.VISIBLE);
+    }
+
+    // called from onResume every other time
+    public void initializeSecondTime(Camera.Parameters params) {
+        initializeZoom(params);
+        if (mController.isImageCaptureIntent()) {
+            hidePostCaptureAlert();
+        }
+        if (mMenu != null) {
+            mMenu.reloadPreferences();
+        }
+    }
+
+    public void showLocationDialog() {
+        mLocationDialog = new AlertDialog.Builder(mActivity)
+                .setTitle(R.string.remember_location_title)
+                .setMessage(R.string.remember_location_prompt)
+                .setPositiveButton(R.string.remember_location_yes,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int arg1) {
+                                mController.enableRecordingLocation(true);
+                                mLocationDialog = null;
+                            }
+                        })
+                .setNegativeButton(R.string.remember_location_no,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int arg1) {
+                                dialog.cancel();
+                            }
+                        })
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    @Override
+                    public void onCancel(DialogInterface dialog) {
+                        mController.enableRecordingLocation(false);
+                        mLocationDialog = null;
+                    }
+                })
+                .show();
+    }
+
+    public void initializeZoom(Camera.Parameters params) {
+        if ((params == null) || !params.isZoomSupported()
+                || (mZoomRenderer == null)) return;
+        mZoomMax = params.getMaxZoom();
+        mZoomRatios = params.getZoomRatios();
+        // Currently we use immediate zoom for fast zooming to get better UX and
+        // there is no plan to take advantage of the smooth zoom.
+        if (mZoomRenderer != null) {
+            mZoomRenderer.setZoomMax(mZoomMax);
+            mZoomRenderer.setZoom(params.getZoom());
+            mZoomRenderer.setZoomValue(mZoomRatios.get(params.getZoom()));
+            mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+        }
+    }
+
+    public void showGpsOnScreenIndicator(boolean hasSignal) { }
+
+    public void hideGpsOnScreenIndicator() { }
+
+    public void overrideSettings(final String ... keyvalues) {
+        mMenu.overrideSettings(keyvalues);
+    }
+
+    public void updateOnScreenIndicators(Camera.Parameters params,
+            PreferenceGroup group, ComboPreferences prefs) {
+        if (params == null) return;
+        mOnScreenIndicators.updateSceneOnScreenIndicator(params.getSceneMode());
+        mOnScreenIndicators.updateExposureOnScreenIndicator(params,
+                CameraSettings.readExposure(prefs));
+        mOnScreenIndicators.updateFlashOnScreenIndicator(params.getFlashMode());
+        int wbIndex = 2;
+        ListPreference pref = group.findPreference(CameraSettings.KEY_WHITE_BALANCE);
+        if (pref != null) {
+            wbIndex = pref.getCurrentIndex();
+        }
+        mOnScreenIndicators.updateWBIndicator(wbIndex);
+        boolean location = RecordLocationPreference.get(
+                prefs, mActivity.getContentResolver());
+        mOnScreenIndicators.updateLocationIndicator(location);
+    }
+
+    public void setCameraState(int state) {
+    }
+
+    public void animateFlash() {
+        // End the previous animation if the previous one is still running
+        if (mFlashAnim != null && mFlashAnim.isRunning()) {
+            mFlashAnim.end();
+        }
+        // Start new flash animation.
+        mFlashOverlay.setVisibility(View.VISIBLE);
+        mFlashAnim = ObjectAnimator.ofFloat((Object) mFlashOverlay, "alpha", 0.3f, 0f);
+        mFlashAnim.setDuration(300);
+        mFlashAnim.addListener(mAnimatorListener);
+        mFlashAnim.start();
+    }
+
+    public void enableGestures(boolean enable) {
+        if (mGestures != null) {
+            mGestures.setEnabled(enable);
+        }
+    }
+
+    // forward from preview gestures to controller
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+        mController.onSingleTapUp(view, x, y);
+    }
+
+    public boolean onBackPressed() {
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+            return true;
+        }
+        // In image capture mode, back button should:
+        // 1) if there is any popup, dismiss them, 2) otherwise, get out of
+        // image capture
+        if (mController.isImageCaptureIntent()) {
+            if (!removeTopLevelPopup()) {
+                // no popup to dismiss, cancel image capture
+                mController.onCaptureCancelled();
+            }
+            return true;
+        } else if (!mController.isCameraIdle()) {
+            // ignore backs while we're taking a picture
+            return true;
+        } else {
+            return removeTopLevelPopup();
+        }
+    }
+
+    public void onSwitchMode(boolean toCamera) {
+        if (toCamera) {
+            showUI();
+        } else {
+            hideUI();
+        }
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(!toCamera);
+        }
+        if (mPopup != null) {
+            dismissPopup(toCamera);
+        }
+        if (mGestures != null) {
+            mGestures.setEnabled(toCamera);
+        }
+        if (mRenderOverlay != null) {
+            // this can not happen in capture mode
+            mRenderOverlay.setVisibility(toCamera ? View.VISIBLE : View.GONE);
+        }
+        if (mPieRenderer != null) {
+            mPieRenderer.setBlockFocus(!toCamera);
+        }
+        setShowMenu(toCamera);
+        if (!toCamera && mCountDownView != null) mCountDownView.cancelCountDown();
+    }
+
+    public void enablePreviewThumb(boolean enabled) {
+        if (enabled) {
+            mPreviewThumb.setVisibility(View.VISIBLE);
+        } else {
+            mPreviewThumb.setVisibility(View.GONE);
+        }
+    }
+
+    public boolean removeTopLevelPopup() {
+        // Remove the top level popup or dialog box and return true if there's any
+        if (mPopup != null) {
+            dismissPopup();
+            return true;
+        }
+        return false;
+    }
+
+    public void showPopup(AbstractSettingPopup popup) {
+        hideUI();
+        mBlocker.setVisibility(View.INVISIBLE);
+        setShowMenu(false);
+        mPopup = popup;
+        mPopup.setVisibility(View.VISIBLE);
+        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT);
+        lp.gravity = Gravity.CENTER;
+        ((FrameLayout) mRootView).addView(mPopup, lp);
+    }
+
+    public void dismissPopup() {
+        dismissPopup(true);
+    }
+
+    private void dismissPopup(boolean fullScreen) {
+        if (fullScreen) {
+            showUI();
+            mBlocker.setVisibility(View.VISIBLE);
+        }
+        setShowMenu(fullScreen);
+        if (mPopup != null) {
+            ((FrameLayout) mRootView).removeView(mPopup);
+            mPopup = null;
+        }
+        mMenu.popupDismissed();
+    }
+
+    public void onShowSwitcherPopup() {
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+        }
+    }
+
+    private void setShowMenu(boolean show) {
+        if (mOnScreenIndicators != null) {
+            mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+        if (mMenuButton != null) {
+            mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    public boolean collapseCameraControls() {
+        // Remove all the popups/dialog boxes
+        boolean ret = false;
+        if (mPopup != null) {
+            dismissPopup();
+            ret = true;
+        }
+        onShowSwitcherPopup();
+        return ret;
+    }
+
+    protected void showPostCaptureAlert() {
+        mOnScreenIndicators.setVisibility(View.GONE);
+        mMenuButton.setVisibility(View.GONE);
+        Util.fadeIn(mReviewDoneButton);
+        mShutterButton.setVisibility(View.INVISIBLE);
+        Util.fadeIn(mReviewRetakeButton);
+        pauseFaceDetection();
+    }
+
+    protected void hidePostCaptureAlert() {
+        mOnScreenIndicators.setVisibility(View.VISIBLE);
+        mMenuButton.setVisibility(View.VISIBLE);
+        Util.fadeOut(mReviewDoneButton);
+        mShutterButton.setVisibility(View.VISIBLE);
+        Util.fadeOut(mReviewRetakeButton);
+        resumeFaceDetection();
+    }
+
+    public void setDisplayOrientation(int orientation) {
+        if (mFaceView != null) {
+            mFaceView.setDisplayOrientation(orientation);
+        }
+    }
+
+    // shutter button handling
+
+    public boolean isShutterPressed() {
+        return mShutterButton.isPressed();
+    }
+
+    public void enableShutter(boolean enabled) {
+        if (mShutterButton != null) {
+            mShutterButton.setEnabled(enabled);
+        }
+    }
+
+    public void pressShutterButton() {
+        if (mShutterButton.isInTouchMode()) {
+            mShutterButton.requestFocusFromTouch();
+        } else {
+            mShutterButton.requestFocus();
+        }
+        mShutterButton.setPressed(true);
+    }
+
+    private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+        @Override
+        public void onZoomValueChanged(int index) {
+            int newZoom = mController.onZoomChanged(index);
+            if (mZoomRenderer != null) {
+                mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom));
+            }
+        }
+
+        @Override
+        public void onZoomStart() {
+            if (mPieRenderer != null) {
+                mPieRenderer.setBlockFocus(true);
+            }
+        }
+
+        @Override
+        public void onZoomEnd() {
+            if (mPieRenderer != null) {
+                mPieRenderer.setBlockFocus(false);
+            }
+        }
+    }
+
+    @Override
+    public void onPieOpened(int centerX, int centerY) {
+        setSwipingEnabled(false);
+        dismissPopup();
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(true);
+        }
+    }
+
+    @Override
+    public void onPieClosed() {
+        setSwipingEnabled(true);
+        if (mFaceView != null) {
+            mFaceView.setBlockDraw(false);
+        }
+    }
+
+    public void setSwipingEnabled(boolean enable) {
+        mActivity.setSwipingEnabled(enable);
+    }
+
+    public Object getSurfaceTexture() {
+        synchronized (mLock) {
+            if (mSurfaceTexture == null) {
+                try {
+                    mLock.wait();
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "Unexpected interruption when waiting to get surface texture");
+                }
+            }
+        }
+        return mSurfaceTexture;
+    }
+
+    // Countdown timer
+
+    private void initializeCountDown() {
+        mActivity.getLayoutInflater().inflate(R.layout.count_down_to_capture,
+                (ViewGroup) mRootView, true);
+        mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture));
+        mCountDownView.setCountDownFinishedListener((OnCountDownFinishedListener) mController);
+    }
+
+    public boolean isCountingDown() {
+        return mCountDownView != null && mCountDownView.isCountingDown();
+    }
+
+    public void cancelCountDown() {
+        if (mCountDownView == null) return;
+        mCountDownView.cancelCountDown();
+    }
+
+    public void startCountDown(int sec, boolean playSound) {
+        if (mCountDownView == null) initializeCountDown();
+        mCountDownView.startCountDown(sec, playSound);
+    }
+
+    public void showPreferencesToast() {
+        if (mNotSelectableToast == null) {
+            String str = mActivity.getResources().getString(R.string.not_selectable_in_scene_mode);
+            mNotSelectableToast = Toast.makeText(mActivity, str, Toast.LENGTH_SHORT);
+        }
+        mNotSelectableToast.show();
+    }
+
+    public void onPause() {
+        cancelCountDown();
+
+        // Clear UI.
+        collapseCameraControls();
+        if (mFaceView != null) mFaceView.clear();
+
+        if (mLocationDialog != null && mLocationDialog.isShowing()) {
+            mLocationDialog.dismiss();
+        }
+        mLocationDialog = null;
+        mPreviewWidth = 0;
+        mPreviewHeight = 0;
+    }
+
+    // focus UI implementation
+
+    private FocusIndicator getFocusIndicator() {
+        return (mFaceView != null && mFaceView.faceExists()) ? mFaceView : mPieRenderer;
+    }
+
+    @Override
+    public boolean hasFaces() {
+        return (mFaceView != null && mFaceView.faceExists());
+    }
+
+    public void clearFaces() {
+        if (mFaceView != null) mFaceView.clear();
+    }
+
+    @Override
+    public void clearFocus() {
+        FocusIndicator indicator = getFocusIndicator();
+        if (indicator != null) indicator.clear();
+    }
+
+    @Override
+    public void setFocusPosition(int x, int y) {
+        mPieRenderer.setFocus(x, y);
+    }
+
+    @Override
+    public void onFocusStarted() {
+        getFocusIndicator().showStart();
+    }
+
+    @Override
+    public void onFocusSucceeded(boolean timeout) {
+        getFocusIndicator().showSuccess(timeout);
+    }
+
+    @Override
+    public void onFocusFailed(boolean timeout) {
+        getFocusIndicator().showFail(timeout);
+    }
+
+    @Override
+    public void pauseFaceDetection() {
+        if (mFaceView != null) mFaceView.pause();
+    }
+
+    @Override
+    public void resumeFaceDetection() {
+        if (mFaceView != null) mFaceView.resume();
+    }
+
+    public void onStartFaceDetection(int orientation, boolean mirror) {
+        mFaceView.clear();
+        mFaceView.setVisibility(View.VISIBLE);
+        mFaceView.setDisplayOrientation(orientation);
+        mFaceView.setMirror(mirror);
+        mFaceView.resume();
+    }
+
+    @Override
+    public void onFaceDetection(Face[] faces, CameraManager.CameraProxy camera) {
+        mFaceView.setFaces(faces);
+    }
+
+    public void onDisplayChanged() {
+        mCameraControls.checkLayoutFlip();
+        mController.updateCameraOrientation();
+    }
+
+}
diff --git a/src/com/android/camera/PieController.java b/src/com/android/camera/PieController.java
new file mode 100644
index 0000000..3cbcb4b
--- /dev/null
+++ b/src/com/android/camera/PieController.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2012 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.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.drawable.TextDrawable;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PieController {
+
+    private static String TAG = "CAM_piecontrol";
+
+    protected static final int MODE_PHOTO = 0;
+    protected static final int MODE_VIDEO = 1;
+
+    protected static float CENTER = (float) Math.PI / 2;
+    protected static final float SWEEP = 0.06f;
+
+    protected Activity mActivity;
+    protected PreferenceGroup mPreferenceGroup;
+    protected OnPreferenceChangedListener mListener;
+    protected PieRenderer mRenderer;
+    private List<IconListPreference> mPreferences;
+    private Map<IconListPreference, PieItem> mPreferenceMap;
+    private Map<IconListPreference, String> mOverrides;
+
+    public void setListener(OnPreferenceChangedListener listener) {
+        mListener = listener;
+    }
+
+    public PieController(Activity activity, PieRenderer pie) {
+        mActivity = activity;
+        mRenderer = pie;
+        mPreferences = new ArrayList<IconListPreference>();
+        mPreferenceMap = new HashMap<IconListPreference, PieItem>();
+        mOverrides = new HashMap<IconListPreference, String>();
+    }
+
+    public void initialize(PreferenceGroup group) {
+        mRenderer.clearItems();
+        mPreferenceMap.clear();
+        setPreferenceGroup(group);
+    }
+
+    public void onSettingChanged(ListPreference pref) {
+        if (mListener != null) {
+            mListener.onSharedPreferenceChanged();
+        }
+    }
+
+    protected void setCameraId(int cameraId) {
+        ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+        pref.setValue("" + cameraId);
+    }
+
+    protected PieItem makeItem(int resId) {
+        // We need a mutable version as we change the alpha
+        Drawable d = mActivity.getResources().getDrawable(resId).mutate();
+        return new PieItem(d, 0);
+    }
+
+    protected PieItem makeItem(CharSequence value) {
+        TextDrawable drawable = new TextDrawable(mActivity.getResources(), value);
+        return new PieItem(drawable, 0);
+    }
+
+    public PieItem makeItem(String prefKey) {
+        final IconListPreference pref =
+                (IconListPreference) mPreferenceGroup.findPreference(prefKey);
+        if (pref == null) return null;
+        int[] iconIds = pref.getLargeIconIds();
+        int resid = -1;
+        if (!pref.getUseSingleIcon() && iconIds != null) {
+            // Each entry has a corresponding icon.
+            int index = pref.findIndexOfValue(pref.getValue());
+            resid = iconIds[index];
+        } else {
+            // The preference only has a single icon to represent it.
+            resid = pref.getSingleIcon();
+        }
+        PieItem item = makeItem(resid);
+        item.setLabel(pref.getTitle().toUpperCase());
+        mPreferences.add(pref);
+        mPreferenceMap.put(pref, item);
+        int nOfEntries = pref.getEntries().length;
+        if (nOfEntries > 1) {
+            for (int i = 0; i < nOfEntries; i++) {
+                PieItem inner = null;
+                if (iconIds != null) {
+                    inner = makeItem(iconIds[i]);
+                } else {
+                    inner = makeItem(pref.getEntries()[i]);
+                }
+                inner.setLabel(pref.getLabels()[i]);
+                item.addItem(inner);
+                final int index = i;
+                inner.setOnClickListener(new OnClickListener() {
+                    @Override
+                    public void onClick(PieItem item) {
+                        pref.setValueIndex(index);
+                        reloadPreference(pref);
+                        onSettingChanged(pref);
+                    }
+                });
+            }
+        }
+        return item;
+    }
+
+    public PieItem makeSwitchItem(final String prefKey, boolean addListener) {
+        final IconListPreference pref =
+                (IconListPreference) mPreferenceGroup.findPreference(prefKey);
+        if (pref == null) return null;
+        int[] iconIds = pref.getLargeIconIds();
+        int resid = -1;
+        int index = pref.findIndexOfValue(pref.getValue());
+        if (!pref.getUseSingleIcon() && iconIds != null) {
+            // Each entry has a corresponding icon.
+            resid = iconIds[index];
+        } else {
+            // The preference only has a single icon to represent it.
+            resid = pref.getSingleIcon();
+        }
+        PieItem item = makeItem(resid);
+        item.setLabel(pref.getLabels()[index]);
+        item.setImageResource(mActivity, resid);
+        mPreferences.add(pref);
+        mPreferenceMap.put(pref, item);
+        if (addListener) {
+            final PieItem fitem = item;
+            item.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(PieItem item) {
+                    IconListPreference pref = (IconListPreference) mPreferenceGroup
+                            .findPreference(prefKey);
+                    int index = pref.findIndexOfValue(pref.getValue());
+                    CharSequence[] values = pref.getEntryValues();
+                    index = (index + 1) % values.length;
+                    pref.setValueIndex(index);
+                    fitem.setLabel(pref.getLabels()[index]);
+                    fitem.setImageResource(mActivity,
+                            ((IconListPreference) pref).getLargeIconIds()[index]);
+                    reloadPreference(pref);
+                    onSettingChanged(pref);
+                }
+            });
+        }
+        return item;
+    }
+
+
+    public PieItem makeDialItem(ListPreference pref, int iconId, float center, float sweep) {
+        PieItem item = makeItem(iconId);
+        return item;
+    }
+
+    public void addItem(String prefKey) {
+        PieItem item = makeItem(prefKey);
+        mRenderer.addItem(item);
+    }
+
+    public void updateItem(PieItem item, String prefKey) {
+        IconListPreference pref = (IconListPreference) mPreferenceGroup
+                .findPreference(prefKey);
+        if (pref != null) {
+            int index = pref.findIndexOfValue(pref.getValue());
+            item.setLabel(pref.getLabels()[index]);
+            item.setImageResource(mActivity,
+                    ((IconListPreference) pref).getLargeIconIds()[index]);
+        }
+    }
+
+    public void setPreferenceGroup(PreferenceGroup group) {
+        mPreferenceGroup = group;
+    }
+
+    public void reloadPreferences() {
+        mPreferenceGroup.reloadValue();
+        for (IconListPreference pref : mPreferenceMap.keySet()) {
+            reloadPreference(pref);
+        }
+    }
+
+    private void reloadPreference(IconListPreference pref) {
+        if (pref.getUseSingleIcon()) return;
+        PieItem item = mPreferenceMap.get(pref);
+        String overrideValue = mOverrides.get(pref);
+        int[] iconIds = pref.getLargeIconIds();
+        if (iconIds != null) {
+            // Each entry has a corresponding icon.
+            int index;
+            if (overrideValue == null) {
+                index = pref.findIndexOfValue(pref.getValue());
+            } else {
+                index = pref.findIndexOfValue(overrideValue);
+                if (index == -1) {
+                    // Avoid the crash if camera driver has bugs.
+                    Log.e(TAG, "Fail to find override value=" + overrideValue);
+                    pref.print();
+                    return;
+                }
+            }
+            item.setImageResource(mActivity, iconIds[index]);
+        } else {
+            // The preference only has a single icon to represent it.
+            item.setImageResource(mActivity, pref.getSingleIcon());
+        }
+    }
+
+    // Scene mode may override other camera settings (ex: flash mode).
+    public void overrideSettings(final String ... keyvalues) {
+        if (keyvalues.length % 2 != 0) {
+            throw new IllegalArgumentException();
+        }
+        for (IconListPreference pref : mPreferenceMap.keySet()) {
+            override(pref, keyvalues);
+        }
+    }
+
+    private void override(IconListPreference pref, final String ... keyvalues) {
+        mOverrides.remove(pref);
+        for (int i = 0; i < keyvalues.length; i += 2) {
+            String key = keyvalues[i];
+            String value = keyvalues[i + 1];
+            if (key.equals(pref.getKey())) {
+                mOverrides.put(pref, value);
+                PieItem item = mPreferenceMap.get(pref);
+                item.setEnabled(value == null);
+                break;
+            }
+        }
+        reloadPreference(pref);
+    }
+}
diff --git a/src/com/android/camera/PreferenceGroup.java b/src/com/android/camera/PreferenceGroup.java
new file mode 100644
index 0000000..4d0519f
--- /dev/null
+++ b/src/com/android/camera/PreferenceGroup.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import java.util.ArrayList;
+
+/**
+ * A collection of <code>CameraPreference</code>s. It may contain other
+ * <code>PreferenceGroup</code> and form a tree structure.
+ */
+public class PreferenceGroup extends CameraPreference {
+    private ArrayList<CameraPreference> list =
+            new ArrayList<CameraPreference>();
+
+    public PreferenceGroup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void addChild(CameraPreference child) {
+        list.add(child);
+    }
+
+    public void removePreference(int index) {
+        list.remove(index);
+    }
+
+    public CameraPreference get(int index) {
+        return list.get(index);
+    }
+
+    public int size() {
+        return list.size();
+    }
+
+    @Override
+    public void reloadValue() {
+        for (CameraPreference pref : list) {
+            pref.reloadValue();
+        }
+    }
+
+    /**
+     * Finds the preference with the given key recursively. Returns
+     * <code>null</code> if cannot find.
+     */
+    public ListPreference findPreference(String key) {
+        // Find a leaf preference with the given key. Currently, the base
+        // type of all "leaf" preference is "ListPreference". If we add some
+        // other types later, we need to change the code.
+        for (CameraPreference pref : list) {
+            if (pref instanceof ListPreference) {
+                ListPreference listPref = (ListPreference) pref;
+                if(listPref.getKey().equals(key)) return listPref;
+            } else if(pref instanceof PreferenceGroup) {
+                ListPreference listPref =
+                        ((PreferenceGroup) pref).findPreference(key);
+                if (listPref != null) return listPref;
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/camera/PreferenceInflater.java b/src/com/android/camera/PreferenceInflater.java
new file mode 100644
index 0000000..231c983
--- /dev/null
+++ b/src/com/android/camera/PreferenceInflater.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 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.util.AttributeSet;
+import android.util.Xml;
+import android.view.InflateException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Inflate <code>CameraPreference</code> from XML resource.
+ */
+public class PreferenceInflater {
+    private static final String PACKAGE_NAME =
+            PreferenceInflater.class.getPackage().getName();
+
+    private static final Class<?>[] CTOR_SIGNATURE =
+            new Class[] {Context.class, AttributeSet.class};
+    private static final HashMap<String, Constructor<?>> sConstructorMap =
+            new HashMap<String, Constructor<?>>();
+
+    private Context mContext;
+
+    public PreferenceInflater(Context context) {
+        mContext = context;
+    }
+
+    public CameraPreference inflate(int resId) {
+        return inflate(mContext.getResources().getXml(resId));
+    }
+
+    private CameraPreference newPreference(String tagName, Object[] args) {
+        String name = PACKAGE_NAME + "." + tagName;
+        Constructor<?> constructor = sConstructorMap.get(name);
+        try {
+            if (constructor == null) {
+                // Class not found in the cache, see if it's real, and try to
+                // add it
+                Class<?> clazz = mContext.getClassLoader().loadClass(name);
+                constructor = clazz.getConstructor(CTOR_SIGNATURE);
+                sConstructorMap.put(name, constructor);
+            }
+            return (CameraPreference) constructor.newInstance(args);
+        } catch (NoSuchMethodException e) {
+            throw new InflateException("Error inflating class " + name, e);
+        } catch (ClassNotFoundException e) {
+            throw new InflateException("No such class: " + name, e);
+        } catch (Exception e) {
+            throw new InflateException("While create instance of" + name, e);
+        }
+    }
+
+    private CameraPreference inflate(XmlPullParser parser) {
+
+        AttributeSet attrs = Xml.asAttributeSet(parser);
+        ArrayList<CameraPreference> list = new ArrayList<CameraPreference>();
+        Object args[] = new Object[]{mContext, attrs};
+
+        try {
+            for (int type = parser.next();
+                    type != XmlPullParser.END_DOCUMENT; type = parser.next()) {
+                if (type != XmlPullParser.START_TAG) continue;
+                CameraPreference pref = newPreference(parser.getName(), args);
+
+                int depth = parser.getDepth();
+                if (depth > list.size()) {
+                    list.add(pref);
+                } else {
+                    list.set(depth - 1, pref);
+                }
+                if (depth > 1) {
+                    ((PreferenceGroup) list.get(depth - 2)).addChild(pref);
+                }
+            }
+
+            if (list.size() == 0) {
+                throw new InflateException("No root element found");
+            }
+            return list.get(0);
+        } catch (XmlPullParserException e) {
+            throw new InflateException(e);
+        } catch (IOException e) {
+            throw new InflateException(parser.getPositionDescription(), e);
+        }
+    }
+}
diff --git a/src/com/android/camera/PreviewFrameLayout.java b/src/com/android/camera/PreviewFrameLayout.java
new file mode 100644
index 0000000..03ef91c
--- /dev/null
+++ b/src/com/android/camera/PreviewFrameLayout.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewStub;
+import android.widget.RelativeLayout;
+
+import com.android.camera.ui.LayoutChangeHelper;
+import com.android.camera.ui.LayoutChangeNotifier;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * A layout which handles the preview aspect ratio.
+ */
+public class PreviewFrameLayout extends RelativeLayout implements LayoutChangeNotifier {
+
+    private static final String TAG = "CAM_preview";
+
+    /** A callback to be invoked when the preview frame's size changes. */
+    public interface OnSizeChangedListener {
+        public void onSizeChanged(int width, int height);
+    }
+
+    private double mAspectRatio;
+    private View mBorder;
+    private OnSizeChangedListener mListener;
+    private LayoutChangeHelper mLayoutChangeHelper;
+
+    public PreviewFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setAspectRatio(4.0 / 3.0);
+        mLayoutChangeHelper = new LayoutChangeHelper(this);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        mBorder = findViewById(R.id.preview_border);
+    }
+
+    public void setAspectRatio(double ratio) {
+        if (ratio <= 0.0) throw new IllegalArgumentException();
+
+        if (mAspectRatio != ratio) {
+            mAspectRatio = ratio;
+            requestLayout();
+        }
+    }
+
+    public void showBorder(boolean enabled) {
+        mBorder.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    public void fadeOutBorder() {
+        Util.fadeOut(mBorder);
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        int previewWidth = MeasureSpec.getSize(widthSpec);
+        int previewHeight = MeasureSpec.getSize(heightSpec);
+
+        if (!ApiHelper.HAS_SURFACE_TEXTURE) {
+            // Get the padding of the border background.
+            int hPadding = getPaddingLeft() + getPaddingRight();
+            int vPadding = getPaddingTop() + getPaddingBottom();
+
+            // Resize the preview frame with correct aspect ratio.
+            previewWidth -= hPadding;
+            previewHeight -= vPadding;
+
+            boolean widthLonger = previewWidth > previewHeight;
+            int longSide = (widthLonger ? previewWidth : previewHeight);
+            int shortSide = (widthLonger ? previewHeight : previewWidth);
+            if (longSide > shortSide * mAspectRatio) {
+                longSide = (int) ((double) shortSide * mAspectRatio);
+            } else {
+                shortSide = (int) ((double) longSide / mAspectRatio);
+            }
+            if (widthLonger) {
+                previewWidth = longSide;
+                previewHeight = shortSide;
+            } else {
+                previewWidth = shortSide;
+                previewHeight = longSide;
+            }
+
+            // Add the padding of the border.
+            previewWidth += hPadding;
+            previewHeight += vPadding;
+        }
+
+        // Ask children to follow the new preview dimension.
+        super.onMeasure(MeasureSpec.makeMeasureSpec(previewWidth, MeasureSpec.EXACTLY),
+                MeasureSpec.makeMeasureSpec(previewHeight, MeasureSpec.EXACTLY));
+    }
+
+    public void setOnSizeChangedListener(OnSizeChangedListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        if (mListener != null) mListener.onSizeChanged(w, h);
+    }
+
+    @Override
+    public void setOnLayoutChangeListener(
+            LayoutChangeNotifier.Listener listener) {
+        mLayoutChangeHelper.setOnLayoutChangeListener(listener);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mLayoutChangeHelper.onLayout(changed, l, t, r, b);
+    }
+}
diff --git a/src/com/android/camera/PreviewGestures.java b/src/com/android/camera/PreviewGestures.java
new file mode 100644
index 0000000..466172b
--- /dev/null
+++ b/src/com/android/camera/PreviewGestures.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2013 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.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.ZoomRenderer;
+
+/* PreviewGestures disambiguates touch events received on RenderOverlay
+ * and dispatch them to the proper recipient (i.e. zoom renderer or pie renderer).
+ * Touch events on CameraControls will be handled by framework.
+ * */
+public class PreviewGestures
+        implements ScaleGestureDetector.OnScaleGestureListener {
+
+    private static final String TAG = "CAM_gestures";
+
+    private static final int MODE_NONE = 0;
+    private static final int MODE_ZOOM = 2;
+
+    public static final int DIR_UP = 0;
+    public static final int DIR_DOWN = 1;
+    public static final int DIR_LEFT = 2;
+    public static final int DIR_RIGHT = 3;
+
+    private SingleTapListener mTapListener;
+    private RenderOverlay mOverlay;
+    private PieRenderer mPie;
+    private ZoomRenderer mZoom;
+    private MotionEvent mDown;
+    private MotionEvent mCurrent;
+    private ScaleGestureDetector mScale;
+    private int mMode;
+    private boolean mZoomEnabled;
+    private boolean mEnabled;
+    private boolean mZoomOnly;
+    private GestureDetector mGestureDetector;
+
+    private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
+        @Override
+        public void onLongPress (MotionEvent e) {
+            // Open pie
+            if (!mZoomOnly && mPie != null && !mPie.showsItems()) {
+                openPie();
+            }
+        }
+
+        @Override
+        public boolean onSingleTapUp (MotionEvent e) {
+            // Tap to focus when pie is not open
+            if (mPie == null || !mPie.showsItems()) {
+                mTapListener.onSingleTapUp(null, (int) e.getX(), (int) e.getY());
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+            if (mZoomOnly || mMode == MODE_ZOOM) return false;
+            int deltaX = (int) (e1.getX() - e2.getX());
+            int deltaY = (int) (e1.getY() - e2.getY());
+            if (deltaY > 2 * deltaX && deltaY > -2 * deltaX) {
+                // Open pie on swipe up
+                if (mPie != null && !mPie.showsItems()) {
+                    openPie();
+                    return true;
+                }
+            }
+            return false;
+        }
+    };
+
+    public interface SingleTapListener {
+        public void onSingleTapUp(View v, int x, int y);
+    }
+
+    public PreviewGestures(CameraActivity ctx, SingleTapListener tapListener,
+            ZoomRenderer zoom, PieRenderer pie) {
+        mTapListener = tapListener;
+        mPie = pie;
+        mZoom = zoom;
+        mMode = MODE_NONE;
+        mScale = new ScaleGestureDetector(ctx, this);
+        mEnabled = true;
+        mGestureDetector = new GestureDetector(mGestureListener);
+    }
+
+    public void setRenderOverlay(RenderOverlay overlay) {
+        mOverlay = overlay;
+    }
+
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+    }
+
+    public void setZoomEnabled(boolean enable) {
+        mZoomEnabled = enable;
+    }
+
+    public void setZoomOnly(boolean zoom) {
+        mZoomOnly = zoom;
+    }
+
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    public boolean dispatchTouch(MotionEvent m) {
+        if (!mEnabled) {
+            return false;
+        }
+        mCurrent = m;
+        if (MotionEvent.ACTION_DOWN == m.getActionMasked()) {
+            mMode = MODE_NONE;
+            mDown = MotionEvent.obtain(m);
+        }
+
+        // If pie is open, redirects all the touch events to pie.
+        if (mPie != null && mPie.isOpen()) {
+            return sendToPie(m);
+        }
+
+        // If pie is not open, send touch events to gesture detector and scale
+        // listener to recognize the gesture.
+        mGestureDetector.onTouchEvent(m);
+        if (mZoom != null) {
+            mScale.onTouchEvent(m);
+            if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
+                mMode = MODE_ZOOM;
+                if (mZoomEnabled) {
+                    // Start showing zoom UI as soon as there is a second finger down
+                    mZoom.onScaleBegin(mScale);
+                }
+            } else if (MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
+                mZoom.onScaleEnd(mScale);
+            }
+        }
+        return true;
+    }
+
+    private MotionEvent makeCancelEvent(MotionEvent m) {
+        MotionEvent c = MotionEvent.obtain(m);
+        c.setAction(MotionEvent.ACTION_CANCEL);
+        return c;
+    }
+
+    private void openPie() {
+        mGestureDetector.onTouchEvent(makeCancelEvent(mDown));
+        mScale.onTouchEvent(makeCancelEvent(mDown));
+        mOverlay.directDispatchTouch(mDown, mPie);
+    }
+
+    private boolean sendToPie(MotionEvent m) {
+        return mOverlay.directDispatchTouch(m, mPie);
+    }
+
+    // OnScaleGestureListener implementation
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+        return mZoom.onScale(detector);
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        if (mPie == null || !mPie.isOpen()) {
+            mMode = MODE_ZOOM;
+            mGestureDetector.onTouchEvent(makeCancelEvent(mCurrent));
+            if (!mZoomEnabled) return false;
+            return mZoom.onScaleBegin(detector);
+        }
+        return false;
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+        mZoom.onScaleEnd(detector);
+    }
+}
+
diff --git a/src/com/android/camera/ProxyLauncher.java b/src/com/android/camera/ProxyLauncher.java
new file mode 100644
index 0000000..8c56621
--- /dev/null
+++ b/src/com/android/camera/ProxyLauncher.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class ProxyLauncher extends Activity {
+
+    public static final int RESULT_USER_CANCELED = -2;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState == null) {
+                Intent intent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
+                startActivityForResult(intent, 0);
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (resultCode == RESULT_CANCELED) {
+            resultCode = RESULT_USER_CANCELED;
+        }
+        setResult(resultCode, data);
+        finish();
+    }
+
+}
diff --git a/src/com/android/camera/RecordLocationPreference.java b/src/com/android/camera/RecordLocationPreference.java
new file mode 100644
index 0000000..9992afa
--- /dev/null
+++ b/src/com/android/camera/RecordLocationPreference.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.AttributeSet;
+
+/**
+ * {@code RecordLocationPreference} is used to keep the "store locaiton"
+ * option in {@code SharedPreference}.
+ */
+public class RecordLocationPreference extends IconListPreference {
+
+    public static final String VALUE_NONE = "none";
+    public static final String VALUE_ON = "on";
+    public static final String VALUE_OFF = "off";
+
+    private final ContentResolver mResolver;
+
+    public RecordLocationPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mResolver = context.getContentResolver();
+    }
+
+    @Override
+    public String getValue() {
+        return get(getSharedPreferences(), mResolver) ? VALUE_ON : VALUE_OFF;
+    }
+
+    public static boolean get(
+            SharedPreferences pref, ContentResolver resolver) {
+        String value = pref.getString(
+                CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE);
+        return VALUE_ON.equals(value);
+    }
+
+    public static boolean isSet(SharedPreferences pref) {
+        String value = pref.getString(
+                CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE);
+        return !VALUE_NONE.equals(value);
+    }
+}
diff --git a/src/com/android/camera/RotateDialogController.java b/src/com/android/camera/RotateDialogController.java
new file mode 100644
index 0000000..5d5e5e7
--- /dev/null
+++ b/src/com/android/camera/RotateDialogController.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2011 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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.camera.ui.Rotatable;
+import com.android.camera.ui.RotateLayout;
+import com.android.gallery3d.R;
+
+public class RotateDialogController implements Rotatable {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "RotateDialogController";
+    private static final long ANIM_DURATION = 150;  // millis
+
+    private Activity mActivity;
+    private int mLayoutResourceID;
+    private View mDialogRootLayout;
+    private RotateLayout mRotateDialog;
+    private View mRotateDialogTitleLayout;
+    private View mRotateDialogButtonLayout;
+    private TextView mRotateDialogTitle;
+    private ProgressBar mRotateDialogSpinner;
+    private TextView mRotateDialogText;
+    private TextView mRotateDialogButton1;
+    private TextView mRotateDialogButton2;
+
+    private Animation mFadeInAnim, mFadeOutAnim;
+
+    public RotateDialogController(Activity a, int layoutResource) {
+        mActivity = a;
+        mLayoutResourceID = layoutResource;
+    }
+
+    private void inflateDialogLayout() {
+        if (mDialogRootLayout == null) {
+            ViewGroup layoutRoot = (ViewGroup) mActivity.getWindow().getDecorView();
+            LayoutInflater inflater = mActivity.getLayoutInflater();
+            View v = inflater.inflate(mLayoutResourceID, layoutRoot);
+            mDialogRootLayout = v.findViewById(R.id.rotate_dialog_root_layout);
+            mRotateDialog = (RotateLayout) v.findViewById(R.id.rotate_dialog_layout);
+            mRotateDialogTitleLayout = v.findViewById(R.id.rotate_dialog_title_layout);
+            mRotateDialogButtonLayout = v.findViewById(R.id.rotate_dialog_button_layout);
+            mRotateDialogTitle = (TextView) v.findViewById(R.id.rotate_dialog_title);
+            mRotateDialogSpinner = (ProgressBar) v.findViewById(R.id.rotate_dialog_spinner);
+            mRotateDialogText = (TextView) v.findViewById(R.id.rotate_dialog_text);
+            mRotateDialogButton1 = (Button) v.findViewById(R.id.rotate_dialog_button1);
+            mRotateDialogButton2 = (Button) v.findViewById(R.id.rotate_dialog_button2);
+
+            mFadeInAnim = AnimationUtils.loadAnimation(
+                    mActivity, android.R.anim.fade_in);
+            mFadeOutAnim = AnimationUtils.loadAnimation(
+                    mActivity, android.R.anim.fade_out);
+            mFadeInAnim.setDuration(ANIM_DURATION);
+            mFadeOutAnim.setDuration(ANIM_DURATION);
+        }
+    }
+
+    @Override
+    public void setOrientation(int orientation, boolean animation) {
+        inflateDialogLayout();
+        mRotateDialog.setOrientation(orientation, animation);
+    }
+
+    public void resetRotateDialog() {
+        inflateDialogLayout();
+        mRotateDialogTitleLayout.setVisibility(View.GONE);
+        mRotateDialogSpinner.setVisibility(View.GONE);
+        mRotateDialogButton1.setVisibility(View.GONE);
+        mRotateDialogButton2.setVisibility(View.GONE);
+        mRotateDialogButtonLayout.setVisibility(View.GONE);
+    }
+
+    private void fadeOutDialog() {
+        mDialogRootLayout.startAnimation(mFadeOutAnim);
+        mDialogRootLayout.setVisibility(View.GONE);
+    }
+
+    private void fadeInDialog() {
+        mDialogRootLayout.startAnimation(mFadeInAnim);
+        mDialogRootLayout.setVisibility(View.VISIBLE);
+    }
+
+    public void dismissDialog() {
+        if (mDialogRootLayout != null && mDialogRootLayout.getVisibility() != View.GONE) {
+            fadeOutDialog();
+        }
+    }
+
+    public void showAlertDialog(String title, String msg, String button1Text,
+                final Runnable r1, String button2Text, final Runnable r2) {
+        resetRotateDialog();
+
+        if (title != null) {
+            mRotateDialogTitle.setText(title);
+            mRotateDialogTitleLayout.setVisibility(View.VISIBLE);
+        }
+
+        mRotateDialogText.setText(msg);
+
+        if (button1Text != null) {
+            mRotateDialogButton1.setText(button1Text);
+            mRotateDialogButton1.setContentDescription(button1Text);
+            mRotateDialogButton1.setVisibility(View.VISIBLE);
+            mRotateDialogButton1.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (r1 != null) r1.run();
+                    dismissDialog();
+                }
+            });
+            mRotateDialogButtonLayout.setVisibility(View.VISIBLE);
+        }
+        if (button2Text != null) {
+            mRotateDialogButton2.setText(button2Text);
+            mRotateDialogButton2.setContentDescription(button2Text);
+            mRotateDialogButton2.setVisibility(View.VISIBLE);
+            mRotateDialogButton2.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (r2 != null) r2.run();
+                    dismissDialog();
+                }
+            });
+            mRotateDialogButtonLayout.setVisibility(View.VISIBLE);
+        }
+
+        fadeInDialog();
+    }
+
+    public void showWaitingDialog(String msg) {
+        resetRotateDialog();
+
+        mRotateDialogText.setText(msg);
+        mRotateDialogSpinner.setVisibility(View.VISIBLE);
+
+        fadeInDialog();
+    }
+
+    public int getVisibility() {
+        if (mDialogRootLayout != null) {
+            return mDialogRootLayout.getVisibility();
+        }
+        return View.INVISIBLE;
+    }
+}
diff --git a/src/com/android/camera/SecureCameraActivity.java b/src/com/android/camera/SecureCameraActivity.java
new file mode 100644
index 0000000..2fa68f8
--- /dev/null
+++ b/src/com/android/camera/SecureCameraActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2012 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;
+
+// Use a different activity for secure camera only. So it can have a different
+// task affinity from others. This makes sure non-secure camera activity is not
+// started in secure lock screen.
+public class SecureCameraActivity extends CameraActivity {
+}
diff --git a/src/com/android/camera/ShutterButton.java b/src/com/android/camera/ShutterButton.java
new file mode 100755
index 0000000..a1bbb1a
--- /dev/null
+++ b/src/com/android/camera/ShutterButton.java
@@ -0,0 +1,130 @@
+/*
+ * 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.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+
+/**
+ * A button designed to be used for the on-screen shutter button.
+ * It's currently an {@code ImageView} that can call a delegate when the
+ * pressed state changes.
+ */
+public class ShutterButton extends ImageView {
+
+    private boolean mTouchEnabled = true;
+
+    /**
+     * A callback to be invoked when a ShutterButton's pressed state changes.
+     */
+    public interface OnShutterButtonListener {
+        /**
+         * Called when a ShutterButton has been pressed.
+         *
+         * @param pressed The ShutterButton that was pressed.
+         */
+        void onShutterButtonFocus(boolean pressed);
+        void onShutterButtonClick();
+    }
+
+    private OnShutterButtonListener mListener;
+    private boolean mOldPressed;
+
+    public ShutterButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setOnShutterButtonListener(OnShutterButtonListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mTouchEnabled) {
+            return super.dispatchTouchEvent(m);
+        } else {
+            return false;
+        }
+    }
+
+    public void enableTouch(boolean enable) {
+        mTouchEnabled = enable;
+    }
+
+    /**
+     * Hook into the drawable state changing to get changes to isPressed -- the
+     * onPressed listener doesn't always get called when the pressed state
+     * changes.
+     */
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        final boolean pressed = isPressed();
+        if (pressed != mOldPressed) {
+            if (!pressed) {
+                // When pressing the physical camera button the sequence of
+                // events is:
+                //    focus pressed, optional camera pressed, focus released.
+                // We want to emulate this sequence of events with the shutter
+                // button. When clicking using a trackball button, the view
+                // system changes the drawable state before posting click
+                // notification, so the sequence of events is:
+                //    pressed(true), optional click, pressed(false)
+                // When clicking using touch events, the view system changes the
+                // drawable state after posting click notification, so the
+                // sequence of events is:
+                //    pressed(true), pressed(false), optional click
+                // Since we're emulating the physical camera button, we want to
+                // have the same order of events. So we want the optional click
+                // callback to be delivered before the pressed(false) callback.
+                //
+                // To do this, we delay the posting of the pressed(false) event
+                // slightly by pushing it on the event queue. This moves it
+                // after the optional click notification, so our client always
+                // sees events in this sequence:
+                //     pressed(true), optional click, pressed(false)
+                post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callShutterButtonFocus(pressed);
+                    }
+                });
+            } else {
+                callShutterButtonFocus(pressed);
+            }
+            mOldPressed = pressed;
+        }
+    }
+
+    private void callShutterButtonFocus(boolean pressed) {
+        if (mListener != null) {
+            mListener.onShutterButtonFocus(pressed);
+        }
+    }
+
+    @Override
+    public boolean performClick() {
+        boolean result = super.performClick();
+        if (mListener != null && getVisibility() == View.VISIBLE) {
+            mListener.onShutterButtonClick();
+        }
+        return result;
+    }
+}
diff --git a/src/com/android/camera/SoundClips.java b/src/com/android/camera/SoundClips.java
new file mode 100644
index 0000000..8155c03
--- /dev/null
+++ b/src/com/android/camera/SoundClips.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaActionSound;
+import android.media.SoundPool;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+/*
+ * This class controls the sound playback according to the API level.
+ */
+public class SoundClips {
+    // Sound actions.
+    public static final int FOCUS_COMPLETE = 0;
+    public static final int START_VIDEO_RECORDING = 1;
+    public static final int STOP_VIDEO_RECORDING = 2;
+
+    public interface Player {
+        public void release();
+        public void play(int action);
+    }
+
+    public static Player getPlayer(Context context) {
+        if (ApiHelper.HAS_MEDIA_ACTION_SOUND) {
+            return new MediaActionSoundPlayer();
+        } else {
+            return new SoundPoolPlayer(context);
+        }
+    }
+
+    public static int getAudioTypeForSoundPool() {
+        return ApiHelper.getIntFieldIfExists(AudioManager.class,
+                "STREAM_SYSTEM_ENFORCED", null, AudioManager.STREAM_RING);
+    }
+
+    /**
+     * This class implements SoundClips.Player using MediaActionSound,
+     * which exists since API level 16.
+     */
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private static class MediaActionSoundPlayer implements Player {
+        private static final String TAG = "MediaActionSoundPlayer";
+        private MediaActionSound mSound;
+
+        @Override
+        public void release() {
+            if (mSound != null) {
+                mSound.release();
+                mSound = null;
+            }
+        }
+
+        public MediaActionSoundPlayer() {
+            mSound = new MediaActionSound();
+            mSound.load(MediaActionSound.START_VIDEO_RECORDING);
+            mSound.load(MediaActionSound.STOP_VIDEO_RECORDING);
+            mSound.load(MediaActionSound.FOCUS_COMPLETE);
+        }
+
+        @Override
+        public synchronized void play(int action) {
+            switch(action) {
+                case FOCUS_COMPLETE:
+                    mSound.play(MediaActionSound.FOCUS_COMPLETE);
+                    break;
+                case START_VIDEO_RECORDING:
+                    mSound.play(MediaActionSound.START_VIDEO_RECORDING);
+                    break;
+                case STOP_VIDEO_RECORDING:
+                    mSound.play(MediaActionSound.STOP_VIDEO_RECORDING);
+                    break;
+                default:
+                    Log.w(TAG, "Unrecognized action:" + action);
+            }
+        }
+    }
+
+    /**
+     * This class implements SoundClips.Player using SoundPool, which
+     * exists since API level 1.
+     */
+    private static class SoundPoolPlayer implements
+            Player, SoundPool.OnLoadCompleteListener {
+
+        private static final String TAG = "SoundPoolPlayer";
+        private static final int NUM_SOUND_STREAMS = 1;
+        private static final int[] SOUND_RES = { // Soundtrack res IDs.
+            R.raw.focus_complete,
+            R.raw.video_record
+        };
+
+        // ID returned by load() should be non-zero.
+        private static final int ID_NOT_LOADED = 0;
+
+        // Maps a sound action to the id;
+        private final int[] mSoundRes = {0, 1, 1};
+        // Store the context for lazy loading.
+        private Context mContext;
+        // mSoundPool is created every time load() is called and cleared every
+        // time release() is called.
+        private SoundPool mSoundPool;
+        // Sound ID of each sound resources. Given when the sound is loaded.
+        private final int[] mSoundIDs;
+        private final boolean[] mSoundIDReady;
+        private int mSoundIDToPlay;
+
+        public SoundPoolPlayer(Context context) {
+            mContext = context;
+
+            mSoundIDToPlay = ID_NOT_LOADED;
+
+            mSoundPool = new SoundPool(NUM_SOUND_STREAMS, getAudioTypeForSoundPool(), 0);
+            mSoundPool.setOnLoadCompleteListener(this);
+
+            mSoundIDs = new int[SOUND_RES.length];
+            mSoundIDReady = new boolean[SOUND_RES.length];
+            for (int i = 0; i < SOUND_RES.length; i++) {
+                mSoundIDs[i] = mSoundPool.load(mContext, SOUND_RES[i], 1);
+                mSoundIDReady[i] = false;
+            }
+        }
+
+        @Override
+        public synchronized void release() {
+            if (mSoundPool != null) {
+                mSoundPool.release();
+                mSoundPool = null;
+            }
+        }
+
+        @Override
+        public synchronized void play(int action) {
+            if (action < 0 || action >= mSoundRes.length) {
+                Log.e(TAG, "Resource ID not found for action:" + action + " in play().");
+                return;
+            }
+
+            int index = mSoundRes[action];
+            if (mSoundIDs[index] == ID_NOT_LOADED) {
+                // Not loaded yet, load first and then play when the loading is complete.
+                mSoundIDs[index] = mSoundPool.load(mContext, SOUND_RES[index], 1);
+                mSoundIDToPlay = mSoundIDs[index];
+            } else if (!mSoundIDReady[index]) {
+                // Loading and not ready yet.
+                mSoundIDToPlay = mSoundIDs[index];
+            } else {
+                mSoundPool.play(mSoundIDs[index], 1f, 1f, 0, 0, 1f);
+            }
+        }
+
+        @Override
+        public void onLoadComplete(SoundPool pool, int soundID, int status) {
+            if (status != 0) {
+                Log.e(TAG, "loading sound tracks failed (status=" + status + ")");
+                for (int i = 0; i < mSoundIDs.length; i++ ) {
+                    if (mSoundIDs[i] == soundID) {
+                        mSoundIDs[i] = ID_NOT_LOADED;
+                        break;
+                    }
+                }
+                return;
+            }
+
+            for (int i = 0; i < mSoundIDs.length; i++ ) {
+                if (mSoundIDs[i] == soundID) {
+                    mSoundIDReady[i] = true;
+                    break;
+                }
+            }
+
+            if (soundID == mSoundIDToPlay) {
+                mSoundIDToPlay = ID_NOT_LOADED;
+                mSoundPool.play(soundID, 1f, 1f, 0, 0, 1f);
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/StaticBitmapScreenNail.java b/src/com/android/camera/StaticBitmapScreenNail.java
new file mode 100644
index 0000000..10788c0
--- /dev/null
+++ b/src/com/android/camera/StaticBitmapScreenNail.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 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 com.android.gallery3d.ui.BitmapScreenNail;
+
+public class StaticBitmapScreenNail extends BitmapScreenNail {
+    public StaticBitmapScreenNail(Bitmap bitmap) {
+        super(bitmap);
+    }
+
+    @Override
+    public void recycle() {
+        // Always keep the bitmap in memory.
+    }
+}
diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java
new file mode 100644
index 0000000..ba995ed
--- /dev/null
+++ b/src/com/android/camera/Storage.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2010 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.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.StatFs;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+public class Storage {
+    private static final String TAG = "CameraStorage";
+
+    public static final String DCIM =
+            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
+
+    public static final String DIRECTORY = DCIM + "/Camera";
+
+    // Match the code in MediaProvider.computeBucketValues().
+    public static final String BUCKET_ID =
+            String.valueOf(DIRECTORY.toLowerCase().hashCode());
+
+    public static final long UNAVAILABLE = -1L;
+    public static final long PREPARING = -2L;
+    public static final long UNKNOWN_SIZE = -3L;
+    public static final long LOW_STORAGE_THRESHOLD = 50000000;
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private static void setImageSize(ContentValues values, int width, int height) {
+        // The two fields are available since ICS but got published in JB
+        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
+            values.put(MediaColumns.WIDTH, width);
+            values.put(MediaColumns.HEIGHT, height);
+        }
+    }
+
+    public static void writeFile(String path, byte[] data) {
+        FileOutputStream out = null;
+        try {
+            out = new FileOutputStream(path);
+            out.write(data);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to write data", e);
+        } finally {
+            try {
+                out.close();
+            } catch (Exception e) {
+            }
+        }
+    }
+
+    // Save the image and add it to media store.
+    public static Uri addImage(ContentResolver resolver, String title,
+            long date, Location location, int orientation, ExifInterface exif,
+            byte[] jpeg, int width, int height) {
+        // Save the image.
+        String path = generateFilepath(title);
+        if (exif != null) {
+            try {
+                exif.writeExif(jpeg, path);
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to write data", e);
+            }
+        } else {
+            writeFile(path, jpeg);
+        }
+        return addImage(resolver, title, date, location, orientation,
+                jpeg.length, path, width, height);
+    }
+
+    // Add the image to media store.
+    public static Uri addImage(ContentResolver resolver, String title,
+            long date, Location location, int orientation, int jpegLength,
+            String path, int width, int height) {
+        // Insert into MediaStore.
+        ContentValues values = new ContentValues(9);
+        values.put(ImageColumns.TITLE, title);
+        values.put(ImageColumns.DISPLAY_NAME, title + ".jpg");
+        values.put(ImageColumns.DATE_TAKEN, date);
+        values.put(ImageColumns.MIME_TYPE, "image/jpeg");
+        // Clockwise rotation in degrees. 0, 90, 180, or 270.
+        values.put(ImageColumns.ORIENTATION, orientation);
+        values.put(ImageColumns.DATA, path);
+        values.put(ImageColumns.SIZE, jpegLength);
+
+        setImageSize(values, width, height);
+
+        if (location != null) {
+            values.put(ImageColumns.LATITUDE, location.getLatitude());
+            values.put(ImageColumns.LONGITUDE, location.getLongitude());
+        }
+
+        Uri uri = null;
+        try {
+            uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
+        } catch (Throwable th)  {
+            // This can happen when the external volume is already mounted, but
+            // MediaScanner has not notify MediaProvider to add that volume.
+            // The picture is still safe and MediaScanner will find it and
+            // insert it into MediaProvider. The only problem is that the user
+            // cannot click the thumbnail to review the picture.
+            Log.e(TAG, "Failed to write MediaStore" + th);
+        }
+        return uri;
+    }
+
+    public static void deleteImage(ContentResolver resolver, Uri uri) {
+        try {
+            resolver.delete(uri, null, null);
+        } catch (Throwable th) {
+            Log.e(TAG, "Failed to delete image: " + uri);
+        }
+    }
+
+    public static String generateFilepath(String title) {
+        return DIRECTORY + '/' + title + ".jpg";
+    }
+
+    public static long getAvailableSpace() {
+        String state = Environment.getExternalStorageState();
+        Log.d(TAG, "External storage state=" + state);
+        if (Environment.MEDIA_CHECKING.equals(state)) {
+            return PREPARING;
+        }
+        if (!Environment.MEDIA_MOUNTED.equals(state)) {
+            return UNAVAILABLE;
+        }
+
+        File dir = new File(DIRECTORY);
+        dir.mkdirs();
+        if (!dir.isDirectory() || !dir.canWrite()) {
+            return UNAVAILABLE;
+        }
+
+        try {
+            StatFs stat = new StatFs(DIRECTORY);
+            return stat.getAvailableBlocks() * (long) stat.getBlockSize();
+        } catch (Exception e) {
+            Log.i(TAG, "Fail to access external storage", e);
+        }
+        return UNKNOWN_SIZE;
+    }
+
+    /**
+     * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
+     * imported. This is a temporary fix for bug#1655552.
+     */
+    public static void ensureOSXCompatible() {
+        File nnnAAAAA = new File(DCIM, "100ANDRO");
+        if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
+            Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
+        }
+    }
+}
diff --git a/src/com/android/camera/SurfaceTextureRenderer.java b/src/com/android/camera/SurfaceTextureRenderer.java
new file mode 100644
index 0000000..66f7aa2
--- /dev/null
+++ b/src/com/android/camera/SurfaceTextureRenderer.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2011 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.SurfaceTexture;
+import android.os.Handler;
+import android.util.Log;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL10;
+
+public class SurfaceTextureRenderer {
+
+    public interface FrameDrawer {
+        public void onDrawFrame(GL10 gl);
+    }
+
+    private static final String TAG = "CAM_" + SurfaceTextureRenderer.class.getSimpleName();
+    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+
+    private EGLConfig mEglConfig;
+    private EGLDisplay mEglDisplay;
+    private EGLContext mEglContext;
+    private EGLSurface mEglSurface;
+    private EGL10 mEgl;
+    private GL10 mGl;
+
+    private Handler mEglHandler;
+    private FrameDrawer mFrameDrawer;
+
+    private Object mRenderLock = new Object();
+    private Runnable mRenderTask = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (mRenderLock) {
+                mFrameDrawer.onDrawFrame(mGl);
+                mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
+                mRenderLock.notifyAll();
+            }
+        }
+    };
+
+    public class RenderThread extends Thread {
+        private Boolean mRenderStopped = false;
+
+        @Override
+        public void run() {
+            while (true) {
+                synchronized (mRenderStopped) {
+                    if (mRenderStopped) return;
+                }
+                draw(true);
+            }
+        }
+
+        public void stopRender() {
+            synchronized (mRenderStopped) {
+                mRenderStopped = true;
+            }
+        }
+    }
+
+    public SurfaceTextureRenderer(SurfaceTexture tex,
+            Handler handler, FrameDrawer renderer) {
+        mEglHandler = handler;
+        mFrameDrawer = renderer;
+
+        initialize(tex);
+    }
+
+    public RenderThread createRenderThread() {
+        return new RenderThread();
+    }
+
+    public void release() {
+        mEglHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+                mEgl.eglDestroyContext(mEglDisplay, mEglContext);
+                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE,
+                        EGL10.EGL_NO_CONTEXT);
+                mEgl.eglTerminate(mEglDisplay);
+                mEglSurface = null;
+                mEglContext = null;
+                mEglDisplay = null;
+            }
+        });
+    }
+
+    /**
+     * Posts a render request to the GL thread.
+     * @param sync      set <code>true</code> if the caller needs it to be
+     *                  a synchronous call.
+     */
+    public void draw(boolean sync) {
+        synchronized (mRenderLock) {
+            mEglHandler.post(mRenderTask);
+            if (sync) {
+                try {
+                    mRenderLock.wait();
+                } catch (InterruptedException ex) {
+                    Log.v(TAG, "RenderLock.wait() interrupted");
+                }
+            }
+        }
+    }
+
+    private void initialize(final SurfaceTexture target) {
+        mEglHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mEgl = (EGL10) EGLContext.getEGL();
+                mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+                if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+                    throw new RuntimeException("eglGetDisplay failed");
+                }
+                int[] version = new int[2];
+                if (!mEgl.eglInitialize(mEglDisplay, version)) {
+                    throw new RuntimeException("eglInitialize failed");
+                } else {
+                    Log.v(TAG, "EGL version: " + version[0] + '.' + version[1]);
+                }
+                int[] attribList = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+                mEglConfig = chooseConfig(mEgl, mEglDisplay);
+                mEglContext = mEgl.eglCreateContext(
+                        mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT, attribList);
+
+                if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
+                    throw new RuntimeException("failed to createContext");
+                }
+                mEglSurface = mEgl.eglCreateWindowSurface(
+                        mEglDisplay, mEglConfig, target, null);
+                if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+                    throw new RuntimeException("failed to createWindowSurface");
+                }
+
+                if (!mEgl.eglMakeCurrent(
+                        mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+                    throw new RuntimeException("failed to eglMakeCurrent");
+                }
+
+                mGl = (GL10) mEglContext.getGL();
+            }
+        });
+        waitDone();
+    }
+
+    private void waitDone() {
+        final Object lock = new Object();
+        synchronized (lock) {
+            mEglHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (lock) {
+                        lock.notifyAll();
+                    }
+                }
+            });
+            try {
+                lock.wait();
+            } catch (InterruptedException ex) {
+                Log.v(TAG, "waitDone() interrupted");
+            }
+        }
+    }
+
+    private static void checkEglError(String prompt, EGL10 egl) {
+        int error;
+        while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) {
+            Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error));
+        }
+    }
+
+    private static final int EGL_OPENGL_ES2_BIT = 4;
+    private static final int[] CONFIG_SPEC = new int[] {
+            EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+            EGL10.EGL_RED_SIZE, 8,
+            EGL10.EGL_GREEN_SIZE, 8,
+            EGL10.EGL_BLUE_SIZE, 8,
+            EGL10.EGL_ALPHA_SIZE, 0,
+            EGL10.EGL_DEPTH_SIZE, 0,
+            EGL10.EGL_STENCIL_SIZE, 0,
+            EGL10.EGL_NONE
+    };
+
+    private static EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+        int[] numConfig = new int[1];
+        if (!egl.eglChooseConfig(display, CONFIG_SPEC, null, 0, numConfig)) {
+            throw new IllegalArgumentException("eglChooseConfig failed");
+        }
+
+        int numConfigs = numConfig[0];
+        if (numConfigs <= 0) {
+            throw new IllegalArgumentException("No configs match configSpec");
+        }
+
+        EGLConfig[] configs = new EGLConfig[numConfigs];
+        if (!egl.eglChooseConfig(
+                display, CONFIG_SPEC, configs, numConfigs, numConfig)) {
+            throw new IllegalArgumentException("eglChooseConfig#2 failed");
+        }
+
+        return configs[0];
+    }
+}
diff --git a/src/com/android/camera/SwitchAnimManager.java b/src/com/android/camera/SwitchAnimManager.java
new file mode 100644
index 0000000..6ec8822
--- /dev/null
+++ b/src/com/android/camera/SwitchAnimManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 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.SystemClock;
+import android.util.Log;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+
+/**
+ * Class to handle the animation when switching between back and front cameras.
+ * An image of the previous camera zooms in and fades out. The preview of the
+ * new camera zooms in and fades in. The image of the previous camera is called
+ * review in this class.
+ */
+public class SwitchAnimManager {
+    private static final String TAG = "SwitchAnimManager";
+    // The amount of change for zooming in and out.
+    private static final float ZOOM_DELTA_PREVIEW = 0.2f;
+    private static final float ZOOM_DELTA_REVIEW = 0.5f;
+    private static final float ANIMATION_DURATION = 400;  // ms
+    public static final float INITIAL_DARKEN_ALPHA = 0.8f;
+
+    private long mAnimStartTime;  // milliseconds.
+    // The drawing width and height of the review image. This is saved when the
+    // texture is copied.
+    private int mReviewDrawingWidth;
+    private int mReviewDrawingHeight;
+    // The maximum width of the camera screen nail width from onDraw. We need to
+    // know how much the preview is scaled and scale the review the same amount.
+    // For example, the preview is not full screen in film strip mode.
+    private int mPreviewFrameLayoutWidth;
+
+    public SwitchAnimManager() {
+    }
+
+    public void setReviewDrawingSize(int width, int height) {
+        mReviewDrawingWidth = width;
+        mReviewDrawingHeight = height;
+    }
+
+    // width: the width of PreviewFrameLayout view.
+    // height: the height of PreviewFrameLayout view. Not used. Kept for
+    //         consistency.
+    public void setPreviewFrameLayoutSize(int width, int height) {
+        mPreviewFrameLayoutWidth = width;
+    }
+
+    // w and h: the rectangle area where the animation takes place.
+    public void startAnimation() {
+        mAnimStartTime = SystemClock.uptimeMillis();
+    }
+
+    // Returns true if the animation has been drawn.
+    // preview: camera preview view.
+    // review: snapshot of the preview before switching the camera.
+    public boolean drawAnimation(GLCanvas canvas, int x, int y, int width,
+            int height, CameraScreenNail preview, RawTexture review) {
+        long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime;
+        if (timeDiff > ANIMATION_DURATION) return false;
+        float fraction = timeDiff / ANIMATION_DURATION;
+
+        // Calculate the position and the size of the preview.
+        float centerX = x + width / 2f;
+        float centerY = y + height / 2f;
+        float previewAnimScale = 1 - ZOOM_DELTA_PREVIEW * (1 - fraction);
+        float previewWidth = width * previewAnimScale;
+        float previewHeight = height * previewAnimScale;
+        int previewX = Math.round(centerX - previewWidth / 2);
+        int previewY = Math.round(centerY - previewHeight / 2);
+
+        // Calculate the position and the size of the review.
+        float reviewAnimScale = 1 + ZOOM_DELTA_REVIEW * fraction;
+
+        // Calculate how much preview is scaled.
+        // The scaling is done by PhotoView in Gallery so we don't have the
+        // scaling information but only the width and the height passed to this
+        // method. The inference of the scale ratio is done by matching the
+        // current width and the original width we have at first when the camera
+        // layout is inflated.
+        float scaleRatio = 1;
+        if (mPreviewFrameLayoutWidth != 0) {
+            scaleRatio = (float) width / mPreviewFrameLayoutWidth;
+        } else {
+            Log.e(TAG, "mPreviewFrameLayoutWidth is 0.");
+        }
+        float reviewWidth = mReviewDrawingWidth * reviewAnimScale * scaleRatio;
+        float reviewHeight = mReviewDrawingHeight * reviewAnimScale * scaleRatio;
+        int reviewX = Math.round(centerX - reviewWidth / 2);
+        int reviewY = Math.round(centerY - reviewHeight / 2);
+
+        // Draw the preview.
+        float alpha = canvas.getAlpha();
+        canvas.setAlpha(fraction); // fade in
+        preview.directDraw(canvas, previewX, previewY, Math.round(previewWidth),
+                Math.round(previewHeight));
+
+        // Draw the review.
+        canvas.setAlpha((1f - fraction) * INITIAL_DARKEN_ALPHA); // fade out
+        review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth),
+                Math.round(reviewHeight));
+        canvas.setAlpha(alpha);
+        return true;
+    }
+
+    public boolean drawDarkPreview(GLCanvas canvas, int x, int y, int width,
+            int height, RawTexture review) {
+        // Calculate the position and the size.
+        float centerX = x + width / 2f;
+        float centerY = y + height / 2f;
+        float scaleRatio = 1;
+        if (mPreviewFrameLayoutWidth != 0) {
+            scaleRatio = (float) width / mPreviewFrameLayoutWidth;
+        } else {
+            Log.e(TAG, "mPreviewFrameLayoutWidth is 0.");
+        }
+        float reviewWidth = mReviewDrawingWidth * scaleRatio;
+        float reviewHeight = mReviewDrawingHeight * scaleRatio;
+        int reviewX = Math.round(centerX - reviewWidth / 2);
+        int reviewY = Math.round(centerY - reviewHeight / 2);
+
+        // Draw the review.
+        float alpha = canvas.getAlpha();
+        canvas.setAlpha(INITIAL_DARKEN_ALPHA);
+        review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth),
+                Math.round(reviewHeight));
+        canvas.setAlpha(alpha);
+        return true;
+    }
+
+}
diff --git a/src/com/android/camera/Thumbnail.java b/src/com/android/camera/Thumbnail.java
new file mode 100644
index 0000000..5f8483d
--- /dev/null
+++ b/src/com/android/camera/Thumbnail.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 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.media.MediaMetadataRetriever;
+
+import java.io.FileDescriptor;
+
+public class Thumbnail {
+    public static Bitmap createVideoThumbnailBitmap(FileDescriptor fd, int targetWidth) {
+        return createVideoThumbnailBitmap(null, fd, targetWidth);
+    }
+
+    public static Bitmap createVideoThumbnailBitmap(String filePath, int targetWidth) {
+        return createVideoThumbnailBitmap(filePath, null, targetWidth);
+    }
+
+    private static Bitmap createVideoThumbnailBitmap(String filePath, FileDescriptor fd,
+            int targetWidth) {
+        Bitmap bitmap = null;
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        try {
+            if (filePath != null) {
+                retriever.setDataSource(filePath);
+            } else {
+                retriever.setDataSource(fd);
+            }
+            bitmap = retriever.getFrameAtTime(-1);
+        } catch (IllegalArgumentException ex) {
+            // Assume this is a corrupt video file
+        } catch (RuntimeException ex) {
+            // Assume this is a corrupt video file.
+        } finally {
+            try {
+                retriever.release();
+            } catch (RuntimeException ex) {
+                // Ignore failures while cleaning up.
+            }
+        }
+        if (bitmap == null) return null;
+
+        // Scale down the bitmap if it is bigger than we need.
+        int width = bitmap.getWidth();
+        int height = bitmap.getHeight();
+        if (width > targetWidth) {
+            float scale = (float) targetWidth / width;
+            int w = Math.round(scale * width);
+            int h = Math.round(scale * height);
+            bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
+        }
+        return bitmap;
+    }
+}
diff --git a/src/com/android/camera/Util.java b/src/com/android/camera/Util.java
new file mode 100644
index 0000000..ccc2d90
--- /dev/null
+++ b/src/com/android/camera/Util.java
@@ -0,0 +1,804 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.admin.DevicePolicyManager;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.telephony.TelephonyManager;
+import android.util.DisplayMetrics;
+import android.util.FloatMath;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.MovieActivity;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Collection of utility functions used in this package.
+ */
+public class Util {
+    private static final String TAG = "Util";
+
+    // Orientation hysteresis amount used in rounding, in degrees
+    public static final int ORIENTATION_HYSTERESIS = 5;
+
+    public static final String REVIEW_ACTION = "com.android.camera.action.REVIEW";
+    // See android.hardware.Camera.ACTION_NEW_PICTURE.
+    public static final String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
+    // See android.hardware.Camera.ACTION_NEW_VIDEO.
+    public static final String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
+
+    // Fields from android.hardware.Camera.Parameters
+    public static final String FOCUS_MODE_CONTINUOUS_PICTURE = "continuous-picture";
+    public static final String RECORDING_HINT = "recording-hint";
+    private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
+    private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED = "auto-whitebalance-lock-supported";
+    private static final String VIDEO_SNAPSHOT_SUPPORTED = "video-snapshot-supported";
+    public static final String SCENE_MODE_HDR = "hdr";
+    public static final String TRUE = "true";
+    public static final String FALSE = "false";
+
+    public static boolean isSupported(String value, List<String> supported) {
+        return supported == null ? false : supported.indexOf(value) >= 0;
+    }
+
+    public static boolean isAutoExposureLockSupported(Parameters params) {
+        return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED));
+    }
+
+    public static boolean isAutoWhiteBalanceLockSupported(Parameters params) {
+        return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED));
+    }
+
+    public static boolean isVideoSnapshotSupported(Parameters params) {
+        return TRUE.equals(params.get(VIDEO_SNAPSHOT_SUPPORTED));
+    }
+
+    public static boolean isCameraHdrSupported(Parameters params) {
+        List<String> supported = params.getSupportedSceneModes();
+        return (supported != null) && supported.contains(SCENE_MODE_HDR);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    public static boolean isMeteringAreaSupported(Parameters params) {
+        if (ApiHelper.HAS_CAMERA_METERING_AREA) {
+            return params.getMaxNumMeteringAreas() > 0;
+        }
+        return false;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    public static boolean isFocusAreaSupported(Parameters params) {
+        if (ApiHelper.HAS_CAMERA_FOCUS_AREA) {
+            return (params.getMaxNumFocusAreas() > 0
+                    && isSupported(Parameters.FOCUS_MODE_AUTO,
+                            params.getSupportedFocusModes()));
+        }
+        return false;
+    }
+
+    // Private intent extras. Test only.
+    private static final String EXTRAS_CAMERA_FACING =
+            "android.intent.extras.CAMERA_FACING";
+
+    private static float sPixelDensity = 1;
+    private static ImageFileNamer sImageFileNamer;
+
+    private Util() {
+    }
+
+    public static void initialize(Context context) {
+        DisplayMetrics metrics = new DisplayMetrics();
+        WindowManager wm = (WindowManager)
+                context.getSystemService(Context.WINDOW_SERVICE);
+        wm.getDefaultDisplay().getMetrics(metrics);
+        sPixelDensity = metrics.density;
+        sImageFileNamer = new ImageFileNamer(
+                context.getString(R.string.image_file_name_format));
+    }
+
+    public static int dpToPixel(int dp) {
+        return Math.round(sPixelDensity * dp);
+    }
+
+    // Rotates the bitmap by the specified degree.
+    // If a new bitmap is created, the original bitmap is recycled.
+    public static Bitmap rotate(Bitmap b, int degrees) {
+        return rotateAndMirror(b, degrees, false);
+    }
+
+    // Rotates and/or mirrors the bitmap. If a new bitmap is created, the
+    // original bitmap is recycled.
+    public static Bitmap rotateAndMirror(Bitmap b, int degrees, boolean mirror) {
+        if ((degrees != 0 || mirror) && b != null) {
+            Matrix m = new Matrix();
+            // Mirror first.
+            // horizontal flip + rotation = -rotation + horizontal flip
+            if (mirror) {
+                m.postScale(-1, 1);
+                degrees = (degrees + 360) % 360;
+                if (degrees == 0 || degrees == 180) {
+                    m.postTranslate(b.getWidth(), 0);
+                } else if (degrees == 90 || degrees == 270) {
+                    m.postTranslate(b.getHeight(), 0);
+                } else {
+                    throw new IllegalArgumentException("Invalid degrees=" + degrees);
+                }
+            }
+            if (degrees != 0) {
+                // clockwise
+                m.postRotate(degrees,
+                        (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+            }
+
+            try {
+                Bitmap b2 = Bitmap.createBitmap(
+                        b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+                if (b != b2) {
+                    b.recycle();
+                    b = b2;
+                }
+            } catch (OutOfMemoryError ex) {
+                // We have no memory to rotate. Return the original bitmap.
+            }
+        }
+        return b;
+    }
+
+    /*
+     * Compute the sample size as a function of minSideLength
+     * and maxNumOfPixels.
+     * minSideLength is used to specify that minimal width or height of a
+     * bitmap.
+     * maxNumOfPixels is used to specify the maximal size in pixels that is
+     * tolerable in terms of memory usage.
+     *
+     * The function returns a sample size based on the constraints.
+     * Both size and minSideLength can be passed in as -1
+     * which indicates no care of the corresponding constraint.
+     * The functions prefers returning a sample size that
+     * generates a smaller bitmap, unless minSideLength = -1.
+     *
+     * Also, the function rounds up the sample size to a power of 2 or multiple
+     * of 8 because BitmapFactory only honors sample size this way.
+     * For example, BitmapFactory downsamples an image by 2 even though the
+     * request is 3. So we round up the sample size to avoid OOM.
+     */
+    public static int computeSampleSize(BitmapFactory.Options options,
+            int minSideLength, int maxNumOfPixels) {
+        int initialSize = computeInitialSampleSize(options, minSideLength,
+                maxNumOfPixels);
+
+        int roundedSize;
+        if (initialSize <= 8) {
+            roundedSize = 1;
+            while (roundedSize < initialSize) {
+                roundedSize <<= 1;
+            }
+        } else {
+            roundedSize = (initialSize + 7) / 8 * 8;
+        }
+
+        return roundedSize;
+    }
+
+    private static int computeInitialSampleSize(BitmapFactory.Options options,
+            int minSideLength, int maxNumOfPixels) {
+        double w = options.outWidth;
+        double h = options.outHeight;
+
+        int lowerBound = (maxNumOfPixels < 0) ? 1 :
+                (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
+        int upperBound = (minSideLength < 0) ? 128 :
+                (int) Math.min(Math.floor(w / minSideLength),
+                Math.floor(h / minSideLength));
+
+        if (upperBound < lowerBound) {
+            // return the larger one when there is no overlapping zone.
+            return lowerBound;
+        }
+
+        if (maxNumOfPixels < 0 && minSideLength < 0) {
+            return 1;
+        } else if (minSideLength < 0) {
+            return lowerBound;
+        } else {
+            return upperBound;
+        }
+    }
+
+    public static Bitmap makeBitmap(byte[] jpegData, int maxNumOfPixels) {
+        try {
+            BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inJustDecodeBounds = true;
+            BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
+                    options);
+            if (options.mCancel || options.outWidth == -1
+                    || options.outHeight == -1) {
+                return null;
+            }
+            options.inSampleSize = computeSampleSize(
+                    options, -1, maxNumOfPixels);
+            options.inJustDecodeBounds = false;
+
+            options.inDither = false;
+            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+            return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
+                    options);
+        } catch (OutOfMemoryError ex) {
+            Log.e(TAG, "Got oom exception ", ex);
+            return null;
+        }
+    }
+
+    public static void closeSilently(Closeable c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (Throwable t) {
+            // do nothing
+        }
+    }
+
+    public static void Assert(boolean cond) {
+        if (!cond) {
+            throw new AssertionError();
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private static void throwIfCameraDisabled(Activity activity) throws CameraDisabledException {
+        // Check if device policy has disabled the camera.
+        if (ApiHelper.HAS_GET_CAMERA_DISABLED) {
+            DevicePolicyManager dpm = (DevicePolicyManager) activity.getSystemService(
+                    Context.DEVICE_POLICY_SERVICE);
+            if (dpm.getCameraDisabled(null)) {
+                throw new CameraDisabledException();
+            }
+        }
+    }
+
+    public static CameraManager.CameraProxy openCamera(
+            Activity activity, int cameraId)
+            throws CameraHardwareException, CameraDisabledException {
+        throwIfCameraDisabled(activity);
+
+        try {
+            return CameraHolder.instance().open(cameraId);
+        } catch (CameraHardwareException e) {
+            // In eng build, we throw the exception so that test tool
+            // can detect it and report it
+            if ("eng".equals(Build.TYPE)) {
+                throw new RuntimeException("openCamera failed", e);
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    public static void showErrorAndFinish(final Activity activity, int msgId) {
+        DialogInterface.OnClickListener buttonListener =
+                new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                activity.finish();
+            }
+        };
+        TypedValue out = new TypedValue();
+        activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, out, true);
+        new AlertDialog.Builder(activity)
+                .setCancelable(false)
+                .setTitle(R.string.camera_error_title)
+                .setMessage(msgId)
+                .setNeutralButton(R.string.dialog_ok, buttonListener)
+                .setIcon(out.resourceId)
+                .show();
+    }
+
+    public static <T> T checkNotNull(T object) {
+        if (object == null) throw new NullPointerException();
+        return object;
+    }
+
+    public static boolean equals(Object a, Object b) {
+        return (a == b) || (a == null ? false : a.equals(b));
+    }
+
+    public static int nextPowerOf2(int n) {
+        n -= 1;
+        n |= n >>> 16;
+        n |= n >>> 8;
+        n |= n >>> 4;
+        n |= n >>> 2;
+        n |= n >>> 1;
+        return n + 1;
+    }
+
+    public static float distance(float x, float y, float sx, float sy) {
+        float dx = x - sx;
+        float dy = y - sy;
+        return FloatMath.sqrt(dx * dx + dy * dy);
+    }
+
+    public static int clamp(int x, int min, int max) {
+        if (x > max) return max;
+        if (x < min) return min;
+        return x;
+    }
+
+    public static int getDisplayRotation(Activity activity) {
+        int rotation = activity.getWindowManager().getDefaultDisplay()
+                .getRotation();
+        switch (rotation) {
+            case Surface.ROTATION_0: return 0;
+            case Surface.ROTATION_90: return 90;
+            case Surface.ROTATION_180: return 180;
+            case Surface.ROTATION_270: return 270;
+        }
+        return 0;
+    }
+
+    public static int getDisplayOrientation(int degrees, int cameraId) {
+        // See android.hardware.Camera.setDisplayOrientation for
+        // documentation.
+        Camera.CameraInfo info = new Camera.CameraInfo();
+        Camera.getCameraInfo(cameraId, info);
+        int result;
+        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+            result = (info.orientation + degrees) % 360;
+            result = (360 - result) % 360;  // compensate the mirror
+        } else {  // back-facing
+            result = (info.orientation - degrees + 360) % 360;
+        }
+        return result;
+    }
+
+    public static int getCameraOrientation(int cameraId) {
+        Camera.CameraInfo info = new Camera.CameraInfo();
+        Camera.getCameraInfo(cameraId, info);
+        return info.orientation;
+    }
+
+    public static int roundOrientation(int orientation, int orientationHistory) {
+        boolean changeOrientation = false;
+        if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) {
+            changeOrientation = true;
+        } else {
+            int dist = Math.abs(orientation - orientationHistory);
+            dist = Math.min( dist, 360 - dist );
+            changeOrientation = ( dist >= 45 + ORIENTATION_HYSTERESIS );
+        }
+        if (changeOrientation) {
+            return ((orientation + 45) / 90 * 90) % 360;
+        }
+        return orientationHistory;
+    }
+
+    @SuppressWarnings("deprecation")
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
+    private static Point getDefaultDisplaySize(Activity activity, Point size) {
+        Display d = activity.getWindowManager().getDefaultDisplay();
+        if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) {
+            d.getSize(size);
+        } else {
+            size.set(d.getWidth(), d.getHeight());
+        }
+        return size;
+    }
+
+    public static Size getOptimalPreviewSize(Activity currentActivity,
+            List<Size> sizes, double targetRatio) {
+        // Use a very small tolerance because we want an exact match.
+        final double ASPECT_TOLERANCE = 0.001;
+        if (sizes == null) return null;
+
+        Size optimalSize = null;
+        double minDiff = Double.MAX_VALUE;
+
+        // Because of bugs of overlay and layout, we sometimes will try to
+        // layout the viewfinder in the portrait orientation and thus get the
+        // wrong size of preview surface. When we change the preview size, the
+        // new overlay will be created before the old one closed, which causes
+        // an exception. For now, just get the screen size.
+        Point point = getDefaultDisplaySize(currentActivity, new Point());
+        int targetHeight = Math.min(point.x, point.y);
+        // Try to find an size match aspect ratio and size
+        for (Size size : sizes) {
+            double ratio = (double) size.width / size.height;
+            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
+            if (Math.abs(size.height - targetHeight) < minDiff) {
+                optimalSize = size;
+                minDiff = Math.abs(size.height - targetHeight);
+            }
+        }
+        // Cannot find the one match the aspect ratio. This should not happen.
+        // Ignore the requirement.
+        if (optimalSize == null) {
+            Log.w(TAG, "No preview size match the aspect ratio");
+            minDiff = Double.MAX_VALUE;
+            for (Size size : sizes) {
+                if (Math.abs(size.height - targetHeight) < minDiff) {
+                    optimalSize = size;
+                    minDiff = Math.abs(size.height - targetHeight);
+                }
+            }
+        }
+        return optimalSize;
+    }
+
+    // Returns the largest picture size which matches the given aspect ratio.
+    public static Size getOptimalVideoSnapshotPictureSize(
+            List<Size> sizes, double targetRatio) {
+        // Use a very small tolerance because we want an exact match.
+        final double ASPECT_TOLERANCE = 0.001;
+        if (sizes == null) return null;
+
+        Size optimalSize = null;
+
+        // Try to find a size matches aspect ratio and has the largest width
+        for (Size size : sizes) {
+            double ratio = (double) size.width / size.height;
+            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
+            if (optimalSize == null || size.width > optimalSize.width) {
+                optimalSize = size;
+            }
+        }
+
+        // Cannot find one that matches the aspect ratio. This should not happen.
+        // Ignore the requirement.
+        if (optimalSize == null) {
+            Log.w(TAG, "No picture size match the aspect ratio");
+            for (Size size : sizes) {
+                if (optimalSize == null || size.width > optimalSize.width) {
+                    optimalSize = size;
+                }
+            }
+        }
+        return optimalSize;
+    }
+
+    public static void dumpParameters(Parameters parameters) {
+        String flattened = parameters.flatten();
+        StringTokenizer tokenizer = new StringTokenizer(flattened, ";");
+        Log.d(TAG, "Dump all camera parameters:");
+        while (tokenizer.hasMoreElements()) {
+            Log.d(TAG, tokenizer.nextToken());
+        }
+    }
+
+    /**
+     * Returns whether the device is voice-capable (meaning, it can do MMS).
+     */
+    public static boolean isMmsCapable(Context context) {
+        TelephonyManager telephonyManager = (TelephonyManager)
+                context.getSystemService(Context.TELEPHONY_SERVICE);
+        if (telephonyManager == null) {
+            return false;
+        }
+
+        try {
+            Class<?> partypes[] = new Class[0];
+            Method sIsVoiceCapable = TelephonyManager.class.getMethod(
+                    "isVoiceCapable", partypes);
+
+            Object arglist[] = new Object[0];
+            Object retobj = sIsVoiceCapable.invoke(telephonyManager, arglist);
+            return (Boolean) retobj;
+        } catch (java.lang.reflect.InvocationTargetException ite) {
+            // Failure, must be another device.
+            // Assume that it is voice capable.
+        } catch (IllegalAccessException iae) {
+            // Failure, must be an other device.
+            // Assume that it is voice capable.
+        } catch (NoSuchMethodException nsme) {
+        }
+        return true;
+    }
+
+    // This is for test only. Allow the camera to launch the specific camera.
+    public static int getCameraFacingIntentExtras(Activity currentActivity) {
+        int cameraId = -1;
+
+        int intentCameraId =
+                currentActivity.getIntent().getIntExtra(Util.EXTRAS_CAMERA_FACING, -1);
+
+        if (isFrontCameraIntent(intentCameraId)) {
+            // Check if the front camera exist
+            int frontCameraId = CameraHolder.instance().getFrontCameraId();
+            if (frontCameraId != -1) {
+                cameraId = frontCameraId;
+            }
+        } else if (isBackCameraIntent(intentCameraId)) {
+            // Check if the back camera exist
+            int backCameraId = CameraHolder.instance().getBackCameraId();
+            if (backCameraId != -1) {
+                cameraId = backCameraId;
+            }
+        }
+        return cameraId;
+    }
+
+    private static boolean isFrontCameraIntent(int intentCameraId) {
+        return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
+    }
+
+    private static boolean isBackCameraIntent(int intentCameraId) {
+        return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK);
+    }
+
+    private static int sLocation[] = new int[2];
+
+    // This method is not thread-safe.
+    public static boolean pointInView(float x, float y, View v) {
+        v.getLocationInWindow(sLocation);
+        return x >= sLocation[0] && x < (sLocation[0] + v.getWidth())
+                && y >= sLocation[1] && y < (sLocation[1] + v.getHeight());
+    }
+
+    public static int[] getRelativeLocation(View reference, View view) {
+        reference.getLocationInWindow(sLocation);
+        int referenceX = sLocation[0];
+        int referenceY = sLocation[1];
+        view.getLocationInWindow(sLocation);
+        sLocation[0] -= referenceX;
+        sLocation[1] -= referenceY;
+        return sLocation;
+    }
+
+    public static boolean isUriValid(Uri uri, ContentResolver resolver) {
+        if (uri == null) return false;
+
+        try {
+            ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
+            if (pfd == null) {
+                Log.e(TAG, "Fail to open URI. URI=" + uri);
+                return false;
+            }
+            pfd.close();
+        } catch (IOException ex) {
+            return false;
+        }
+        return true;
+    }
+
+    public static void viewUri(Uri uri, Context context) {
+        if (!isUriValid(uri, context.getContentResolver())) {
+            Log.e(TAG, "Uri invalid. uri=" + uri);
+            return;
+        }
+
+        try {
+            context.startActivity(new Intent(Util.REVIEW_ACTION, uri));
+        } catch (ActivityNotFoundException ex) {
+            try {
+                context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+            } catch (ActivityNotFoundException e) {
+                Log.e(TAG, "review image fail. uri=" + uri, e);
+            }
+        }
+    }
+
+    public static void dumpRect(RectF rect, String msg) {
+        Log.v(TAG, msg + "=(" + rect.left + "," + rect.top
+                + "," + rect.right + "," + rect.bottom + ")");
+    }
+
+    public static void rectFToRect(RectF rectF, Rect rect) {
+        rect.left = Math.round(rectF.left);
+        rect.top = Math.round(rectF.top);
+        rect.right = Math.round(rectF.right);
+        rect.bottom = Math.round(rectF.bottom);
+    }
+
+    public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation,
+            int viewWidth, int viewHeight) {
+        // Need mirror for front camera.
+        matrix.setScale(mirror ? -1 : 1, 1);
+        // This is the value for android.hardware.Camera.setDisplayOrientation.
+        matrix.postRotate(displayOrientation);
+        // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
+        // UI coordinates range from (0, 0) to (width, height).
+        matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);
+        matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
+    }
+
+    public static String createJpegName(long dateTaken) {
+        synchronized (sImageFileNamer) {
+            return sImageFileNamer.generateName(dateTaken);
+        }
+    }
+
+    public static void broadcastNewPicture(Context context, Uri uri) {
+        context.sendBroadcast(new Intent(ACTION_NEW_PICTURE, uri));
+        // Keep compatibility
+        context.sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", uri));
+    }
+
+    public static void fadeIn(View view, float startAlpha, float endAlpha, long duration) {
+        if (view.getVisibility() == View.VISIBLE) return;
+
+        view.setVisibility(View.VISIBLE);
+        Animation animation = new AlphaAnimation(startAlpha, endAlpha);
+        animation.setDuration(duration);
+        view.startAnimation(animation);
+    }
+
+    public static void fadeIn(View view) {
+        fadeIn(view, 0F, 1F, 400);
+
+        // We disabled the button in fadeOut(), so enable it here.
+        view.setEnabled(true);
+    }
+
+    public static void fadeOut(View view) {
+        if (view.getVisibility() != View.VISIBLE) return;
+
+        // Since the button is still clickable before fade-out animation
+        // ends, we disable the button first to block click.
+        view.setEnabled(false);
+        Animation animation = new AlphaAnimation(1F, 0F);
+        animation.setDuration(400);
+        view.startAnimation(animation);
+        view.setVisibility(View.GONE);
+    }
+
+    public static int getJpegRotation(int cameraId, int orientation) {
+        // See android.hardware.Camera.Parameters.setRotation for
+        // documentation.
+        int rotation = 0;
+        if (orientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[cameraId];
+            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
+                rotation = (info.orientation - orientation + 360) % 360;
+            } else {  // back-facing camera
+                rotation = (info.orientation + orientation) % 360;
+            }
+        }
+        return rotation;
+    }
+
+    public static void setGpsParameters(Parameters parameters, Location loc) {
+        // Clear previous GPS location from the parameters.
+        parameters.removeGpsData();
+
+        // We always encode GpsTimeStamp
+        parameters.setGpsTimestamp(System.currentTimeMillis() / 1000);
+
+        // Set GPS location.
+        if (loc != null) {
+            double lat = loc.getLatitude();
+            double lon = loc.getLongitude();
+            boolean hasLatLon = (lat != 0.0d) || (lon != 0.0d);
+
+            if (hasLatLon) {
+                Log.d(TAG, "Set gps location");
+                parameters.setGpsLatitude(lat);
+                parameters.setGpsLongitude(lon);
+                parameters.setGpsProcessingMethod(loc.getProvider().toUpperCase());
+                if (loc.hasAltitude()) {
+                    parameters.setGpsAltitude(loc.getAltitude());
+                } else {
+                    // for NETWORK_PROVIDER location provider, we may have
+                    // no altitude information, but the driver needs it, so
+                    // we fake one.
+                    parameters.setGpsAltitude(0);
+                }
+                if (loc.getTime() != 0) {
+                    // Location.getTime() is UTC in milliseconds.
+                    // gps-timestamp is UTC in seconds.
+                    long utcTimeSeconds = loc.getTime() / 1000;
+                    parameters.setGpsTimestamp(utcTimeSeconds);
+                }
+            } else {
+                loc = null;
+            }
+        }
+    }
+
+
+    public static int[] getMaxPreviewFpsRange(Parameters params) {
+        List<int[]> frameRates = params.getSupportedPreviewFpsRange();
+        if (frameRates != null && frameRates.size() > 0) {
+            // The list is sorted. Return the last element.
+            return frameRates.get(frameRates.size() - 1);
+        }
+        return new int[0];
+    }
+
+    private static class ImageFileNamer {
+        private SimpleDateFormat mFormat;
+
+        // The date (in milliseconds) used to generate the last name.
+        private long mLastDate;
+
+        // Number of names generated for the same second.
+        private int mSameSecondCount;
+
+        public ImageFileNamer(String format) {
+            mFormat = new SimpleDateFormat(format);
+        }
+
+        public String generateName(long dateTaken) {
+            Date date = new Date(dateTaken);
+            String result = mFormat.format(date);
+
+            // If the last name was generated for the same second,
+            // we append _1, _2, etc to the name.
+            if (dateTaken / 1000 == mLastDate / 1000) {
+                mSameSecondCount++;
+                result += "_" + mSameSecondCount;
+            } else {
+                mLastDate = dateTaken;
+                mSameSecondCount = 0;
+            }
+
+            return result;
+        }
+    }
+
+    public static void playVideo(Context context, Uri uri, String title) {
+        try {
+            Intent intent = new Intent(Intent.ACTION_VIEW)
+                    .setDataAndType(uri, "video/*")
+                    .putExtra(Intent.EXTRA_TITLE, title)
+                    .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
+            context.startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(context, context.getString(R.string.video_err),
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+}
diff --git a/src/com/android/camera/VideoController.java b/src/com/android/camera/VideoController.java
new file mode 100644
index 0000000..e846548
--- /dev/null
+++ b/src/com/android/camera/VideoController.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013 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.view.View;
+
+import com.android.camera.ShutterButton.OnShutterButtonListener;
+
+public interface VideoController extends OnShutterButtonListener {
+
+    public void onReviewDoneClicked(View view);
+    public void onReviewCancelClicked(View viwe);
+    public void onReviewPlayClicked(View view);
+
+    public boolean isVideoCaptureIntent();
+    public boolean isInReviewMode();
+    public int onZoomChanged(int index);
+
+    public void onSingleTapUp(View view, int x, int y);
+
+    public void stopPreview();
+
+    public void updateCameraOrientation();
+
+    // Callbacks for camera preview UI events.
+    public void onPreviewUIReady();
+    public void onPreviewUIDestroyed();
+}
diff --git a/src/com/android/camera/VideoMenu.java b/src/com/android/camera/VideoMenu.java
new file mode 100644
index 0000000..da0bde1
--- /dev/null
+++ b/src/com/android/camera/VideoMenu.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2012 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.Context;
+import android.view.LayoutInflater;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.ListPrefSettingPopup;
+import com.android.camera.ui.MoreSettingPopup;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.TimeIntervalPopup;
+import com.android.gallery3d.R;
+
+public class VideoMenu extends PieController
+        implements MoreSettingPopup.Listener,
+        ListPrefSettingPopup.Listener,
+        TimeIntervalPopup.Listener {
+
+    private static String TAG = "CAM_VideoMenu";
+
+    private VideoUI mUI;
+    private String[] mOtherKeys;
+    private AbstractSettingPopup mPopup;
+
+    private static final int POPUP_NONE = 0;
+    private static final int POPUP_FIRST_LEVEL = 1;
+    private static final int POPUP_SECOND_LEVEL = 2;
+    private int mPopupStatus;
+    private CameraActivity mActivity;
+
+    public VideoMenu(CameraActivity activity, VideoUI ui, PieRenderer pie) {
+        super(activity, pie);
+        mUI = ui;
+        mActivity = activity;
+    }
+
+
+    public void initialize(PreferenceGroup group) {
+        super.initialize(group);
+        mPopup = null;
+        mPopupStatus = POPUP_NONE;
+        PieItem item = null;
+        // white balance
+        if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+            item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+            mRenderer.addItem(item);
+        }
+        // settings popup
+        mOtherKeys = new String[] {
+                CameraSettings.KEY_VIDEO_EFFECT,
+                CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
+                CameraSettings.KEY_VIDEO_QUALITY,
+                CameraSettings.KEY_RECORD_LOCATION
+        };
+        item = makeItem(R.drawable.ic_settings_holo_light);
+        item.setLabel(mActivity.getResources().getString(R.string.camera_menu_settings_label));
+        item.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(PieItem item) {
+                if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) {
+                    initializePopup();
+                    mPopupStatus = POPUP_FIRST_LEVEL;
+                }
+                mUI.showPopup(mPopup);
+            }
+        });
+        mRenderer.addItem(item);
+        // camera switcher
+        if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+            item = makeItem(R.drawable.ic_switch_back);
+            IconListPreference lpref = (IconListPreference) group.findPreference(
+                    CameraSettings.KEY_CAMERA_ID);
+            item.setLabel(lpref.getLabel());
+            item.setImageResource(mActivity,
+                    ((IconListPreference) lpref).getIconIds()
+                    [lpref.findIndexOfValue(lpref.getValue())]);
+
+            final PieItem fitem = item;
+            item.setOnClickListener(new OnClickListener() {
+
+                @Override
+                public void onClick(PieItem item) {
+                    // Find the index of next camera.
+                    ListPreference pref =
+                            mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+                    if (pref != null) {
+                        int index = pref.findIndexOfValue(pref.getValue());
+                        CharSequence[] values = pref.getEntryValues();
+                        index = (index + 1) % values.length;
+                        int newCameraId = Integer.parseInt((String) values[index]);
+                        fitem.setImageResource(mActivity,
+                                ((IconListPreference) pref).getIconIds()[index]);
+                        fitem.setLabel(pref.getLabel());
+                        mListener.onCameraPickerClicked(newCameraId);
+                    }
+                }
+            });
+            mRenderer.addItem(item);
+        }
+        // flash
+        if (group.findPreference(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE) != null) {
+            item = makeItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE);
+            mRenderer.addItem(item);
+        }
+    }
+
+    @Override
+    public void reloadPreferences() {
+        super.reloadPreferences();
+        if (mPopup != null) {
+            mPopup.reloadPreference();
+        }
+    }
+
+    @Override
+    public void overrideSettings(final String ... keyvalues) {
+        super.overrideSettings(keyvalues);
+        if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) {
+            mPopupStatus = POPUP_FIRST_LEVEL;
+            initializePopup();
+        }
+        ((MoreSettingPopup) mPopup).overrideSettings(keyvalues);
+    }
+
+    @Override
+    // Hit when an item in the second-level popup gets selected
+    public void onListPrefChanged(ListPreference pref) {
+        if (mPopup != null) {
+            if (mPopupStatus == POPUP_SECOND_LEVEL) {
+                mUI.dismissPopup(true);
+            }
+        }
+        super.onSettingChanged(pref);
+    }
+
+    protected void initializePopup() {
+        LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        MoreSettingPopup popup = (MoreSettingPopup) inflater.inflate(
+                R.layout.more_setting_popup, null, false);
+        popup.setSettingChangedListener(this);
+        popup.initialize(mPreferenceGroup, mOtherKeys);
+        if (mActivity.isSecureCamera()) {
+            // Prevent location preference from getting changed in secure camera mode
+            popup.setPreferenceEnabled(CameraSettings.KEY_RECORD_LOCATION, false);
+        }
+        mPopup = popup;
+    }
+
+    public void popupDismissed(boolean topPopupOnly) {
+        // if the 2nd level popup gets dismissed
+        if (mPopupStatus == POPUP_SECOND_LEVEL) {
+            initializePopup();
+            mPopupStatus = POPUP_FIRST_LEVEL;
+            if (topPopupOnly) mUI.showPopup(mPopup);
+        }
+    }
+
+    @Override
+    // Hit when an item in the first-level popup gets selected, then bring up
+    // the second-level popup
+    public void onPreferenceClicked(ListPreference pref) {
+        if (mPopupStatus != POPUP_FIRST_LEVEL) return;
+
+        LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        if (CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL.equals(pref.getKey())) {
+            TimeIntervalPopup timeInterval = (TimeIntervalPopup) inflater.inflate(
+                    R.layout.time_interval_popup, null, false);
+            timeInterval.initialize((IconListPreference) pref);
+            timeInterval.setSettingChangedListener(this);
+            mUI.dismissPopup(true);
+            mPopup = timeInterval;
+        } else {
+            ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate(
+                    R.layout.list_pref_setting_popup, null, false);
+            basic.initialize(pref);
+            basic.setSettingChangedListener(this);
+            mUI.dismissPopup(true);
+            mPopup = basic;
+        }
+        mUI.showPopup(mPopup);
+        mPopupStatus = POPUP_SECOND_LEVEL;
+    }
+}
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
new file mode 100644
index 0000000..956890e
--- /dev/null
+++ b/src/com/android/camera/VideoModule.java
@@ -0,0 +1,2233 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.location.Location;
+import android.media.CamcorderProfile;
+import android.media.CameraProfile;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.camera.CameraManager.CameraPictureCallback;
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.RotateTextToast;
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.OrientationManager;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.util.AccessibilityUtils;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+public class VideoModule implements CameraModule,
+    VideoController,
+    CameraPreference.OnPreferenceChangedListener,
+    ShutterButton.OnShutterButtonListener,
+    MediaRecorder.OnErrorListener,
+    MediaRecorder.OnInfoListener,
+    EffectsRecorder.EffectsListener {
+
+    private static final String TAG = "CAM_VideoModule";
+
+    // We number the request code from 1000 to avoid collision with Gallery.
+    private static final int REQUEST_EFFECT_BACKDROPPER = 1000;
+
+    private static final int CHECK_DISPLAY_ROTATION = 3;
+    private static final int CLEAR_SCREEN_DELAY = 4;
+    private static final int UPDATE_RECORD_TIME = 5;
+    private static final int ENABLE_SHUTTER_BUTTON = 6;
+    private static final int SHOW_TAP_TO_SNAPSHOT_TOAST = 7;
+    private static final int SWITCH_CAMERA = 8;
+    private static final int SWITCH_CAMERA_START_ANIMATION = 9;
+    private static final int HIDE_SURFACE_VIEW = 10;
+    private static final int CAPTURE_ANIMATION_DONE = 11;
+
+    private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+    private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms
+
+    /**
+     * An unpublished intent flag requesting to start recording straight away
+     * and return as soon as recording is stopped.
+     * TODO: consider publishing by moving into MediaStore.
+     */
+    private static final String EXTRA_QUICK_CAPTURE =
+            "android.intent.extra.quickCapture";
+
+    private static final int MIN_THUMB_SIZE = 64;
+    // module fields
+    private CameraActivity mActivity;
+    private boolean mPaused;
+    private int mCameraId;
+    private Parameters mParameters;
+
+    private boolean mIsInReviewMode;
+    private boolean mSnapshotInProgress = false;
+
+    private static final String EFFECT_BG_FROM_GALLERY = "gallery";
+
+    private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
+
+    private ComboPreferences mPreferences;
+    private PreferenceGroup mPreferenceGroup;
+    // Preference must be read before starting preview. We check this before starting
+    // preview.
+    private boolean mPreferenceRead;
+
+    private boolean mIsVideoCaptureIntent;
+    private boolean mQuickCapture;
+
+    private MediaRecorder mMediaRecorder;
+    private EffectsRecorder mEffectsRecorder;
+    private boolean mEffectsDisplayResult;
+
+    private int mEffectType = EffectsRecorder.EFFECT_NONE;
+    private Object mEffectParameter = null;
+    private String mEffectUriFromGallery = null;
+    private String mPrefVideoEffectDefault;
+    private boolean mResetEffect = true;
+
+    private boolean mSwitchingCamera;
+    private boolean mMediaRecorderRecording = false;
+    private long mRecordingStartTime;
+    private boolean mRecordingTimeCountsDown = false;
+    private long mOnResumeTime;
+    // The video file that the hardware camera is about to record into
+    // (or is recording into.)
+    private String mVideoFilename;
+    private ParcelFileDescriptor mVideoFileDescriptor;
+
+    // The video file that has already been recorded, and that is being
+    // examined by the user.
+    private String mCurrentVideoFilename;
+    private Uri mCurrentVideoUri;
+    private ContentValues mCurrentVideoValues;
+
+    private CamcorderProfile mProfile;
+
+    // The video duration limit. 0 menas no limit.
+    private int mMaxVideoDurationInMs;
+
+    // Time Lapse parameters.
+    private boolean mCaptureTimeLapse = false;
+    // Default 0. If it is larger than 0, the camcorder is in time lapse mode.
+    private int mTimeBetweenTimeLapseFrameCaptureMs = 0;
+
+    boolean mPreviewing = false; // True if preview is started.
+    // The display rotation in degrees. This is only valid when mPreviewing is
+    // true.
+    private int mDisplayRotation;
+    private int mCameraDisplayOrientation;
+
+    private int mDesiredPreviewWidth;
+    private int mDesiredPreviewHeight;
+    private ContentResolver mContentResolver;
+
+    private LocationManager mLocationManager;
+    private OrientationManager mOrientationManager;
+
+    private Surface mSurface;
+    private int mPendingSwitchCameraId;
+    private boolean mOpenCameraFail;
+    private boolean mCameraDisabled;
+    private final Handler mHandler = new MainHandler();
+    private VideoUI mUI;
+    private CameraProxy mCameraDevice;
+
+    // The degrees of the device rotated clockwise from its natural orientation.
+    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+
+    private int mZoomValue;  // The current zoom value.
+
+    private boolean mRestoreFlash;  // This is used to check if we need to restore the flash
+                                    // status when going back from gallery.
+
+    private final MediaSaveService.OnMediaSavedListener mOnVideoSavedListener =
+            new MediaSaveService.OnMediaSavedListener() {
+                @Override
+                public void onMediaSaved(Uri uri) {
+                    if (uri != null) {
+                        mActivity.sendBroadcast(
+                                new Intent(Util.ACTION_NEW_VIDEO, uri));
+                        Util.broadcastNewPicture(mActivity, uri);
+                    }
+                }
+            };
+
+    private final MediaSaveService.OnMediaSavedListener mOnPhotoSavedListener =
+            new MediaSaveService.OnMediaSavedListener() {
+                @Override
+                public void onMediaSaved(Uri uri) {
+                    if (uri != null) {
+                        Util.broadcastNewPicture(mActivity, uri);
+                    }
+                }
+            };
+
+
+    protected class CameraOpenThread extends Thread {
+        @Override
+        public void run() {
+            openCamera();
+        }
+    }
+
+    private void openCamera() {
+        try {
+            if (mCameraDevice == null) {
+                mCameraDevice = Util.openCamera(mActivity, mCameraId);
+            }
+            mParameters = mCameraDevice.getParameters();
+        } catch (CameraHardwareException e) {
+            mOpenCameraFail = true;
+        } catch (CameraDisabledException e) {
+            mCameraDisabled = true;
+        }
+    }
+
+    // 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 ENABLE_SHUTTER_BUTTON:
+                    mUI.enableShutter(true);
+                    break;
+
+                case CLEAR_SCREEN_DELAY: {
+                    mActivity.getWindow().clearFlags(
+                            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                    break;
+                }
+
+                case UPDATE_RECORD_TIME: {
+                    updateRecordingTime();
+                    break;
+                }
+
+                case CHECK_DISPLAY_ROTATION: {
+                    // Restart the preview if display rotation has changed.
+                    // Sometimes this happens when the device is held upside
+                    // down and camera app is opened. Rotation animation will
+                    // take some time and the rotation value we have got may be
+                    // wrong. Framework does not have a callback for this now.
+                    if ((Util.getDisplayRotation(mActivity) != mDisplayRotation)
+                            && !mMediaRecorderRecording && !mSwitchingCamera) {
+                        startPreview();
+                    }
+                    if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) {
+                        mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+                    }
+                    break;
+                }
+
+                case SHOW_TAP_TO_SNAPSHOT_TOAST: {
+                    showTapToSnapshotToast();
+                    break;
+                }
+
+                case SWITCH_CAMERA: {
+                    switchCamera();
+                    break;
+                }
+
+                case SWITCH_CAMERA_START_ANIMATION: {
+                    //TODO:
+                    //((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+
+                    // Enable all camera controls.
+                    mSwitchingCamera = false;
+                    break;
+                }
+
+                case CAPTURE_ANIMATION_DONE: {
+                    mUI.enablePreviewThumb(false);
+                    break;
+                }
+
+                default:
+                    Log.v(TAG, "Unhandled message: " + msg.what);
+                    break;
+            }
+        }
+    }
+
+    private BroadcastReceiver mReceiver = null;
+
+    private class MyBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+                stopVideoRecording();
+            } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+                Toast.makeText(mActivity,
+                        mActivity.getResources().getString(R.string.wait), Toast.LENGTH_LONG).show();
+            }
+        }
+    }
+
+    private String createName(long dateTaken) {
+        Date date = new Date(dateTaken);
+        SimpleDateFormat dateFormat = new SimpleDateFormat(
+                mActivity.getString(R.string.video_file_name_format));
+
+        return dateFormat.format(date);
+    }
+
+    private int getPreferredCameraId(ComboPreferences preferences) {
+        int intentCameraId = Util.getCameraFacingIntentExtras(mActivity);
+        if (intentCameraId != -1) {
+            // Testing purpose. Launch a specific camera through the intent
+            // extras.
+            return intentCameraId;
+        } else {
+            return CameraSettings.readPreferredCameraId(preferences);
+        }
+    }
+
+    private void initializeSurfaceView() {
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {  // API level < 16
+            mUI.initializeSurfaceView();
+        }
+    }
+
+    @Override
+    public void init(CameraActivity activity, View root) {
+        mActivity = activity;
+        mUI = new VideoUI(activity, this, root);
+        mPreferences = new ComboPreferences(mActivity);
+        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+        mCameraId = getPreferredCameraId(mPreferences);
+
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+
+        mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default);
+        resetEffect();
+        mOrientationManager = new OrientationManager(mActivity);
+
+        /*
+         * To reduce startup time, we start the preview in another thread.
+         * We make sure the preview is started at the end of onCreate.
+         */
+        CameraOpenThread cameraOpenThread = new CameraOpenThread();
+        cameraOpenThread.start();
+
+        mContentResolver = mActivity.getContentResolver();
+
+        // Surface texture is from camera screen nail and startPreview needs it.
+        // This must be done before startPreview.
+        mIsVideoCaptureIntent = isVideoCaptureIntent();
+        initializeSurfaceView();
+
+        // Make sure camera device is opened.
+        try {
+            cameraOpenThread.join();
+            if (mOpenCameraFail) {
+                Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+                return;
+            } else if (mCameraDisabled) {
+                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                return;
+            }
+        } catch (InterruptedException ex) {
+            // ignore
+        }
+
+        readVideoPreferences();
+        mUI.setPrefChangedListener(this);
+
+        mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
+        mLocationManager = new LocationManager(mActivity, null);
+
+        mUI.setOrientationIndicator(0, false);
+        setDisplayOrientation();
+
+        mUI.showTimeLapseUI(mCaptureTimeLapse);
+        initializeVideoSnapshot();
+        resizeForPreviewAspectRatio();
+
+        initializeVideoControl();
+        mPendingSwitchCameraId = -1;
+        mUI.updateOnScreenIndicators(mParameters, mPreferences);
+
+        // Disable the shutter button if effects are ON since it might take
+        // a little more time for the effects preview to be ready. We do not
+        // want to allow recording before that happens. The shutter button
+        // will be enabled when we get the message from effectsrecorder that
+        // the preview is running. This becomes critical when the camera is
+        // swapped.
+        if (effectsActive()) {
+            mUI.enableShutter(false);
+        }
+    }
+
+    // SingleTapListener
+    // Preview area is touched. Take a picture.
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+        if (mMediaRecorderRecording && effectsActive()) {
+            new RotateTextToast(mActivity, R.string.disable_video_snapshot_hint,
+                    mOrientation).show();
+            return;
+        }
+
+        MediaSaveService s = mActivity.getMediaSaveService();
+        if (mPaused || mSnapshotInProgress || effectsActive() || s == null || s.isQueueFull()) {
+            return;
+        }
+
+        if (!mMediaRecorderRecording) {
+            // check for dismissing popup
+            mUI.dismissPopup(true);
+            return;
+        }
+
+        // Set rotation and gps data.
+        int rotation = Util.getJpegRotation(mCameraId, mOrientation);
+        mParameters.setRotation(rotation);
+        Location loc = mLocationManager.getCurrentLocation();
+        Util.setGpsParameters(mParameters, loc);
+        mCameraDevice.setParameters(mParameters);
+
+        Log.v(TAG, "Video snapshot start");
+        mCameraDevice.takePicture(mHandler,
+                null, null, null, new JpegPictureCallback(loc));
+        showVideoSnapshotUI(true);
+        mSnapshotInProgress = true;
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+                UsageStatistics.ACTION_CAPTURE_DONE, "VideoSnapshot");
+    }
+
+    @Override
+    public void onStop() {}
+
+    private void loadCameraPreferences() {
+        CameraSettings settings = new CameraSettings(mActivity, mParameters,
+                mCameraId, CameraHolder.instance().getCameraInfo());
+        // Remove the video quality preference setting when the quality is given in the intent.
+        mPreferenceGroup = filterPreferenceScreenByIntent(
+                settings.getPreferenceGroup(R.xml.video_preferences));
+    }
+
+    private void initializeVideoControl() {
+        loadCameraPreferences();
+        mUI.initializePopup(mPreferenceGroup);
+        if (effectsActive()) {
+            mUI.overrideSettings(
+                    CameraSettings.KEY_VIDEO_QUALITY,
+                    Integer.toString(CamcorderProfile.QUALITY_480P));
+        }
+    }
+
+    @Override
+    public void onOrientationChanged(int orientation) {
+        // We keep the last known orientation. So if the user first orient
+        // the camera then point the camera to floor or sky, we still have
+        // the correct orientation.
+        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;
+        int newOrientation = Util.roundOrientation(orientation, mOrientation);
+
+        if (mOrientation != newOrientation) {
+            mOrientation = newOrientation;
+            // The input of effects recorder is affected by
+            // android.hardware.Camera.setDisplayOrientation. Its value only
+            // compensates the camera orientation (no Display.getRotation).
+            // So the orientation hint here should only consider sensor
+            // orientation.
+            if (effectsActive()) {
+                mEffectsRecorder.setOrientationHint(mOrientation);
+            }
+        }
+
+        // Show the toast after getting the first orientation changed.
+        if (mHandler.hasMessages(SHOW_TAP_TO_SNAPSHOT_TOAST)) {
+            mHandler.removeMessages(SHOW_TAP_TO_SNAPSHOT_TOAST);
+            showTapToSnapshotToast();
+        }
+    }
+
+    private void startPlayVideoActivity() {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setDataAndType(mCurrentVideoUri, convertOutputFormatToMimeType(mProfile.fileFormat));
+        try {
+            mActivity.startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
+        }
+    }
+
+    @OnClickAttr
+    public void onReviewPlayClicked(View v) {
+        startPlayVideoActivity();
+    }
+
+    @OnClickAttr
+    public void onReviewDoneClicked(View v) {
+        mIsInReviewMode = false;
+        doReturnToCaller(true);
+    }
+
+    @OnClickAttr
+    public void onReviewCancelClicked(View v) {
+        mIsInReviewMode = false;
+        stopVideoRecording();
+        doReturnToCaller(false);
+    }
+
+    @Override
+    public boolean isInReviewMode() {
+        return mIsInReviewMode;
+    }
+
+    private void onStopVideoRecording() {
+        mEffectsDisplayResult = true;
+        boolean recordFail = stopVideoRecording();
+        if (mIsVideoCaptureIntent) {
+            if (!effectsActive()) {
+                if (mQuickCapture) {
+                    doReturnToCaller(!recordFail);
+                } else if (!recordFail) {
+                    showCaptureResult();
+                }
+            }
+        } else if (!recordFail){
+            // Start capture animation.
+            if (!mPaused && ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+                // The capture animation is disabled on ICS because we use SurfaceView
+                // for preview during recording. When the recording is done, we switch
+                // back to use SurfaceTexture for preview and we need to stop then start
+                // the preview. This will cause the preview flicker since the preview
+                // will not be continuous for a short period of time.
+                // TODO: need to get the capture animation to work
+                // ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+
+                mUI.enablePreviewThumb(true);
+
+                // Make sure to disable the thumbnail preview after the
+                // animation is done to disable the click target.
+                mHandler.removeMessages(CAPTURE_ANIMATION_DONE);
+                mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+                        CaptureAnimManager.getAnimationDuration());
+            }
+        }
+    }
+
+    public void onProtectiveCurtainClick(View v) {
+        // Consume clicks
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        if (mUI.collapseCameraControls() || mSwitchingCamera) return;
+
+        boolean stop = mMediaRecorderRecording;
+
+        if (stop) {
+            onStopVideoRecording();
+        } else {
+            startVideoRecording();
+        }
+        mUI.enableShutter(false);
+
+        // Keep the shutter button disabled when in video capture intent
+        // mode and recording is stopped. It'll be re-enabled when
+        // re-take button is clicked.
+        if (!(mIsVideoCaptureIntent && stop)) {
+            mHandler.sendEmptyMessageDelayed(
+                    ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT);
+        }
+    }
+
+    @Override
+    public void onShutterButtonFocus(boolean pressed) {
+        mUI.setShutterPressed(pressed);
+    }
+
+    private void readVideoPreferences() {
+        // The preference stores values from ListPreference and is thus string type for all values.
+        // We need to convert it to int manually.
+        String videoQuality = mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY,
+                        null);
+        if (videoQuality == null) {
+            // check for highest quality before setting default value
+            videoQuality = CameraSettings.getSupportedHighestVideoQuality(mCameraId,
+                    mActivity.getResources().getString(R.string.pref_video_quality_default));
+            mPreferences.edit().putString(CameraSettings.KEY_VIDEO_QUALITY, videoQuality);
+        }
+        int quality = Integer.valueOf(videoQuality);
+
+        // Set video quality.
+        Intent intent = mActivity.getIntent();
+        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
+            int extraVideoQuality =
+                    intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
+            if (extraVideoQuality > 0) {
+                quality = CamcorderProfile.QUALITY_HIGH;
+            } else {  // 0 is mms.
+                quality = CamcorderProfile.QUALITY_LOW;
+            }
+        }
+
+        // Set video duration limit. The limit is read from the preference,
+        // unless it is specified in the intent.
+        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
+            int seconds =
+                    intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0);
+            mMaxVideoDurationInMs = 1000 * seconds;
+        } else {
+            mMaxVideoDurationInMs = CameraSettings.getMaxVideoDuration(mActivity);
+        }
+
+        // Set effect
+        mEffectType = CameraSettings.readEffectType(mPreferences);
+        if (mEffectType != EffectsRecorder.EFFECT_NONE) {
+            mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
+            // Set quality to be no higher than 480p.
+            CamcorderProfile profile = CamcorderProfile.get(mCameraId, quality);
+            if (profile.videoFrameHeight > 480) {
+                quality = CamcorderProfile.QUALITY_480P;
+            }
+        } else {
+            mEffectParameter = null;
+        }
+        // Read time lapse recording interval.
+        if (ApiHelper.HAS_TIME_LAPSE_RECORDING) {
+            String frameIntervalStr = mPreferences.getString(
+                    CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
+                    mActivity.getString(R.string.pref_video_time_lapse_frame_interval_default));
+            mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr);
+            mCaptureTimeLapse = (mTimeBetweenTimeLapseFrameCaptureMs != 0);
+        }
+        // TODO: This should be checked instead directly +1000.
+        if (mCaptureTimeLapse) quality += 1000;
+        mProfile = CamcorderProfile.get(mCameraId, quality);
+        getDesiredPreviewSize();
+        mPreferenceRead = true;
+    }
+
+    private void writeDefaultEffectToPrefs()  {
+        ComboPreferences.Editor editor = mPreferences.edit();
+        editor.putString(CameraSettings.KEY_VIDEO_EFFECT,
+                mActivity.getString(R.string.pref_video_effect_default));
+        editor.apply();
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private void getDesiredPreviewSize() {
+        mParameters = mCameraDevice.getParameters();
+        if (ApiHelper.HAS_GET_SUPPORTED_VIDEO_SIZE) {
+            if (mParameters.getSupportedVideoSizes() == null || effectsActive()) {
+                mDesiredPreviewWidth = mProfile.videoFrameWidth;
+                mDesiredPreviewHeight = mProfile.videoFrameHeight;
+            } else {  // Driver supports separates outputs for preview and video.
+                List<Size> sizes = mParameters.getSupportedPreviewSizes();
+                Size preferred = mParameters.getPreferredPreviewSizeForVideo();
+                int product = preferred.width * preferred.height;
+                Iterator<Size> it = sizes.iterator();
+                // Remove the preview sizes that are not preferred.
+                while (it.hasNext()) {
+                    Size size = it.next();
+                    if (size.width * size.height > product) {
+                        it.remove();
+                    }
+                }
+                Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes,
+                        (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
+                mDesiredPreviewWidth = optimalSize.width;
+                mDesiredPreviewHeight = optimalSize.height;
+            }
+        } else {
+            mDesiredPreviewWidth = mProfile.videoFrameWidth;
+            mDesiredPreviewHeight = mProfile.videoFrameHeight;
+        }
+        mUI.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
+        Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth +
+                ". mDesiredPreviewHeight=" + mDesiredPreviewHeight);
+    }
+
+    private void resizeForPreviewAspectRatio() {
+        mUI.setAspectRatio(
+                (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
+    }
+
+    @Override
+    public void installIntentFilter() {
+        // install an intent filter to receive SD card related events.
+        IntentFilter intentFilter =
+                new IntentFilter(Intent.ACTION_MEDIA_EJECT);
+        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+        intentFilter.addDataScheme("file");
+        mReceiver = new MyBroadcastReceiver();
+        mActivity.registerReceiver(mReceiver, intentFilter);
+    }
+
+    @Override
+    public void onResumeBeforeSuper() {
+        mPaused = false;
+    }
+
+    @Override
+    public void onResumeAfterSuper() {
+        if (mOpenCameraFail || mCameraDisabled)
+            return;
+        mUI.enableShutter(false);
+        mZoomValue = 0;
+
+        showVideoSnapshotUI(false);
+
+        if (!mPreviewing) {
+            resetEffect();
+            openCamera();
+            if (mOpenCameraFail) {
+                Util.showErrorAndFinish(mActivity,
+                        R.string.cannot_connect_camera);
+                return;
+            } else if (mCameraDisabled) {
+                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+                return;
+            }
+            readVideoPreferences();
+            resizeForPreviewAspectRatio();
+            startPreview();
+        } else {
+            // preview already started
+            mUI.enableShutter(true);
+        }
+
+        // Initializing it here after the preview is started.
+        mUI.initializeZoom(mParameters);
+
+        keepScreenOnAwhile();
+
+        // Initialize location service.
+        boolean recordLocation = RecordLocationPreference.get(mPreferences,
+                mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+
+        if (mPreviewing) {
+            mOnResumeTime = SystemClock.uptimeMillis();
+            mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+        }
+        // Dismiss open menu if exists.
+        PopupManager.getInstance(mActivity).notifyShowPopup(null);
+
+        UsageStatistics.onContentViewChanged(
+                UsageStatistics.COMPONENT_CAMERA, "VideoModule");
+    }
+
+    private void setDisplayOrientation() {
+        mDisplayRotation = Util.getDisplayRotation(mActivity);
+        mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
+        // Change the camera display orientation
+        if (mCameraDevice != null) {
+            mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+        }
+    }
+
+    @Override
+    public void updateCameraOrientation() {
+        if (mMediaRecorderRecording) return;
+        if (mDisplayRotation != Util.getDisplayRotation(mActivity)) {
+            setDisplayOrientation();
+        }
+    }
+
+    @Override
+    public int onZoomChanged(int index) {
+        // Not useful to change zoom value when the activity is paused.
+        if (mPaused) return index;
+        mZoomValue = index;
+        if (mParameters == null || mCameraDevice == null) return index;
+        // Set zoom parameters asynchronously
+        mParameters.setZoom(mZoomValue);
+        mCameraDevice.setParameters(mParameters);
+        Parameters p = mCameraDevice.getParameters();
+        if (p != null) return p.getZoom();
+        return index;
+    }
+
+    private void startPreview() {
+        Log.v(TAG, "startPreview");
+
+        SurfaceTexture surfaceTexture = mUI.getSurfaceTexture();
+        if (!mPreferenceRead || surfaceTexture == null || mPaused == true) return;
+
+        mCameraDevice.setErrorCallback(mErrorCallback);
+        if (mPreviewing == true) {
+            stopPreview();
+            if (effectsActive() && mEffectsRecorder != null) {
+                mEffectsRecorder.release();
+                mEffectsRecorder = null;
+            }
+        }
+
+        setDisplayOrientation();
+        mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+        setCameraParameters();
+
+        try {
+            if (!effectsActive()) {
+                mCameraDevice.setPreviewTexture(surfaceTexture);
+                mCameraDevice.startPreview();
+                mPreviewing = true;
+                onPreviewStarted();
+            } else {
+                initializeEffectsPreview();
+                mEffectsRecorder.startPreview();
+                mPreviewing = true;
+                onPreviewStarted();
+            }
+        } catch (Throwable ex) {
+            closeCamera();
+            throw new RuntimeException("startPreview failed", ex);
+        } finally {
+            if (mOpenCameraFail) {
+                Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+            } else if (mCameraDisabled) {
+                Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+            }
+        }
+
+    }
+
+    private void onPreviewStarted() {
+        mUI.enableShutter(true);
+    }
+
+    @Override
+    public void stopPreview() {
+        if (!mPreviewing) return;
+        mCameraDevice.stopPreview();
+        mPreviewing = false;
+    }
+
+    // Closing the effects out. Will shut down the effects graph.
+    private void closeEffects() {
+        Log.v(TAG, "Closing effects");
+        mEffectType = EffectsRecorder.EFFECT_NONE;
+        if (mEffectsRecorder == null) {
+            Log.d(TAG, "Effects are already closed. Nothing to do");
+            return;
+        }
+        // This call can handle the case where the camera is already released
+        // after the recording has been stopped.
+        mEffectsRecorder.release();
+        mEffectsRecorder = null;
+    }
+
+    // By default, we want to close the effects as well with the camera.
+    private void closeCamera() {
+        closeCamera(true);
+    }
+
+    // In certain cases, when the effects are active, we may want to shutdown
+    // only the camera related parts, and handle closing the effects in the
+    // effectsUpdate callback.
+    // For example, in onPause, we want to make the camera available to
+    // outside world immediately, however, want to wait till the effects
+    // callback to shut down the effects. In such a case, we just disconnect
+    // the effects from the camera by calling disconnectCamera. That way
+    // the effects can handle that when shutting down.
+    //
+    // @param closeEffectsAlso - indicates whether we want to close the
+    // effects also along with the camera.
+    private void closeCamera(boolean closeEffectsAlso) {
+        Log.v(TAG, "closeCamera");
+        if (mCameraDevice == null) {
+            Log.d(TAG, "already stopped.");
+            return;
+        }
+
+        if (mEffectsRecorder != null) {
+            // Disconnect the camera from effects so that camera is ready to
+            // be released to the outside world.
+            mEffectsRecorder.disconnectCamera();
+        }
+        if (closeEffectsAlso) closeEffects();
+        mCameraDevice.setZoomChangeListener(null);
+        mCameraDevice.setErrorCallback(null);
+        CameraHolder.instance().release();
+        mCameraDevice = null;
+        mPreviewing = false;
+        mSnapshotInProgress = false;
+    }
+
+    private void releasePreviewResources() {
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+            mUI.hideSurfaceView();
+        }
+    }
+
+    @Override
+    public void onPauseBeforeSuper() {
+        mPaused = true;
+
+        if (mMediaRecorderRecording) {
+            // Camera will be released in onStopVideoRecording.
+            onStopVideoRecording();
+        } else {
+            closeCamera();
+            if (!effectsActive()) releaseMediaRecorder();
+        }
+        if (effectsActive()) {
+            // If the effects are active, make sure we tell the graph that the
+            // surfacetexture is not valid anymore. Disconnect the graph from
+            // the display. This should be done before releasing the surface
+            // texture.
+            mEffectsRecorder.disconnectDisplay();
+        } else {
+            // Close the file descriptor and clear the video namer only if the
+            // effects are not active. If effects are active, we need to wait
+            // till we get the callback from the Effects that the graph is done
+            // recording. That also needs a change in the stopVideoRecording()
+            // call to not call closeCamera if the effects are active, because
+            // that will close down the effects are well, thus making this if
+            // condition invalid.
+            closeVideoFileDescriptor();
+        }
+
+        releasePreviewResources();
+
+        if (mReceiver != null) {
+            mActivity.unregisterReceiver(mReceiver);
+            mReceiver = null;
+        }
+        resetScreenOn();
+
+        if (mLocationManager != null) mLocationManager.recordLocation(false);
+
+        mHandler.removeMessages(CHECK_DISPLAY_ROTATION);
+        mHandler.removeMessages(SWITCH_CAMERA);
+        mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION);
+        mPendingSwitchCameraId = -1;
+        mSwitchingCamera = false;
+        mPreferenceRead = false;
+        // Call onPause after stopping video recording. So the camera can be
+        // released as soon as possible.
+    }
+
+    @Override
+    public void onPauseAfterSuper() {
+    }
+
+    @Override
+    public void onUserInteraction() {
+        if (!mMediaRecorderRecording && !mActivity.isFinishing()) {
+            keepScreenOnAwhile();
+        }
+    }
+
+    @Override
+    public boolean onBackPressed() {
+        if (mPaused) return true;
+        if (mMediaRecorderRecording) {
+            onStopVideoRecording();
+            return true;
+        } else if (mUI.hidePieRenderer()) {
+            return true;
+        } else {
+            return mUI.removeTopLevelPopup();
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // Do not handle any key if the activity is paused.
+        if (mPaused) {
+            return true;
+        }
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_CAMERA:
+                if (event.getRepeatCount() == 0) {
+                    mUI.clickShutter();
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                if (event.getRepeatCount() == 0) {
+                    mUI.clickShutter();
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_MENU:
+                if (mMediaRecorderRecording) return true;
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_CAMERA:
+                mUI.pressShutter(false);
+                return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isVideoCaptureIntent() {
+        String action = mActivity.getIntent().getAction();
+        return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
+    }
+
+    private void doReturnToCaller(boolean valid) {
+        Intent resultIntent = new Intent();
+        int resultCode;
+        if (valid) {
+            resultCode = Activity.RESULT_OK;
+            resultIntent.setData(mCurrentVideoUri);
+        } else {
+            resultCode = Activity.RESULT_CANCELED;
+        }
+        mActivity.setResultEx(resultCode, resultIntent);
+        mActivity.finish();
+    }
+
+    private void cleanupEmptyFile() {
+        if (mVideoFilename != null) {
+            File f = new File(mVideoFilename);
+            if (f.length() == 0 && f.delete()) {
+                Log.v(TAG, "Empty video file deleted: " + mVideoFilename);
+                mVideoFilename = null;
+            }
+        }
+    }
+
+    private void setupMediaRecorderPreviewDisplay() {
+        // Nothing to do here if using SurfaceTexture.
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+            // We stop the preview here before unlocking the device because we
+            // need to change the SurfaceTexture to SurfaceView for preview.
+            stopPreview();
+            mCameraDevice.setPreviewDisplay(mUI.getSurfaceHolder());
+            // The orientation for SurfaceTexture is different from that for
+            // SurfaceView. For SurfaceTexture we don't need to consider the
+            // display rotation. Just consider the sensor's orientation and we
+            // will set the orientation correctly when showing the texture.
+            // Gallery will handle the orientation for the preview. For
+            // SurfaceView we will have to take everything into account so the
+            // display rotation is considered.
+            mCameraDevice.setDisplayOrientation(
+                    Util.getDisplayOrientation(mDisplayRotation, mCameraId));
+            mCameraDevice.startPreview();
+            mPreviewing = true;
+            mMediaRecorder.setPreviewDisplay(mUI.getSurfaceHolder().getSurface());
+        }
+    }
+
+    // Prepares media recorder.
+    private void initializeRecorder() {
+        Log.v(TAG, "initializeRecorder");
+        // If the mCameraDevice is null, then this activity is going to finish
+        if (mCameraDevice == null) return;
+
+        if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+            // Set the SurfaceView to visible so the surface gets created.
+            // surfaceCreated() is called immediately when the visibility is
+            // changed to visible. Thus, mSurfaceViewReady should become true
+            // right after calling setVisibility().
+            mUI.showSurfaceView();
+        }
+
+        Intent intent = mActivity.getIntent();
+        Bundle myExtras = intent.getExtras();
+
+        long requestedSizeLimit = 0;
+        closeVideoFileDescriptor();
+        if (mIsVideoCaptureIntent && myExtras != null) {
+            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+            if (saveUri != null) {
+                try {
+                    mVideoFileDescriptor =
+                            mContentResolver.openFileDescriptor(saveUri, "rw");
+                    mCurrentVideoUri = saveUri;
+                } catch (java.io.FileNotFoundException ex) {
+                    // invalid uri
+                    Log.e(TAG, ex.toString());
+                }
+            }
+            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
+        }
+        mMediaRecorder = new MediaRecorder();
+
+        setupMediaRecorderPreviewDisplay();
+        // Unlock the camera object before passing it to media recorder.
+        mCameraDevice.unlock();
+        mMediaRecorder.setCamera(mCameraDevice.getCamera());
+        if (!mCaptureTimeLapse) {
+            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
+        }
+        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+        mMediaRecorder.setProfile(mProfile);
+        mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
+        if (mCaptureTimeLapse) {
+            double fps = 1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs;
+            setCaptureRate(mMediaRecorder, fps);
+        }
+
+        setRecordLocation();
+
+        // Set output file.
+        // Try Uri in the intent first. If it doesn't exist, use our own
+        // instead.
+        if (mVideoFileDescriptor != null) {
+            mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
+        } else {
+            generateVideoFilename(mProfile.fileFormat);
+            mMediaRecorder.setOutputFile(mVideoFilename);
+        }
+
+        // Set maximum file size.
+        long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
+        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
+            maxFileSize = requestedSizeLimit;
+        }
+
+        try {
+            mMediaRecorder.setMaxFileSize(maxFileSize);
+        } catch (RuntimeException exception) {
+            // We are going to ignore failure of setMaxFileSize here, as
+            // a) The composer selected may simply not support it, or
+            // b) The underlying media framework may not handle 64-bit range
+            // on the size restriction.
+        }
+
+        // See android.hardware.Camera.Parameters.setRotation for
+        // documentation.
+        // Note that mOrientation here is the device orientation, which is the opposite of
+        // what activity.getWindowManager().getDefaultDisplay().getRotation() would return,
+        // which is the orientation the graphics need to rotate in order to render correctly.
+        int rotation = 0;
+        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
+                rotation = (info.orientation - mOrientation + 360) % 360;
+            } else {  // back-facing camera
+                rotation = (info.orientation + mOrientation) % 360;
+            }
+        }
+        mMediaRecorder.setOrientationHint(rotation);
+
+        try {
+            mMediaRecorder.prepare();
+        } catch (IOException e) {
+            Log.e(TAG, "prepare failed for " + mVideoFilename, e);
+            releaseMediaRecorder();
+            throw new RuntimeException(e);
+        }
+
+        mMediaRecorder.setOnErrorListener(this);
+        mMediaRecorder.setOnInfoListener(this);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static void setCaptureRate(MediaRecorder recorder, double fps) {
+        recorder.setCaptureRate(fps);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void setRecordLocation() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            Location loc = mLocationManager.getCurrentLocation();
+            if (loc != null) {
+                mMediaRecorder.setLocation((float) loc.getLatitude(),
+                        (float) loc.getLongitude());
+            }
+        }
+    }
+
+    private void initializeEffectsPreview() {
+        Log.v(TAG, "initializeEffectsPreview");
+        // If the mCameraDevice is null, then this activity is going to finish
+        if (mCameraDevice == null) return;
+
+        boolean inLandscape = (mActivity.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE);
+
+        CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+
+        mEffectsDisplayResult = false;
+        mEffectsRecorder = new EffectsRecorder(mActivity);
+
+        // TODO: Confirm none of the following need to go to initializeEffectsRecording()
+        // and none of these change even when the preview is not refreshed.
+        mEffectsRecorder.setCameraDisplayOrientation(mCameraDisplayOrientation);
+        mEffectsRecorder.setCamera(mCameraDevice);
+        mEffectsRecorder.setCameraFacing(info.facing);
+        mEffectsRecorder.setProfile(mProfile);
+        mEffectsRecorder.setEffectsListener(this);
+        mEffectsRecorder.setOnInfoListener(this);
+        mEffectsRecorder.setOnErrorListener(this);
+
+        // The input of effects recorder is affected by
+        // android.hardware.Camera.setDisplayOrientation. Its value only
+        // compensates the camera orientation (no Display.getRotation). So the
+        // orientation hint here should only consider sensor orientation.
+        int orientation = 0;
+        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+            orientation = mOrientation;
+        }
+        mEffectsRecorder.setOrientationHint(orientation);
+
+        mEffectsRecorder.setPreviewSurfaceTexture(mUI.getSurfaceTexture(),
+            mUI.getPreviewWidth(), mUI.getPreviewHeight());
+
+        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
+                ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
+            mEffectsRecorder.setEffect(mEffectType, mEffectUriFromGallery);
+        } else {
+            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
+        }
+    }
+
+    private void initializeEffectsRecording() {
+        Log.v(TAG, "initializeEffectsRecording");
+
+        Intent intent = mActivity.getIntent();
+        Bundle myExtras = intent.getExtras();
+
+        long requestedSizeLimit = 0;
+        closeVideoFileDescriptor();
+        if (mIsVideoCaptureIntent && myExtras != null) {
+            Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+            if (saveUri != null) {
+                try {
+                    mVideoFileDescriptor =
+                            mContentResolver.openFileDescriptor(saveUri, "rw");
+                    mCurrentVideoUri = saveUri;
+                } catch (java.io.FileNotFoundException ex) {
+                    // invalid uri
+                    Log.e(TAG, ex.toString());
+                }
+            }
+            requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
+        }
+
+        mEffectsRecorder.setProfile(mProfile);
+        // important to set the capture rate to zero if not timelapsed, since the
+        // effectsrecorder object does not get created again for each recording
+        // session
+        if (mCaptureTimeLapse) {
+            mEffectsRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs));
+        } else {
+            mEffectsRecorder.setCaptureRate(0);
+        }
+
+        // Set output file
+        if (mVideoFileDescriptor != null) {
+            mEffectsRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
+        } else {
+            generateVideoFilename(mProfile.fileFormat);
+            mEffectsRecorder.setOutputFile(mVideoFilename);
+        }
+
+        // Set maximum file size.
+        long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
+        if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
+            maxFileSize = requestedSizeLimit;
+        }
+        mEffectsRecorder.setMaxFileSize(maxFileSize);
+        mEffectsRecorder.setMaxDuration(mMaxVideoDurationInMs);
+    }
+
+
+    private void releaseMediaRecorder() {
+        Log.v(TAG, "Releasing media recorder.");
+        if (mMediaRecorder != null) {
+            cleanupEmptyFile();
+            mMediaRecorder.reset();
+            mMediaRecorder.release();
+            mMediaRecorder = null;
+        }
+        mVideoFilename = null;
+    }
+
+    private void releaseEffectsRecorder() {
+        Log.v(TAG, "Releasing effects recorder.");
+        if (mEffectsRecorder != null) {
+            cleanupEmptyFile();
+            mEffectsRecorder.release();
+            mEffectsRecorder = null;
+        }
+        mEffectType = EffectsRecorder.EFFECT_NONE;
+        mVideoFilename = null;
+    }
+
+    private void generateVideoFilename(int outputFileFormat) {
+        long dateTaken = System.currentTimeMillis();
+        String title = createName(dateTaken);
+        // Used when emailing.
+        String filename = title + convertOutputFormatToFileExt(outputFileFormat);
+        String mime = convertOutputFormatToMimeType(outputFileFormat);
+        String path = Storage.DIRECTORY + '/' + filename;
+        String tmpPath = path + ".tmp";
+        mCurrentVideoValues = new ContentValues(9);
+        mCurrentVideoValues.put(Video.Media.TITLE, title);
+        mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename);
+        mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken);
+        mCurrentVideoValues.put(MediaColumns.DATE_MODIFIED, dateTaken / 1000);
+        mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime);
+        mCurrentVideoValues.put(Video.Media.DATA, path);
+        mCurrentVideoValues.put(Video.Media.RESOLUTION,
+                Integer.toString(mProfile.videoFrameWidth) + "x" +
+                Integer.toString(mProfile.videoFrameHeight));
+        Location loc = mLocationManager.getCurrentLocation();
+        if (loc != null) {
+            mCurrentVideoValues.put(Video.Media.LATITUDE, loc.getLatitude());
+            mCurrentVideoValues.put(Video.Media.LONGITUDE, loc.getLongitude());
+        }
+        mVideoFilename = tmpPath;
+        Log.v(TAG, "New video filename: " + mVideoFilename);
+    }
+
+    private void saveVideo() {
+        if (mVideoFileDescriptor == null) {
+            long duration = SystemClock.uptimeMillis() - mRecordingStartTime;
+            if (duration > 0) {
+                if (mCaptureTimeLapse) {
+                    duration = getTimeLapseVideoLength(duration);
+                }
+            } else {
+                Log.w(TAG, "Video duration <= 0 : " + duration);
+            }
+            mActivity.getMediaSaveService().addVideo(mCurrentVideoFilename,
+                    duration, mCurrentVideoValues,
+                    mOnVideoSavedListener, mContentResolver);
+        }
+        mCurrentVideoValues = null;
+    }
+
+    private void deleteVideoFile(String fileName) {
+        Log.v(TAG, "Deleting video " + fileName);
+        File f = new File(fileName);
+        if (!f.delete()) {
+            Log.v(TAG, "Could not delete " + fileName);
+        }
+    }
+
+    private PreferenceGroup filterPreferenceScreenByIntent(
+            PreferenceGroup screen) {
+        Intent intent = mActivity.getIntent();
+        if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
+            CameraSettings.removePreferenceFromScreen(screen,
+                    CameraSettings.KEY_VIDEO_QUALITY);
+        }
+
+        if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
+            CameraSettings.removePreferenceFromScreen(screen,
+                    CameraSettings.KEY_VIDEO_QUALITY);
+        }
+        return screen;
+    }
+
+    // from MediaRecorder.OnErrorListener
+    @Override
+    public void onError(MediaRecorder mr, int what, int extra) {
+        Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra);
+        if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
+            // We may have run out of space on the sdcard.
+            stopVideoRecording();
+            mActivity.updateStorageSpaceAndHint();
+        }
+    }
+
+    // from MediaRecorder.OnInfoListener
+    @Override
+    public void onInfo(MediaRecorder mr, int what, int extra) {
+        if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
+            if (mMediaRecorderRecording) onStopVideoRecording();
+        } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
+            if (mMediaRecorderRecording) onStopVideoRecording();
+
+            // Show the toast.
+            Toast.makeText(mActivity, R.string.video_reach_size_limit,
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /*
+     * Make sure we're not recording music playing in the background, ask the
+     * MediaPlaybackService to pause playback.
+     */
+    private void pauseAudioPlayback() {
+        // Shamelessly copied from MediaPlaybackService.java, which
+        // should be public, but isn't.
+        Intent i = new Intent("com.android.music.musicservicecommand");
+        i.putExtra("command", "pause");
+
+        mActivity.sendBroadcast(i);
+    }
+
+    // For testing.
+    public boolean isRecording() {
+        return mMediaRecorderRecording;
+    }
+
+    private void startVideoRecording() {
+        Log.v(TAG, "startVideoRecording");
+        mUI.enablePreviewThumb(false);
+        mUI.setSwipingEnabled(false);
+
+        mActivity.updateStorageSpaceAndHint();
+        if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) {
+            Log.v(TAG, "Storage issue, ignore the start request");
+            return;
+        }
+
+        //??
+        //if (!mCameraDevice.waitDone()) return;
+        mCurrentVideoUri = null;
+        if (effectsActive()) {
+            initializeEffectsRecording();
+            if (mEffectsRecorder == null) {
+                Log.e(TAG, "Fail to initialize effect recorder");
+                return;
+            }
+        } else {
+            initializeRecorder();
+            if (mMediaRecorder == null) {
+                Log.e(TAG, "Fail to initialize media recorder");
+                return;
+            }
+        }
+
+        pauseAudioPlayback();
+
+        if (effectsActive()) {
+            try {
+                mEffectsRecorder.startRecording();
+            } catch (RuntimeException e) {
+                Log.e(TAG, "Could not start effects recorder. ", e);
+                releaseEffectsRecorder();
+                return;
+            }
+        } else {
+            try {
+                mMediaRecorder.start(); // Recording is now started
+            } catch (RuntimeException e) {
+                Log.e(TAG, "Could not start media recorder. ", e);
+                releaseMediaRecorder();
+                // If start fails, frameworks will not lock the camera for us.
+                mCameraDevice.lock();
+                return;
+            }
+        }
+
+        // Make sure the video recording has started before announcing
+        // this in accessibility.
+        AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(),
+                mActivity.getString(R.string.video_recording_started));
+
+        // The parameters might have been altered by MediaRecorder already.
+        // We need to force mCameraDevice to refresh before getting it.
+        mCameraDevice.refreshParameters();
+        // The parameters may have been changed by MediaRecorder upon starting
+        // recording. We need to alter the parameters if we support camcorder
+        // zoom. To reduce latency when setting the parameters during zoom, we
+        // update mParameters here once.
+        if (ApiHelper.HAS_ZOOM_WHEN_RECORDING) {
+            mParameters = mCameraDevice.getParameters();
+        }
+
+        mUI.enableCameraControls(false);
+
+        mMediaRecorderRecording = true;
+        mOrientationManager.lockOrientation();
+        mRecordingStartTime = SystemClock.uptimeMillis();
+        mUI.showRecordingUI(true, mParameters.isZoomSupported());
+
+        updateRecordingTime();
+        keepScreenOn();
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+                UsageStatistics.ACTION_CAPTURE_START, "Video");
+    }
+
+    private void showCaptureResult() {
+        mIsInReviewMode = true;
+        Bitmap bitmap = null;
+        if (mVideoFileDescriptor != null) {
+            bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(),
+                    mDesiredPreviewWidth);
+        } else if (mCurrentVideoFilename != null) {
+            bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename,
+                    mDesiredPreviewWidth);
+        }
+        if (bitmap != null) {
+            // MetadataRetriever already rotates the thumbnail. We should rotate
+            // it to match the UI orientation (and mirror if it is front-facing camera).
+            CameraInfo[] info = CameraHolder.instance().getCameraInfo();
+            boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT);
+            bitmap = Util.rotateAndMirror(bitmap, 0, mirror);
+            mUI.showReviewImage(bitmap);
+        }
+
+        mUI.showReviewControls();
+        mUI.enableCameraControls(false);
+        mUI.showTimeLapseUI(false);
+    }
+
+    private void hideAlert() {
+        mUI.enableCameraControls(true);
+        mUI.hideReviewUI();
+        if (mCaptureTimeLapse) {
+            mUI.showTimeLapseUI(true);
+        }
+    }
+
+    private boolean stopVideoRecording() {
+        Log.v(TAG, "stopVideoRecording");
+        mUI.setSwipingEnabled(true);
+        mUI.showSwitcher();
+
+        boolean fail = false;
+        if (mMediaRecorderRecording) {
+            boolean shouldAddToMediaStoreNow = false;
+
+            try {
+                if (effectsActive()) {
+                    // This is asynchronous, so we can't add to media store now because thumbnail
+                    // may not be ready. In such case saveVideo() is called later
+                    // through a callback from the MediaEncoderFilter to EffectsRecorder,
+                    // and then to the VideoModule.
+                    mEffectsRecorder.stopRecording();
+                } else {
+                    mMediaRecorder.setOnErrorListener(null);
+                    mMediaRecorder.setOnInfoListener(null);
+                    mMediaRecorder.stop();
+                    shouldAddToMediaStoreNow = true;
+                }
+                mCurrentVideoFilename = mVideoFilename;
+                Log.v(TAG, "stopVideoRecording: Setting current video filename: "
+                        + mCurrentVideoFilename);
+                AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(),
+                        mActivity.getString(R.string.video_recording_stopped));
+            } catch (RuntimeException e) {
+                Log.e(TAG, "stop fail",  e);
+                if (mVideoFilename != null) deleteVideoFile(mVideoFilename);
+                fail = true;
+            }
+            mMediaRecorderRecording = false;
+            mOrientationManager.unlockOrientation();
+
+            // If the activity is paused, this means activity is interrupted
+            // during recording. Release the camera as soon as possible because
+            // face unlock or other applications may need to use the camera.
+            // However, if the effects are active, then we can only release the
+            // camera and cannot release the effects recorder since that will
+            // stop the graph. It is possible to separate out the Camera release
+            // part and the effects release part. However, the effects recorder
+            // does hold on to the camera, hence, it needs to be "disconnected"
+            // from the camera in the closeCamera call.
+            if (mPaused) {
+                // Closing only the camera part if effects active. Effects will
+                // be closed in the callback from effects.
+                boolean closeEffects = !effectsActive();
+                closeCamera(closeEffects);
+            }
+
+            mUI.showRecordingUI(false, mParameters.isZoomSupported());
+            if (!mIsVideoCaptureIntent) {
+                mUI.enableCameraControls(true);
+            }
+            // The orientation was fixed during video recording. Now make it
+            // reflect the device orientation as video recording is stopped.
+            mUI.setOrientationIndicator(0, true);
+            keepScreenOnAwhile();
+            if (shouldAddToMediaStoreNow) {
+                saveVideo();
+            }
+        }
+        // always release media recorder if no effects running
+        if (!effectsActive()) {
+            releaseMediaRecorder();
+            if (!mPaused) {
+                mCameraDevice.lock();
+                if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+                    stopPreview();
+                    mUI.hideSurfaceView();
+                    // Switch back to use SurfaceTexture for preview.
+                    startPreview();
+                }
+            }
+        }
+        // Update the parameters here because the parameters might have been altered
+        // by MediaRecorder.
+        if (!mPaused) mParameters = mCameraDevice.getParameters();
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+                fail ? UsageStatistics.ACTION_CAPTURE_FAIL :
+                    UsageStatistics.ACTION_CAPTURE_DONE, "Video",
+                    SystemClock.uptimeMillis() - mRecordingStartTime);
+        return fail;
+    }
+
+    private void resetScreenOn() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private void keepScreenOnAwhile() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+    }
+
+    private void keepScreenOn() {
+        mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) {
+        long seconds = milliSeconds / 1000; // round down to compute seconds
+        long minutes = seconds / 60;
+        long hours = minutes / 60;
+        long remainderMinutes = minutes - (hours * 60);
+        long remainderSeconds = seconds - (minutes * 60);
+
+        StringBuilder timeStringBuilder = new StringBuilder();
+
+        // Hours
+        if (hours > 0) {
+            if (hours < 10) {
+                timeStringBuilder.append('0');
+            }
+            timeStringBuilder.append(hours);
+
+            timeStringBuilder.append(':');
+        }
+
+        // Minutes
+        if (remainderMinutes < 10) {
+            timeStringBuilder.append('0');
+        }
+        timeStringBuilder.append(remainderMinutes);
+        timeStringBuilder.append(':');
+
+        // Seconds
+        if (remainderSeconds < 10) {
+            timeStringBuilder.append('0');
+        }
+        timeStringBuilder.append(remainderSeconds);
+
+        // Centi seconds
+        if (displayCentiSeconds) {
+            timeStringBuilder.append('.');
+            long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10;
+            if (remainderCentiSeconds < 10) {
+                timeStringBuilder.append('0');
+            }
+            timeStringBuilder.append(remainderCentiSeconds);
+        }
+
+        return timeStringBuilder.toString();
+    }
+
+    private long getTimeLapseVideoLength(long deltaMs) {
+        // For better approximation calculate fractional number of frames captured.
+        // This will update the video time at a higher resolution.
+        double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs;
+        return (long) (numberOfFrames / mProfile.videoFrameRate * 1000);
+    }
+
+    private void updateRecordingTime() {
+        if (!mMediaRecorderRecording) {
+            return;
+        }
+        long now = SystemClock.uptimeMillis();
+        long delta = now - mRecordingStartTime;
+
+        // Starting a minute before reaching the max duration
+        // limit, we'll countdown the remaining time instead.
+        boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0
+                && delta >= mMaxVideoDurationInMs - 60000);
+
+        long deltaAdjusted = delta;
+        if (countdownRemainingTime) {
+            deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999;
+        }
+        String text;
+
+        long targetNextUpdateDelay;
+        if (!mCaptureTimeLapse) {
+            text = millisecondToTimeString(deltaAdjusted, false);
+            targetNextUpdateDelay = 1000;
+        } else {
+            // The length of time lapse video is different from the length
+            // of the actual wall clock time elapsed. Display the video length
+            // only in format hh:mm:ss.dd, where dd are the centi seconds.
+            text = millisecondToTimeString(getTimeLapseVideoLength(delta), true);
+            targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs;
+        }
+
+        mUI.setRecordingTime(text);
+
+        if (mRecordingTimeCountsDown != countdownRemainingTime) {
+            // Avoid setting the color on every update, do it only
+            // when it needs changing.
+            mRecordingTimeCountsDown = countdownRemainingTime;
+
+            int color = mActivity.getResources().getColor(countdownRemainingTime
+                    ? R.color.recording_time_remaining_text
+                    : R.color.recording_time_elapsed_text);
+
+            mUI.setRecordingTimeTextColor(color);
+        }
+
+        long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay);
+        mHandler.sendEmptyMessageDelayed(
+                UPDATE_RECORD_TIME, actualNextUpdateDelay);
+    }
+
+    private static boolean isSupported(String value, List<String> supported) {
+        return supported == null ? false : supported.indexOf(value) >= 0;
+    }
+
+    @SuppressWarnings("deprecation")
+    private void setCameraParameters() {
+        mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
+        int[] fpsRange = Util.getMaxPreviewFpsRange(mParameters);
+        if (fpsRange.length > 0) {
+            mParameters.setPreviewFpsRange(
+                    fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX],
+                    fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]);
+        } else {
+            mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
+        }
+
+        // Set flash mode.
+        String flashMode;
+        if (mUI.isVisible()) {
+            flashMode = mPreferences.getString(
+                    CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
+                    mActivity.getString(R.string.pref_camera_video_flashmode_default));
+        } else {
+            flashMode = Parameters.FLASH_MODE_OFF;
+        }
+        List<String> supportedFlash = mParameters.getSupportedFlashModes();
+        if (isSupported(flashMode, supportedFlash)) {
+            mParameters.setFlashMode(flashMode);
+        } else {
+            flashMode = mParameters.getFlashMode();
+            if (flashMode == null) {
+                flashMode = mActivity.getString(
+                        R.string.pref_camera_flashmode_no_flash);
+            }
+        }
+
+        // Set white balance parameter.
+        String whiteBalance = mPreferences.getString(
+                CameraSettings.KEY_WHITE_BALANCE,
+                mActivity.getString(R.string.pref_camera_whitebalance_default));
+        if (isSupported(whiteBalance,
+                mParameters.getSupportedWhiteBalance())) {
+            mParameters.setWhiteBalance(whiteBalance);
+        } else {
+            whiteBalance = mParameters.getWhiteBalance();
+            if (whiteBalance == null) {
+                whiteBalance = Parameters.WHITE_BALANCE_AUTO;
+            }
+        }
+
+        // Set zoom.
+        if (mParameters.isZoomSupported()) {
+            mParameters.setZoom(mZoomValue);
+        }
+
+        // Set continuous autofocus.
+        List<String> supportedFocus = mParameters.getSupportedFocusModes();
+        if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) {
+            mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+        }
+
+        mParameters.set(Util.RECORDING_HINT, Util.TRUE);
+
+        // Enable video stabilization. Convenience methods not available in API
+        // level <= 14
+        String vstabSupported = mParameters.get("video-stabilization-supported");
+        if ("true".equals(vstabSupported)) {
+            mParameters.set("video-stabilization", "true");
+        }
+
+        // Set picture size.
+        // The logic here is different from the logic in still-mode camera.
+        // There we determine the preview size based on the picture size, but
+        // here we determine the picture size based on the preview size.
+        List<Size> supported = mParameters.getSupportedPictureSizes();
+        Size optimalSize = Util.getOptimalVideoSnapshotPictureSize(supported,
+                (double) mDesiredPreviewWidth / mDesiredPreviewHeight);
+        Size original = mParameters.getPictureSize();
+        if (!original.equals(optimalSize)) {
+            mParameters.setPictureSize(optimalSize.width, optimalSize.height);
+        }
+        Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" +
+                optimalSize.height);
+
+        // Set JPEG quality.
+        int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
+                CameraProfile.QUALITY_HIGH);
+        mParameters.setJpegQuality(jpegQuality);
+
+        mCameraDevice.setParameters(mParameters);
+        // Keep preview size up to date.
+        mParameters = mCameraDevice.getParameters();
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_EFFECT_BACKDROPPER:
+                if (resultCode == Activity.RESULT_OK) {
+                    // onActivityResult() runs before onResume(), so this parameter will be
+                    // seen by startPreview from onResume()
+                    mEffectUriFromGallery = data.getData().toString();
+                    Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery);
+                    mResetEffect = false;
+                } else {
+                    mEffectUriFromGallery = null;
+                    Log.w(TAG, "No URI from gallery");
+                    mResetEffect = true;
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void onEffectsUpdate(int effectId, int effectMsg) {
+        Log.v(TAG, "onEffectsUpdate. Effect Message = " + effectMsg);
+        if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) {
+            // Effects have shut down. Hide learning message if any,
+            // and restart regular preview.
+            checkQualityAndStartPreview();
+        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) {
+            // This follows the codepath from onStopVideoRecording.
+            if (mEffectsDisplayResult) {
+                saveVideo();
+                if (mIsVideoCaptureIntent) {
+                    if (mQuickCapture) {
+                        doReturnToCaller(true);
+                    } else {
+                        showCaptureResult();
+                    }
+                }
+            }
+            mEffectsDisplayResult = false;
+            // In onPause, these were not called if the effects were active. We
+            // had to wait till the effects recording is complete to do this.
+            if (mPaused) {
+                closeVideoFileDescriptor();
+            }
+        } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) {
+            // Enable the shutter button once the preview is complete.
+            mUI.enableShutter(true);
+        }
+        // In onPause, this was not called if the effects were active. We had to
+        // wait till the effects completed to do this.
+        if (mPaused) {
+            Log.v(TAG, "OnEffectsUpdate: closing effects if activity paused");
+            closeEffects();
+        }
+    }
+
+    public void onCancelBgTraining(View v) {
+        // Write default effect out to shared prefs
+        writeDefaultEffectToPrefs();
+        // Tell VideoCamer to re-init based on new shared pref values.
+        onSharedPreferenceChanged();
+    }
+
+    @Override
+    public synchronized void onEffectsError(Exception exception, String fileName) {
+        // TODO: Eventually we may want to show the user an error dialog, and then restart the
+        // camera and encoder gracefully. For now, we just delete the file and bail out.
+        if (fileName != null && new File(fileName).exists()) {
+            deleteVideoFile(fileName);
+        }
+        try {
+            if (Class.forName("android.filterpacks.videosink.MediaRecorderStopException")
+                    .isInstance(exception)) {
+                Log.w(TAG, "Problem recoding video file. Removing incomplete file.");
+                return;
+            }
+        } catch (ClassNotFoundException ex) {
+            Log.w(TAG, ex);
+        }
+        throw new RuntimeException("Error during recording!", exception);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        Log.v(TAG, "onConfigurationChanged");
+        setDisplayOrientation();
+    }
+
+    @Override
+    public void onOverriddenPreferencesClicked() {
+    }
+
+    @Override
+    // TODO: Delete this after old camera code is removed
+    public void onRestorePreferencesClicked() {
+    }
+
+    private boolean effectsActive() {
+        return (mEffectType != EffectsRecorder.EFFECT_NONE);
+    }
+
+    @Override
+    public void onSharedPreferenceChanged() {
+        // ignore the events after "onPause()" or preview has not started yet
+        if (mPaused) return;
+        synchronized (mPreferences) {
+            // If mCameraDevice is not ready then we can set the parameter in
+            // startPreview().
+            if (mCameraDevice == null) return;
+
+            boolean recordLocation = RecordLocationPreference.get(
+                    mPreferences, mContentResolver);
+            mLocationManager.recordLocation(recordLocation);
+
+            // Check if the current effects selection has changed
+            if (updateEffectSelection()) return;
+
+            readVideoPreferences();
+            mUI.showTimeLapseUI(mCaptureTimeLapse);
+            // We need to restart the preview if preview size is changed.
+            Size size = mParameters.getPreviewSize();
+            if (size.width != mDesiredPreviewWidth
+                    || size.height != mDesiredPreviewHeight) {
+                if (!effectsActive()) {
+                    stopPreview();
+                } else {
+                    mEffectsRecorder.release();
+                    mEffectsRecorder = null;
+                }
+                resizeForPreviewAspectRatio();
+                startPreview(); // Parameters will be set in startPreview().
+            } else {
+                setCameraParameters();
+            }
+            mUI.updateOnScreenIndicators(mParameters, mPreferences);
+        }
+    }
+
+    protected void setCameraId(int cameraId) {
+        ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+        pref.setValue("" + cameraId);
+    }
+
+    private void switchCamera() {
+        if (mPaused) return;
+
+        Log.d(TAG, "Start to switch camera.");
+        mCameraId = mPendingSwitchCameraId;
+        mPendingSwitchCameraId = -1;
+        setCameraId(mCameraId);
+
+        closeCamera();
+        mUI.collapseCameraControls();
+        // Restart the camera and initialize the UI. From onCreate.
+        mPreferences.setLocalId(mActivity, mCameraId);
+        CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+        openCamera();
+        readVideoPreferences();
+        startPreview();
+        initializeVideoSnapshot();
+        resizeForPreviewAspectRatio();
+        initializeVideoControl();
+
+        // From onResume
+        mZoomValue = 0;
+        mUI.initializeZoom(mParameters);
+        mUI.setOrientationIndicator(0, false);
+
+        // Start switch camera animation. Post a message because
+        // onFrameAvailable from the old camera may already exist.
+        mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION);
+        mUI.updateOnScreenIndicators(mParameters, mPreferences);
+    }
+
+    // Preview texture has been copied. Now camera can be released and the
+    // animation can be started.
+    @Override
+    public void onPreviewTextureCopied() {
+        mHandler.sendEmptyMessage(SWITCH_CAMERA);
+    }
+
+    @Override
+    public void onCaptureTextureCopied() {
+    }
+
+    private boolean updateEffectSelection() {
+        int previousEffectType = mEffectType;
+        Object previousEffectParameter = mEffectParameter;
+        mEffectType = CameraSettings.readEffectType(mPreferences);
+        mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
+
+        if (mEffectType == previousEffectType) {
+            if (mEffectType == EffectsRecorder.EFFECT_NONE) return false;
+            if (mEffectParameter.equals(previousEffectParameter)) return false;
+        }
+        Log.v(TAG, "New effect selection: " + mPreferences.getString(
+                CameraSettings.KEY_VIDEO_EFFECT, "none"));
+
+        if (mEffectType == EffectsRecorder.EFFECT_NONE) {
+            // Stop effects and return to normal preview
+            mEffectsRecorder.stopPreview();
+            mPreviewing = false;
+            return true;
+        }
+        if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
+            ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
+            // Request video from gallery to use for background
+            Intent i = new Intent(Intent.ACTION_PICK);
+            i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI,
+                             "video/*");
+            i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
+            mActivity.startActivityForResult(i, REQUEST_EFFECT_BACKDROPPER);
+            return true;
+        }
+        if (previousEffectType == EffectsRecorder.EFFECT_NONE) {
+            // Stop regular preview and start effects.
+            stopPreview();
+            checkQualityAndStartPreview();
+        } else {
+            // Switch currently running effect
+            mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
+        }
+        return true;
+    }
+
+    // Verifies that the current preview view size is correct before starting
+    // preview. If not, resets the surface texture and resizes the view.
+    private void checkQualityAndStartPreview() {
+        readVideoPreferences();
+        mUI.showTimeLapseUI(mCaptureTimeLapse);
+        Size size = mParameters.getPreviewSize();
+        if (size.width != mDesiredPreviewWidth
+                || size.height != mDesiredPreviewHeight) {
+            resizeForPreviewAspectRatio();
+        }
+        // Start up preview again
+        startPreview();
+    }
+
+    private void initializeVideoSnapshot() {
+        if (mParameters == null) return;
+        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+            // Show the tap to focus toast if this is the first start.
+            if (mPreferences.getBoolean(
+                        CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) {
+                // Delay the toast for one second to wait for orientation.
+                mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000);
+            }
+        }
+    }
+
+    void showVideoSnapshotUI(boolean enabled) {
+        if (mParameters == null) return;
+        if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+            if (enabled) {
+             // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+            } else {
+                mUI.showPreviewBorder(enabled);
+            }
+            mUI.enableShutter(!enabled);
+        }
+    }
+
+    @Override
+    public void updateCameraAppView() {
+        if (!mPreviewing || mParameters.getFlashMode() == null) return;
+
+        // When going to and back from gallery, we need to turn off/on the flash.
+        if (!mUI.isVisible()) {
+            if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) {
+                mRestoreFlash = false;
+                return;
+            }
+            mRestoreFlash = true;
+            setCameraParameters();
+        } else if (mRestoreFlash) {
+            mRestoreFlash = false;
+            setCameraParameters();
+        }
+    }
+
+    @Override
+    public void onSwitchMode(boolean toCamera) {
+        mUI.onSwitchMode(toCamera);
+    }
+
+    private final class JpegPictureCallback implements CameraPictureCallback {
+        Location mLocation;
+
+        public JpegPictureCallback(Location loc) {
+            mLocation = loc;
+        }
+
+        @Override
+        public void onPictureTaken(byte [] jpegData, CameraProxy camera) {
+            Log.v(TAG, "onPictureTaken");
+            mSnapshotInProgress = false;
+            showVideoSnapshotUI(false);
+            storeImage(jpegData, mLocation);
+        }
+    }
+
+    private void storeImage(final byte[] data, Location loc) {
+        long dateTaken = System.currentTimeMillis();
+        String title = Util.createJpegName(dateTaken);
+        ExifInterface exif = Exif.getExif(data);
+        int orientation = Exif.getOrientation(exif);
+        Size s = mParameters.getPictureSize();
+        mActivity.getMediaSaveService().addImage(
+                data, title, dateTaken, loc, s.width, s.height, orientation,
+                exif, mOnPhotoSavedListener, mContentResolver);
+    }
+
+    private boolean resetEffect() {
+        if (mResetEffect) {
+            String value = mPreferences.getString(CameraSettings.KEY_VIDEO_EFFECT,
+                    mPrefVideoEffectDefault);
+            if (!mPrefVideoEffectDefault.equals(value)) {
+                writeDefaultEffectToPrefs();
+                return true;
+            }
+        }
+        mResetEffect = true;
+        return false;
+    }
+
+    private String convertOutputFormatToMimeType(int outputFileFormat) {
+        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
+            return "video/mp4";
+        }
+        return "video/3gpp";
+    }
+
+    private String convertOutputFormatToFileExt(int outputFileFormat) {
+        if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
+            return ".mp4";
+        }
+        return ".3gp";
+    }
+
+    private void closeVideoFileDescriptor() {
+        if (mVideoFileDescriptor != null) {
+            try {
+                mVideoFileDescriptor.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Fail to close fd", e);
+            }
+            mVideoFileDescriptor = null;
+        }
+    }
+
+    private void showTapToSnapshotToast() {
+        new RotateTextToast(mActivity, R.string.video_snapshot_hint, 0)
+                .show();
+        // Clear the preference.
+        Editor editor = mPreferences.edit();
+        editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false);
+        editor.apply();
+    }
+
+    @Override
+    public boolean updateStorageHintOnResume() {
+        return true;
+    }
+
+    // required by OnPreferenceChangedListener
+    @Override
+    public void onCameraPickerClicked(int cameraId) {
+        if (mPaused || mPendingSwitchCameraId != -1) return;
+
+        mPendingSwitchCameraId = cameraId;
+        Log.d(TAG, "Start to copy texture.");
+        // We need to keep a preview frame for the animation before
+        // releasing the camera. This will trigger onPreviewTextureCopied.
+        // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture();
+        // Disable all camera controls.
+        mSwitchingCamera = true;
+
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+        mUI.onShowSwitcherPopup();
+    }
+
+    @Override
+    public void onMediaSaveServiceConnected(MediaSaveService s) {
+        // do nothing.
+    }
+
+    @Override
+    public void onPreviewUIReady() {
+        startPreview();
+    }
+
+    @Override
+    public void onPreviewUIDestroyed() {
+        stopPreview();
+    }
+}
diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java
new file mode 100644
index 0000000..551b725
--- /dev/null
+++ b/src/com/android/camera/VideoUI.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2012 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.Matrix;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CameraControls;
+import com.android.camera.ui.CameraRootView;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.RotateLayout;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.List;
+
+public class VideoUI implements PieRenderer.PieListener,
+        PreviewGestures.SingleTapListener,
+        CameraRootView.MyDisplayListener,
+        SurfaceTextureListener, SurfaceHolder.Callback {
+    private static final String TAG = "CAM_VideoUI";
+    private static final int UPDATE_TRANSFORM_MATRIX = 1;
+    // module fields
+    private CameraActivity mActivity;
+    private View mRootView;
+    private TextureView mTextureView;
+    // An review image having same size as preview. It is displayed when
+    // recording is stopped in capture intent.
+    private ImageView mReviewImage;
+    private View mReviewCancelButton;
+    private View mReviewDoneButton;
+    private View mReviewPlayButton;
+    private ShutterButton mShutterButton;
+    private CameraSwitcher mSwitcher;
+    private TextView mRecordingTimeView;
+    private LinearLayout mLabelsLinearLayout;
+    private View mTimeLapseLabel;
+    private RenderOverlay mRenderOverlay;
+    private PieRenderer mPieRenderer;
+    private VideoMenu mVideoMenu;
+    private CameraControls mCameraControls;
+    private AbstractSettingPopup mPopup;
+    private ZoomRenderer mZoomRenderer;
+    private PreviewGestures mGestures;
+    private View mMenuButton;
+    private View mBlocker;
+    private OnScreenIndicators mOnScreenIndicators;
+    private RotateLayout mRecordingTimeRect;
+    private final Object mLock = new Object();
+    private SurfaceTexture mSurfaceTexture;
+    private VideoController mController;
+    private int mZoomMax;
+    private List<Integer> mZoomRatios;
+    private View mPreviewThumb;
+
+    private SurfaceView mSurfaceView = null;
+    private int mPreviewWidth = 0;
+    private int mPreviewHeight = 0;
+    private float mSurfaceTextureUncroppedWidth;
+    private float mSurfaceTextureUncroppedHeight;
+    private float mAspectRatio = 4f / 3f;
+    private Matrix mMatrix = null;
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case UPDATE_TRANSFORM_MATRIX:
+                    setTransformMatrix(mPreviewWidth, mPreviewHeight);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+    private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right,
+                int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+            int width = right - left;
+            int height = bottom - top;
+            // Full-screen screennail
+            int w = width;
+            int h = height;
+            if (Util.getDisplayRotation(mActivity) % 180 != 0) {
+                w = height;
+                h = width;
+            }
+            if (mPreviewWidth != width || mPreviewHeight != height) {
+                mPreviewWidth = width;
+                mPreviewHeight = height;
+                onScreenSizeChanged(width, height, w, h);
+            }
+        }
+    };
+
+    public VideoUI(CameraActivity activity, VideoController controller, View parent) {
+        mActivity = activity;
+        mController = controller;
+        mRootView = parent;
+        mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView, true);
+        mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content);
+        mTextureView.setSurfaceTextureListener(this);
+        mRootView.addOnLayoutChangeListener(mLayoutListener);
+        ((CameraRootView) mRootView).setDisplayChangeListener(this);
+        mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
+        mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
+        mSwitcher.setCurrentIndex(CameraSwitcher.VIDEO_MODULE_INDEX);
+        mSwitcher.setSwitchListener((CameraSwitchListener) mActivity);
+        initializeMiscControls();
+        initializeControlByIntent();
+        initializeOverlay();
+    }
+
+
+    public void initializeSurfaceView() {
+        mSurfaceView = new SurfaceView(mActivity);
+        ((ViewGroup) mRootView).addView(mSurfaceView, 0);
+        mSurfaceView.getHolder().addCallback(this);
+    }
+
+    private void initializeControlByIntent() {
+        mBlocker = mActivity.findViewById(R.id.blocker);
+        mMenuButton = mActivity.findViewById(R.id.menu);
+        mMenuButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mPieRenderer != null) {
+                    mPieRenderer.showInCenter();
+                }
+            }
+        });
+
+        mCameraControls = (CameraControls) mActivity.findViewById(R.id.camera_controls);
+        mOnScreenIndicators = new OnScreenIndicators(mActivity,
+                mActivity.findViewById(R.id.on_screen_indicators));
+        mOnScreenIndicators.resetToDefault();
+        if (mController.isVideoCaptureIntent()) {
+            hideSwitcher();
+            mActivity.getLayoutInflater().inflate(R.layout.review_module_control,
+                    (ViewGroup) mCameraControls);
+            // Cannot use RotateImageView for "done" and "cancel" button because
+            // the tablet layout uses RotateLayout, which cannot be cast to
+            // RotateImageView.
+            mReviewDoneButton = mActivity.findViewById(R.id.btn_done);
+            mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel);
+            mReviewPlayButton = mActivity.findViewById(R.id.btn_play);
+            mReviewCancelButton.setVisibility(View.VISIBLE);
+            mReviewDoneButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onReviewDoneClicked(v);
+                }
+            });
+            mReviewCancelButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onReviewCancelClicked(v);
+                }
+            });
+            mReviewPlayButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mController.onReviewPlayClicked(v);
+                }
+            });
+        }
+    }
+
+    public void setPreviewSize(int width, int height) {
+        if (width == 0 || height == 0) {
+            Log.w(TAG, "Preview size should not be 0.");
+            return;
+        }
+        if (width > height) {
+            mAspectRatio = (float) width / height;
+        } else {
+            mAspectRatio = (float) height / width;
+        }
+        mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX);
+    }
+
+    public int getPreviewWidth() {
+        return mPreviewWidth;
+    }
+
+    public int getPreviewHeight() {
+        return mPreviewHeight;
+    }
+
+    public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+        setTransformMatrix(width, height);
+    }
+
+    private void setTransformMatrix(int width, int height) {
+        mMatrix = mTextureView.getTransform(mMatrix);
+        int orientation = Util.getDisplayRotation(mActivity);
+        float scaleX = 1f, scaleY = 1f;
+        float scaledTextureWidth, scaledTextureHeight;
+        if (width > height) {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height * mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int)(width / mAspectRatio));
+        } else {
+            scaledTextureWidth = Math.max(width,
+                    (int) (height / mAspectRatio));
+            scaledTextureHeight = Math.max(height,
+                    (int) (width * mAspectRatio));
+        }
+
+        if (mSurfaceTextureUncroppedWidth != scaledTextureWidth ||
+                mSurfaceTextureUncroppedHeight != scaledTextureHeight) {
+            mSurfaceTextureUncroppedWidth = scaledTextureWidth;
+            mSurfaceTextureUncroppedHeight = scaledTextureHeight;
+        }
+        scaleX = scaledTextureWidth / width;
+        scaleY = scaledTextureHeight / height;
+        mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2);
+        mTextureView.setTransform(mMatrix);
+
+        if (mSurfaceView != null && mSurfaceView.getVisibility() == View.VISIBLE) {
+            LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
+            lp.width = (int) mSurfaceTextureUncroppedWidth;
+            lp.height = (int) mSurfaceTextureUncroppedHeight;
+            lp.gravity = Gravity.CENTER;
+            mSurfaceView.requestLayout();
+        }
+    }
+
+    public void hideUI() {
+        mCameraControls.setVisibility(View.INVISIBLE);
+        mSwitcher.closePopup();
+    }
+
+    public void showUI() {
+        mCameraControls.setVisibility(View.VISIBLE);
+    }
+
+    public void hideSwitcher() {
+        mSwitcher.closePopup();
+        mSwitcher.setVisibility(View.INVISIBLE);
+    }
+
+    public void showSwitcher() {
+        mSwitcher.setVisibility(View.VISIBLE);
+    }
+
+    public boolean collapseCameraControls() {
+        boolean ret = false;
+        if (mPopup != null) {
+            dismissPopup(false);
+            ret = true;
+        }
+        return ret;
+    }
+
+    public boolean removeTopLevelPopup() {
+        if (mPopup != null) {
+            dismissPopup(true);
+            return true;
+        }
+        return false;
+    }
+
+    public void enableCameraControls(boolean enable) {
+        if (mGestures != null) {
+            mGestures.setZoomOnly(!enable);
+        }
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+        }
+    }
+
+    public void overrideSettings(final String... keyvalues) {
+        mVideoMenu.overrideSettings(keyvalues);
+    }
+
+    public void setOrientationIndicator(int orientation, boolean animation) {
+        // We change the orientation of the linearlayout only for phone UI
+        // because when in portrait the width is not enough.
+        if (mLabelsLinearLayout != null) {
+            if (((orientation / 90) & 1) == 0) {
+                mLabelsLinearLayout.setOrientation(LinearLayout.VERTICAL);
+            } else {
+                mLabelsLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
+            }
+        }
+        mRecordingTimeRect.setOrientation(0, animation);
+    }
+
+    public SurfaceHolder getSurfaceHolder() {
+        return mSurfaceView.getHolder();
+    }
+
+    public void hideSurfaceView() {
+        mSurfaceView.setVisibility(View.GONE);
+        mTextureView.setVisibility(View.VISIBLE);
+        setTransformMatrix(mPreviewWidth, mPreviewHeight);
+    }
+
+    public void showSurfaceView() {
+        mSurfaceView.setVisibility(View.VISIBLE);
+        mTextureView.setVisibility(View.GONE);
+        setTransformMatrix(mPreviewWidth, mPreviewHeight);
+    }
+
+    private void initializeOverlay() {
+        mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+        if (mPieRenderer == null) {
+            mPieRenderer = new PieRenderer(mActivity);
+            mVideoMenu = new VideoMenu(mActivity, this, mPieRenderer);
+            mPieRenderer.setPieListener(this);
+        }
+        mRenderOverlay.addRenderer(mPieRenderer);
+        if (mZoomRenderer == null) {
+            mZoomRenderer = new ZoomRenderer(mActivity);
+        }
+        mRenderOverlay.addRenderer(mZoomRenderer);
+        if (mGestures == null) {
+            mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
+            mRenderOverlay.setGestures(mGestures);
+        }
+        mGestures.setRenderOverlay(mRenderOverlay);
+
+        mPreviewThumb = mActivity.findViewById(R.id.preview_thumb);
+        mPreviewThumb.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // TODO: Go to filmstrip view
+            }
+        });
+    }
+
+    public void setPrefChangedListener(OnPreferenceChangedListener listener) {
+        mVideoMenu.setListener(listener);
+    }
+
+    private void initializeMiscControls() {
+        mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image);
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+        mShutterButton.setOnShutterButtonListener(mController);
+        mShutterButton.setVisibility(View.VISIBLE);
+        mShutterButton.requestFocus();
+        mShutterButton.enableTouch(true);
+        mRecordingTimeView = (TextView) mRootView.findViewById(R.id.recording_time);
+        mRecordingTimeRect = (RotateLayout) mRootView.findViewById(R.id.recording_time_rect);
+        mTimeLapseLabel = mRootView.findViewById(R.id.time_lapse_label);
+        // The R.id.labels can only be found in phone layout.
+        // That is, mLabelsLinearLayout should be null in tablet layout.
+        mLabelsLinearLayout = (LinearLayout) mRootView.findViewById(R.id.labels);
+    }
+
+    public void updateOnScreenIndicators(Parameters param, ComboPreferences prefs) {
+      mOnScreenIndicators.updateFlashOnScreenIndicator(param.getFlashMode());
+      boolean location = RecordLocationPreference.get(
+              prefs, mActivity.getContentResolver());
+      mOnScreenIndicators.updateLocationIndicator(location);
+
+    }
+
+    public void setAspectRatio(double ratio) {
+      //  mPreviewFrameLayout.setAspectRatio(ratio);
+    }
+
+    public void showTimeLapseUI(boolean enable) {
+        if (mTimeLapseLabel != null) {
+            mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    private void openMenu() {
+        if (mPieRenderer != null) {
+            mPieRenderer.showInCenter();
+        }
+    }
+
+    public void showPopup(AbstractSettingPopup popup) {
+        hideUI();
+        mBlocker.setVisibility(View.INVISIBLE);
+        setShowMenu(false);
+        mPopup = popup;
+        mPopup.setVisibility(View.VISIBLE);
+        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT);
+        lp.gravity = Gravity.CENTER;
+        ((FrameLayout) mRootView).addView(mPopup, lp);
+    }
+
+    public void dismissPopup(boolean topLevelOnly) {
+        dismissPopup(topLevelOnly, true);
+    }
+
+    public void dismissPopup(boolean topLevelPopupOnly, boolean fullScreen) {
+        // In review mode, we do not want to bring up the camera UI
+        if (mController.isInReviewMode()) return;
+
+        if (fullScreen) {
+            showUI();
+            mBlocker.setVisibility(View.VISIBLE);
+        }
+        setShowMenu(fullScreen);
+        if (mPopup != null) {
+            ((FrameLayout) mRootView).removeView(mPopup);
+            mPopup = null;
+        }
+        mVideoMenu.popupDismissed(topLevelPopupOnly);
+    }
+
+    public void onShowSwitcherPopup() {
+        hidePieRenderer();
+    }
+
+    public boolean hidePieRenderer() {
+        if (mPieRenderer != null && mPieRenderer.showsItems()) {
+            mPieRenderer.hide();
+            return true;
+        }
+        return false;
+    }
+
+    // disable preview gestures after shutter is pressed
+    public void setShutterPressed(boolean pressed) {
+        if (mGestures == null) return;
+        mGestures.setEnabled(!pressed);
+    }
+
+    public void enableShutter(boolean enable) {
+        if (mShutterButton != null) {
+            mShutterButton.setEnabled(enable);
+        }
+    }
+
+    // PieListener
+    @Override
+    public void onPieOpened(int centerX, int centerY) {
+        setSwipingEnabled(false);
+        dismissPopup(false, true);
+    }
+
+    @Override
+    public void onPieClosed() {
+        setSwipingEnabled(true);
+    }
+
+    public void setSwipingEnabled(boolean enable) {
+        mActivity.setSwipingEnabled(enable);
+    }
+
+    public void showPreviewBorder(boolean enable) {
+       // TODO: mPreviewFrameLayout.showBorder(enable);
+    }
+
+    // SingleTapListener
+    // Preview area is touched. Take a picture.
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+        mController.onSingleTapUp(view, x, y);
+    }
+
+    public void showRecordingUI(boolean recording, boolean zoomSupported) {
+        mMenuButton.setVisibility(recording ? View.GONE : View.VISIBLE);
+        mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE);
+        if (recording) {
+            mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording);
+            hideSwitcher();
+            mRecordingTimeView.setText("");
+            mRecordingTimeView.setVisibility(View.VISIBLE);
+            // The camera is not allowed to be accessed in older api levels during
+            // recording. It is therefore necessary to hide the zoom UI on older
+            // platforms.
+            // See the documentation of android.media.MediaRecorder.start() for
+            // further explanation.
+            if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) {
+                // TODO: disable zoom UI here.
+            }
+        } else {
+            mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+            showSwitcher();
+            mRecordingTimeView.setVisibility(View.GONE);
+            if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) {
+                // TODO: enable zoom UI here.
+            }
+        }
+    }
+
+    public void showReviewImage(Bitmap bitmap) {
+        mReviewImage.setImageBitmap(bitmap);
+        mReviewImage.setVisibility(View.VISIBLE);
+    }
+
+    public void showReviewControls() {
+        Util.fadeOut(mShutterButton);
+        Util.fadeIn(mReviewDoneButton);
+        Util.fadeIn(mReviewPlayButton);
+        mReviewImage.setVisibility(View.VISIBLE);
+        mMenuButton.setVisibility(View.GONE);
+        mOnScreenIndicators.setVisibility(View.GONE);
+    }
+
+    public void hideReviewUI() {
+        mReviewImage.setVisibility(View.GONE);
+        mShutterButton.setEnabled(true);
+        mMenuButton.setVisibility(View.VISIBLE);
+        mOnScreenIndicators.setVisibility(View.VISIBLE);
+        Util.fadeOut(mReviewDoneButton);
+        Util.fadeOut(mReviewPlayButton);
+        Util.fadeIn(mShutterButton);
+    }
+
+    private void setShowMenu(boolean show) {
+        if (mOnScreenIndicators != null) {
+            mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+        if (mMenuButton != null) {
+            mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    public void onSwitchMode(boolean toCamera) {
+        if (toCamera) {
+            showUI();
+        } else {
+            hideUI();
+        }
+        if (mGestures != null) {
+            mGestures.setEnabled(toCamera);
+        }
+        if (mPopup != null) {
+            dismissPopup(false, toCamera);
+        }
+        if (mRenderOverlay != null) {
+            // this can not happen in capture mode
+            mRenderOverlay.setVisibility(toCamera ? View.VISIBLE : View.GONE);
+        }
+        setShowMenu(toCamera);
+    }
+
+    public void initializePopup(PreferenceGroup pref) {
+        mVideoMenu.initialize(pref);
+    }
+
+    public void initializeZoom(Parameters param) {
+        if (param == null || !param.isZoomSupported()) {
+            mGestures.setZoomEnabled(false);
+            return;
+        }
+        mGestures.setZoomEnabled(true);
+        mZoomMax = param.getMaxZoom();
+        mZoomRatios = param.getZoomRatios();
+        // Currently we use immediate zoom for fast zooming to get better UX and
+        // there is no plan to take advantage of the smooth zoom.
+        mZoomRenderer.setZoomMax(mZoomMax);
+        mZoomRenderer.setZoom(param.getZoom());
+        mZoomRenderer.setZoomValue(mZoomRatios.get(param.getZoom()));
+        mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+    }
+
+    public void clickShutter() {
+        mShutterButton.performClick();
+    }
+
+    public void pressShutter(boolean pressed) {
+        mShutterButton.setPressed(pressed);
+    }
+
+    public View getShutterButton() {
+        return mShutterButton;
+    }
+
+    public void setRecordingTime(String text) {
+        mRecordingTimeView.setText(text);
+    }
+
+    public void setRecordingTimeTextColor(int color) {
+        mRecordingTimeView.setTextColor(color);
+    }
+
+    public boolean isVisible() {
+        return mTextureView.getVisibility() == View.VISIBLE;
+    }
+
+    public void onDisplayChanged() {
+        mCameraControls.checkLayoutFlip();
+        mController.updateCameraOrientation();
+    }
+
+    /**
+     * Enable or disable the preview thumbnail for click events.
+     */
+    public void enablePreviewThumb(boolean enabled) {
+        if (enabled) {
+            mPreviewThumb.setVisibility(View.VISIBLE);
+        } else {
+            mPreviewThumb.setVisibility(View.GONE);
+        }
+    }
+
+    private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+        @Override
+        public void onZoomValueChanged(int index) {
+            int newZoom = mController.onZoomChanged(index);
+            if (mZoomRenderer != null) {
+                mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom));
+            }
+        }
+
+        @Override
+        public void onZoomStart() {
+        }
+
+        @Override
+        public void onZoomEnd() {
+        }
+    }
+
+    public SurfaceTexture getSurfaceTexture() {
+        return mSurfaceTexture;
+    }
+
+    // SurfaceTexture callbacks
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+        mSurfaceTexture = surface;
+        mController.onPreviewUIReady();
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        mSurfaceTexture = null;
+        mController.onPreviewUIDestroyed();
+        Log.d(TAG, "surfaceTexture is destroyed");
+        return true;
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+    }
+
+    // SurfaceHolder callbacks
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        Log.v(TAG, "Surface changed. width=" + width + ". height=" + height);
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        Log.v(TAG, "Surface created");
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        Log.v(TAG, "Surface destroyed");
+        mController.stopPreview();
+    }
+}
diff --git a/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java b/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java
new file mode 100644
index 0000000..66c5585
--- /dev/null
+++ b/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * An abstract {@link LocalDataAdapter} implementation to wrap another
+ * {@link LocalDataAdapter}. All implementations related to data id is not
+ * addressed in this abstract class since wrapping another data adapter
+ * surely makes things different for data id.
+ *
+ * @see FixedFirstDataAdapter
+ * @see FixedLastDataAdapter
+ */
+public abstract class AbstractLocalDataAdapterWrapper implements LocalDataAdapter {
+
+    protected final LocalDataAdapter mAdapter;
+    protected int mSuggestedWidth;
+    protected int mSuggestedHeight;
+
+    /**
+     * Constructor.
+     *
+     * @param wrappedAdapter  The {@link LocalDataAdapter} to be wrapped.
+     */
+    AbstractLocalDataAdapterWrapper(LocalDataAdapter wrappedAdapter) {
+        if (wrappedAdapter == null) {
+            throw new AssertionError("data adapter is null");
+        }
+        mAdapter = wrappedAdapter;
+    }
+
+    @Override
+    public void suggestViewSizeBound(int w, int h) {
+        mSuggestedWidth = w;
+        mSuggestedHeight = h;
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        mAdapter.setListener(listener);
+    }
+
+    @Override
+    public void requestLoad(ContentResolver resolver) {
+        mAdapter.requestLoad(resolver);
+    }
+
+    @Override
+    public void addNewVideo(ContentResolver resolver, Uri uri) {
+        mAdapter.addNewVideo(resolver, uri);
+    }
+
+    @Override
+    public void addNewPhoto(ContentResolver resolver, Uri uri) {
+        mAdapter.addNewPhoto(resolver, uri);
+    }
+
+    @Override
+    public void flush() {
+        mAdapter.flush();
+    }
+
+    @Override
+    public boolean executeDeletion(Context context) {
+        return mAdapter.executeDeletion(context);
+    }
+
+    @Override
+    public boolean undoDataRemoval() {
+        return mAdapter.undoDataRemoval();
+    }
+}
diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java
new file mode 100644
index 0000000..3605f71
--- /dev/null
+++ b/src/com/android/camera/data/CameraDataAdapter.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.View;
+
+import com.android.camera.Storage;
+import com.android.camera.ui.FilmStripView.ImageData;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A {@link LocalDataAdapter} that provides data in the camera folder.
+ */
+public class CameraDataAdapter implements LocalDataAdapter {
+    private static final String TAG = CameraDataAdapter.class.getSimpleName();
+
+    private static final int DEFAULT_DECODE_SIZE = 3000;
+    private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" };
+
+    private List<LocalData> mImages;
+
+    private Listener mListener;
+    private Drawable mPlaceHolder;
+
+    private int mSuggestedWidth = DEFAULT_DECODE_SIZE;
+    private int mSuggestedHeight = DEFAULT_DECODE_SIZE;
+
+    private LocalData mLocalDataToDelete;
+
+    public CameraDataAdapter(Drawable placeHolder) {
+        mPlaceHolder = placeHolder;
+    }
+
+    @Override
+    public void requestLoad(ContentResolver resolver) {
+        QueryTask qtask = new QueryTask();
+        qtask.execute(resolver);
+    }
+
+    @Override
+    public int getTotalNumber() {
+        if (mImages == null) {
+            return 0;
+        }
+        return mImages.size();
+    }
+
+    @Override
+    public ImageData getImageData(int id) {
+        return getData(id);
+    }
+
+    @Override
+    public void suggestViewSizeBound(int w, int h) {
+        if (w <= 0 || h <= 0) {
+            mSuggestedWidth  = mSuggestedHeight = DEFAULT_DECODE_SIZE;
+        } else {
+            mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE);
+            mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE);
+        }
+    }
+
+    @Override
+    public View getView(Context c, int dataID) {
+        if (mImages == null) {
+            return null;
+        }
+        if (dataID >= mImages.size() || dataID < 0) {
+            return null;
+        }
+
+        return mImages.get(dataID).getView(
+                c, mSuggestedWidth, mSuggestedHeight,
+                mPlaceHolder.getConstantState().newDrawable());
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        mListener = listener;
+        if (mImages != null) {
+            mListener.onDataLoaded();
+        }
+    }
+
+    @Override
+    public void onDataFullScreen(int dataID, boolean fullScreen) {
+        if (dataID < mImages.size() && dataID >= 0) {
+            mImages.get(dataID).onFullScreen(fullScreen);
+        }
+    }
+
+    @Override
+    public void onDataCentered(int dataID, boolean centered) {
+        // do nothing.
+    }
+
+    @Override
+    public boolean canSwipeInFullScreen(int dataID) {
+        if (dataID < mImages.size() && dataID > 0) {
+            return mImages.get(dataID).canSwipeInFullScreen();
+        }
+        return true;
+    }
+
+    @Override
+    public void removeData(Context c, int dataID) {
+        if (dataID >= mImages.size()) return;
+        LocalData d = mImages.remove(dataID);
+        // Delete previously removed data first.
+        executeDeletion(c);
+        mLocalDataToDelete = d;
+        mListener.onDataRemoved(dataID, d);
+    }
+
+    private void insertData(LocalData data) {
+        if (mImages == null) {
+            mImages = new ArrayList<LocalData>();
+        }
+
+        // Since this function is mostly for adding the newest data,
+        // a simple linear search should yield the best performance over a
+        // binary search.
+        int pos = 0;
+        Comparator<LocalData> comp = new LocalData.NewestFirstComparator();
+        for (; pos < mImages.size()
+                && comp.compare(data, mImages.get(pos)) > 0; pos++);
+        mImages.add(pos, data);
+        if (mListener != null) {
+            mListener.onDataInserted(pos, data);
+        }
+    }
+
+    @Override
+    public void addNewVideo(ContentResolver cr, Uri uri) {
+        Cursor c = cr.query(uri,
+                LocalData.Video.QUERY_PROJECTION,
+                MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+                LocalData.Video.QUERY_ORDER);
+        if (c != null && c.moveToFirst()) {
+            insertData(LocalData.Video.buildFromCursor(c));
+        }
+    }
+
+    @Override
+    public void addNewPhoto(ContentResolver cr, Uri uri) {
+        Cursor c = cr.query(uri,
+                LocalData.Photo.QUERY_PROJECTION,
+                MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+                LocalData.Photo.QUERY_ORDER);
+        if (c != null && c.moveToFirst()) {
+            insertData(LocalData.Photo.buildFromCursor(c));
+        }
+    }
+
+    @Override
+    public int findDataByContentUri(Uri uri) {
+        // TODO: find the data.
+        return -1;
+    }
+
+    @Override
+    public boolean undoDataRemoval() {
+        if (mLocalDataToDelete == null) return false;
+        LocalData d = mLocalDataToDelete;
+        mLocalDataToDelete = null;
+        insertData(d);
+        return true;
+    }
+
+    @Override
+    public boolean executeDeletion(Context c) {
+        if (mLocalDataToDelete == null) return false;
+
+        DeletionTask task = new DeletionTask(c);
+        task.execute(mLocalDataToDelete);
+        mLocalDataToDelete = null;
+        return true;
+    }
+
+    @Override
+    public void flush() {
+        replaceData(null);
+    }
+
+    private LocalData getData(int id) {
+        if (mImages == null || id >= mImages.size() || id < 0) {
+            return null;
+        }
+        return mImages.get(id);
+    }
+
+    // Update all the data but keep the camera data if already set.
+    private void replaceData(List<LocalData> list) {
+        boolean changed = (list != mImages);
+        LocalData cameraData = null;
+        if (mImages != null && mImages.size() > 0) {
+            cameraData = mImages.get(0);
+            if (cameraData.getType() != ImageData.TYPE_CAMERA_PREVIEW) {
+                cameraData = null;
+            }
+        }
+
+        mImages = list;
+        if (cameraData != null) {
+            // camera view exists, so we make sure at least 1 data is in the list.
+            if (mImages == null) {
+                mImages = new ArrayList<LocalData>();
+            }
+            mImages.add(0, cameraData);
+            if (mListener != null) {
+                // Only the camera data is not changed, everything else is changed.
+                mListener.onDataUpdated(new UpdateReporter() {
+                    @Override
+                    public boolean isDataRemoved(int id) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean isDataUpdated(int id) {
+                        if (id == 0) return false;
+                        return true;
+                    }
+                });
+            }
+        } else {
+            // both might be null.
+            if (changed) {
+                mListener.onDataLoaded();
+            }
+        }
+    }
+
+    private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalData>> {
+        @Override
+        protected List<LocalData> doInBackground(ContentResolver... resolver) {
+            List<LocalData> l = new ArrayList<LocalData>();
+            // Photos
+            Cursor c = resolver[0].query(
+                    LocalData.Photo.CONTENT_URI,
+                    LocalData.Photo.QUERY_PROJECTION,
+                    MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+                    LocalData.Photo.QUERY_ORDER);
+            if (c != null && c.moveToFirst()) {
+                // build up the list.
+                while (true) {
+                    LocalData data = LocalData.Photo.buildFromCursor(c);
+                    if (data != null) {
+                        l.add(data);
+                    } else {
+                        Log.e(TAG, "Error loading data:"
+                                + c.getString(LocalData.Photo.COL_DATA));
+                    }
+                    if (c.isLast()) {
+                        break;
+                    }
+                    c.moveToNext();
+                }
+            }
+            if (c != null) {
+                c.close();
+            }
+
+            c = resolver[0].query(
+                    LocalData.Video.CONTENT_URI,
+                    LocalData.Video.QUERY_PROJECTION,
+                    MediaStore.Video.Media.DATA + " like ? ", CAMERA_PATH,
+                    LocalData.Video.QUERY_ORDER);
+            if (c != null && c.moveToFirst()) {
+                // build up the list.
+                c.moveToFirst();
+                while (true) {
+                    LocalData data = LocalData.Video.buildFromCursor(c);
+                    if (data != null) {
+                        l.add(data);
+                    } else {
+                        Log.e(TAG, "Error loading data:"
+                                + c.getString(LocalData.Video.COL_DATA));
+                    }
+                    if (!c.isLast()) {
+                        c.moveToNext();
+                    } else {
+                        break;
+                    }
+                }
+            }
+            if (c != null) {
+                c.close();
+            }
+
+            if (l.size() == 0) return null;
+
+            Collections.sort(l, new LocalData.NewestFirstComparator());
+            return l;
+        }
+
+        @Override
+        protected void onPostExecute(List<LocalData> l) {
+            replaceData(l);
+        }
+    }
+
+    private class DeletionTask extends AsyncTask<LocalData, Void, Void> {
+        Context mContext;
+
+        DeletionTask(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        protected Void doInBackground(LocalData... data) {
+            for (int i = 0; i < data.length; i++) {
+                if (!data[i].isDataActionSupported(LocalData.ACTION_DELETE)) {
+                    Log.v(TAG, "Deletion is not supported:" + data[i]);
+                    continue;
+                }
+                data[i].delete(mContext);
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/camera/data/CameraPreviewData.java b/src/com/android/camera/data/CameraPreviewData.java
new file mode 100644
index 0000000..8f8e213
--- /dev/null
+++ b/src/com/android/camera/data/CameraPreviewData.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.view.View;
+
+import com.android.camera.ui.FilmStripView.ImageData;
+
+/**
+ * A class implementing {@link LocalData} to represent a camera preview.
+ */
+public class CameraPreviewData extends LocalData.LocalViewData {
+
+    private boolean mPreviewLocked;
+
+    /**
+     * Constructor.
+     *
+     * @param v      The {@link android.view.View} for camera preview.
+     * @param width  The width of the camera preview.
+     * @param height The height of the camera preview.
+     */
+    public CameraPreviewData(View v, int width, int height) {
+        super(v, width, height, -1, -1);
+        mPreviewLocked = true;
+    }
+
+    @Override
+    public int getType() {
+        return ImageData.TYPE_CAMERA_PREVIEW;
+    }
+
+    @Override
+    public boolean canSwipeInFullScreen() {
+        return !mPreviewLocked;
+    }
+
+    /**
+     * Locks the camera preview. When the camera preview is locked, swipe
+     * to film strip is not allowed. One case is when the video recording
+     * is in progress.
+     *
+     * @param lock {@code true} if the preview should be locked. {@code false}
+     *             otherwise.
+     */
+    public void lockPreview(boolean lock) {
+        mPreviewLocked = lock;
+    }
+}
diff --git a/src/com/android/camera/data/FixedFirstDataAdapter.java b/src/com/android/camera/data/FixedFirstDataAdapter.java
new file mode 100644
index 0000000..34ba0a1
--- /dev/null
+++ b/src/com/android/camera/data/FixedFirstDataAdapter.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.View;
+
+import com.android.camera.ui.FilmStripView;
+import com.android.camera.ui.FilmStripView.DataAdapter;
+import com.android.camera.ui.FilmStripView.ImageData;
+
+/**
+ * A {@link LocalDataAdapter} which puts a {@link LocalData} fixed at the first
+ * position. It's done by combining a {@link LocalData} and another
+ * {@link LocalDataAdapter}.
+ */
+public class FixedFirstDataAdapter extends AbstractLocalDataAdapterWrapper
+        implements DataAdapter.Listener {
+
+    private final LocalData mFirstData;
+    private Listener mListener;
+
+    /**
+     * Constructor.
+     *
+     * @param wrappedAdapter The {@link LocalDataAdapter} to be wrapped.
+     * @param firstData      The {@link LocalData} to be placed at the first
+     *                       position.
+     */
+    public FixedFirstDataAdapter(
+            LocalDataAdapter wrappedAdapter,
+            LocalData firstData) {
+        super(wrappedAdapter);
+        if (firstData == null) {
+            throw new AssertionError("data is null");
+        }
+        mFirstData = firstData;
+    }
+
+    @Override
+    public void removeData(Context context, int dataID) {
+        if (dataID > 0) {
+            mAdapter.removeData(context, dataID - 1);
+        }
+    }
+
+    @Override
+    public int findDataByContentUri(Uri uri) {
+        int pos = mAdapter.findDataByContentUri(uri);
+        if (pos != -1) {
+            return pos + 1;
+        }
+        return -1;
+    }
+
+    @Override
+    public int getTotalNumber() {
+        return (mAdapter.getTotalNumber() + 1);
+    }
+
+    @Override
+    public View getView(Context context, int dataID) {
+        if (dataID == 0) {
+            return mFirstData.getView(
+                    context, mSuggestedWidth, mSuggestedHeight, null);
+        }
+        return mAdapter.getView(context, dataID - 1);
+    }
+
+    @Override
+    public ImageData getImageData(int dataID) {
+        if (dataID == 0) {
+            return mFirstData;
+        }
+        return mAdapter.getImageData(dataID - 1);
+    }
+
+    @Override
+    public void onDataFullScreen(int dataID, boolean fullScreen) {
+        if (dataID == 0) {
+            mFirstData.onFullScreen(fullScreen);
+        } else {
+            mAdapter.onDataFullScreen(dataID - 1, fullScreen);
+        }
+    }
+
+    @Override
+    public void onDataCentered(int dataID, boolean centered) {
+        if (dataID != 0) {
+            mAdapter.onDataCentered(dataID, centered);
+        } else {
+            // TODO: notify the data
+        }
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        mListener = listener;
+        mAdapter.setListener((listener == null) ? null : this);
+    }
+
+    @Override
+    public boolean canSwipeInFullScreen(int dataID) {
+        if (dataID == 0) {
+            return mFirstData.canSwipeInFullScreen();
+        }
+        return mAdapter.canSwipeInFullScreen(dataID - 1);
+    }
+
+    @Override
+    public void onDataLoaded() {
+        mListener.onDataLoaded();
+    }
+
+    @Override
+    public void onDataUpdated(final UpdateReporter reporter) {
+        mListener.onDataUpdated(new UpdateReporter() {
+            @Override
+            public boolean isDataRemoved(int dataID) {
+                return reporter.isDataRemoved(dataID + 1);
+            }
+
+            @Override
+            public boolean isDataUpdated(int dataID) {
+                return reporter.isDataUpdated(dataID + 1);
+            }
+        });
+    }
+
+    @Override
+    public void onDataInserted(int dataID, ImageData data) {
+        mListener.onDataInserted(dataID + 1, data);
+    }
+
+    @Override
+    public void onDataRemoved(int dataID, ImageData data) {
+        mListener.onDataRemoved(dataID + 1, data);
+    }
+}
diff --git a/src/com/android/camera/data/FixedLastDataAdapter.java b/src/com/android/camera/data/FixedLastDataAdapter.java
new file mode 100644
index 0000000..16c047d
--- /dev/null
+++ b/src/com/android/camera/data/FixedLastDataAdapter.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.View;
+
+import com.android.camera.ui.FilmStripView;
+
+/**
+ * A {@link LocalDataAdapter} which puts a {@link LocalData} fixed at the last
+ * position. It's done by combining a {@link LocalData} and another
+ * {@link LocalDataAdapter}.
+ */
+public class FixedLastDataAdapter extends AbstractLocalDataAdapterWrapper {
+
+    private final LocalData mLastData;
+
+    /**
+     * Constructor.
+     *
+     * @param wrappedAdapter  The {@link LocalDataAdapter} to be wrapped.
+     * @param lastData       The {@link LocalData} to be placed at the last position.
+     */
+    public FixedLastDataAdapter(
+            LocalDataAdapter wrappedAdapter,
+            LocalData lastData) {
+        super(wrappedAdapter);
+        if (lastData == null) {
+            throw new AssertionError("data is null");
+        }
+        mLastData = lastData;
+    }
+
+    @Override
+    public void removeData(Context context, int dataID) {
+        if (dataID < mAdapter.getTotalNumber()) {
+            mAdapter.removeData(context, dataID);
+        }
+    }
+
+    @Override
+    public int findDataByContentUri(Uri uri) {
+        return mAdapter.findDataByContentUri(uri);
+    }
+
+    @Override
+    public int getTotalNumber() {
+        return mAdapter.getTotalNumber() + 1;
+    }
+
+    @Override
+    public View getView(Context context, int dataID) {
+        int totalNumber = mAdapter.getTotalNumber();
+
+        if (dataID < totalNumber) {
+            return mAdapter.getView(context, dataID);
+        } else if (dataID == totalNumber) {
+            return mLastData.getView(context,
+                    mSuggestedWidth, mSuggestedHeight, null);
+        }
+
+        return null;
+    }
+
+    @Override
+    public FilmStripView.ImageData getImageData(int dataID) {
+        int totalNumber = mAdapter.getTotalNumber();
+
+        if (dataID < totalNumber) {
+            return mAdapter.getImageData(dataID);
+        } else if (dataID == totalNumber) {
+            return mLastData;
+        }
+        return null;
+    }
+
+    @Override
+    public void onDataFullScreen(int dataID, boolean fullScreen) {
+        int totalNumber = mAdapter.getTotalNumber();
+
+        if (dataID < totalNumber) {
+            mAdapter.onDataFullScreen(dataID, fullScreen);
+        } else if (dataID == totalNumber) {
+            mLastData.onFullScreen(fullScreen);
+        }
+    }
+
+    @Override
+    public void onDataCentered(int dataID, boolean centered) {
+        int totalNumber = mAdapter.getTotalNumber();
+
+        if (dataID < totalNumber) {
+            mAdapter.onDataCentered(dataID, centered);
+        } else if (dataID == totalNumber) {
+            // TODO: notify the data
+        }
+    }
+
+    @Override
+    public boolean canSwipeInFullScreen(int dataID) {
+        int totalNumber = mAdapter.getTotalNumber();
+
+        if (dataID < totalNumber) {
+            return mAdapter.canSwipeInFullScreen(dataID);
+        } else if (dataID == totalNumber) {
+            return mLastData.canSwipeInFullScreen();
+        }
+        return false;
+    }
+}
+
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java
new file mode 100644
index 0000000..efccfe3
--- /dev/null
+++ b/src/com/android/camera/data/LocalData.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.camera.Util;
+import com.android.camera.data.PanoramaMetadataLoader.PanoramaMetadataCallback;
+import com.android.camera.ui.FilmStripView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+import java.io.File;
+import java.util.Comparator;
+import java.util.Date;
+
+/**
+ * An abstract interface that represents the local media data. Also implements
+ * Comparable interface so we can sort in DataAdapter.
+ */
+public interface LocalData extends FilmStripView.ImageData {
+    static final String TAG = "CAM_LocalData";
+
+    public static final int ACTION_NONE = 0;
+    public static final int ACTION_PLAY = 1;
+    public static final int ACTION_DELETE = (1 << 1);
+
+    View getView(Context c, int width, int height, Drawable placeHolder);
+    long getDateTaken();
+    long getDateModified();
+    String getTitle();
+    boolean isDataActionSupported(int action);
+    boolean delete(Context c);
+    void onFullScreen(boolean fullScreen);
+    boolean canSwipeInFullScreen();
+    String getPath();
+
+    static class NewestFirstComparator implements Comparator<LocalData> {
+
+        /** Compare taken/modified date of LocalData in descent order to make
+         newer data in the front.
+         The negative numbers here are always considered "bigger" than
+         positive ones. Thus, if any one of the numbers is negative, the logic
+         is reversed. */
+        private static int compareDate(long v1, long v2) {
+            if (v1 >= 0 && v2 >= 0) {
+                return ((v1 < v2) ? 1 : ((v1 > v2) ? -1 : 0));
+            }
+            return ((v2 < v1) ? 1 : ((v2 > v1) ? -1 : 0));
+        }
+
+        @Override
+        public int compare(LocalData d1, LocalData d2) {
+            int cmp = compareDate(d1.getDateTaken(), d2.getDateTaken());
+            if (cmp == 0) {
+                cmp = compareDate(d1.getDateModified(), d2.getDateModified());
+            }
+            if (cmp == 0) {
+                cmp = d1.getTitle().compareTo(d2.getTitle());
+            }
+            return cmp;
+        }
+    }
+
+    // Implementations below.
+
+    /**
+<<<<<<< HEAD
+     * A base class for all the local media files. The bitmap is loaded in
+     * background thread. Subclasses should implement their own background
+     * loading thread by subclassing BitmapLoadTask and overriding
+     * doInBackground() to return a bitmap.
+=======
+     * A base class for all the local media files. The bitmap is loaded in background
+     * thread. Subclasses should implement their own background loading thread by
+     * sub-classing BitmapLoadTask and overriding doInBackground() to return a bitmap.
+>>>>>>> Add LocalDataAdapter and wrappers.
+     */
+    abstract static class LocalMediaData implements LocalData {
+        protected long id;
+        protected String title;
+        protected String mimeType;
+        protected long dateTaken;
+        protected long dateModified;
+        protected String path;
+        // width and height should be adjusted according to orientation.
+        protected int width;
+        protected int height;
+
+        /** The panorama metadata information of this media data. */
+        private PanoramaMetadata mPanoramaMetadata;
+
+        /** Used to load photo sphere metadata from image files. */
+        private PanoramaMetadataLoader mPanoramaMetadataLoader = null;
+
+        // true if this data has a corresponding visible view.
+        protected Boolean mUsing = false;
+
+        @Override
+        public long getDateTaken() {
+            return dateTaken;
+        }
+
+        @Override
+        public long getDateModified() {
+            return dateModified;
+        }
+
+        @Override
+        public String getTitle() {
+            return new String(title);
+        }
+
+        @Override
+        public int getWidth() {
+            return width;
+        }
+
+        @Override
+        public int getHeight() {
+            return height;
+        }
+
+        @Override
+        public String getPath() {
+            return path;
+        }
+
+        @Override
+        public boolean isUIActionSupported(int action) {
+            return false;
+        }
+
+        @Override
+        public boolean isDataActionSupported(int action) {
+            return false;
+        }
+
+        @Override
+        public boolean delete(Context ctx) {
+            File f = new File(path);
+            return f.delete();
+        }
+
+        @Override
+        public void viewPhotoSphere(PanoramaViewHelper helper) {
+            helper.showPanorama(getContentUri());
+        }
+
+        @Override
+        public void isPhotoSphere(Context context, final PanoramaSupportCallback callback) {
+            // If we already have metadata, use it.
+            if (mPanoramaMetadata != null) {
+                callback.panoramaInfoAvailable(mPanoramaMetadata.mUsePanoramaViewer,
+                        mPanoramaMetadata.mIsPanorama360);
+            }
+
+            // Otherwise prepare a loader, if we don't have one already.
+            if (mPanoramaMetadataLoader == null) {
+                mPanoramaMetadataLoader = new PanoramaMetadataLoader(getContentUri());
+            }
+
+            // Load the metadata asynchronously.
+            mPanoramaMetadataLoader.getPanoramaMetadata(context, new PanoramaMetadataCallback() {
+                @Override
+                public void onPanoramaMetadataLoaded(PanoramaMetadata metadata) {
+                    // Store the metadata and remove the loader to free up space.
+                    mPanoramaMetadata = metadata;
+                    mPanoramaMetadataLoader = null;
+                    callback.panoramaInfoAvailable(metadata.mUsePanoramaViewer,
+                            metadata.mIsPanorama360);
+                }
+            });
+        }
+
+        @Override
+        public void onFullScreen(boolean fullScreen) {
+            // do nothing.
+        }
+
+        @Override
+        public boolean canSwipeInFullScreen() {
+            return true;
+        }
+
+        protected ImageView fillImageView(Context ctx, ImageView v,
+                int decodeWidth, int decodeHeight, Drawable placeHolder) {
+            v.setScaleType(ImageView.ScaleType.FIT_XY);
+            v.setImageDrawable(placeHolder);
+
+            BitmapLoadTask task = getBitmapLoadTask(v, decodeWidth, decodeHeight);
+            task.execute();
+            return v;
+        }
+
+        @Override
+        public View getView(Context ctx,
+                int decodeWidth, int decodeHeight, Drawable placeHolder) {
+            return fillImageView(ctx, new ImageView(ctx),
+                    decodeWidth, decodeHeight, placeHolder);
+        }
+
+        @Override
+        public void prepare() {
+            synchronized (mUsing) {
+                mUsing = true;
+            }
+        }
+
+        @Override
+        public void recycle() {
+            synchronized (mUsing) {
+                mUsing = false;
+            }
+        }
+
+        protected boolean isUsing() {
+            synchronized (mUsing) {
+                return mUsing;
+            }
+        }
+
+        /**
+         * Returns the content URI of this data item.
+         */
+        private Uri getContentUri() {
+            Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+            return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
+        }
+
+        @Override
+        public abstract int getType();
+
+        protected abstract BitmapLoadTask getBitmapLoadTask(
+                ImageView v, int decodeWidth, int decodeHeight);
+
+        /**
+         * An AsyncTask class that loads the bitmap in the background thread.
+         * Sub-classes should implement their own "protected Bitmap doInBackground(Void... )"
+         */
+        protected abstract class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {
+            protected ImageView mView;
+
+            protected BitmapLoadTask(ImageView v) {
+                mView = v;
+            }
+
+            @Override
+            protected void onPostExecute(Bitmap bitmap) {
+                if (!isUsing()) return;
+                if (bitmap == null) {
+                    Log.e(TAG, "Failed decoding bitmap for file:" + path);
+                    return;
+                }
+                BitmapDrawable d = new BitmapDrawable(bitmap);
+                mView.setScaleType(ImageView.ScaleType.FIT_XY);
+                mView.setImageDrawable(d);
+            }
+        }
+    }
+
+    static class Photo extends LocalMediaData {
+        public static final int COL_ID = 0;
+        public static final int COL_TITLE = 1;
+        public static final int COL_MIME_TYPE = 2;
+        public static final int COL_DATE_TAKEN = 3;
+        public static final int COL_DATE_MODIFIED = 4;
+        public static final int COL_DATA = 5;
+        public static final int COL_ORIENTATION = 6;
+        public static final int COL_WIDTH = 7;
+        public static final int COL_HEIGHT = 8;
+
+        static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+
+        static final String QUERY_ORDER = ImageColumns.DATE_TAKEN + " DESC, "
+                + ImageColumns._ID + " DESC";
+        /**
+         * These values should be kept in sync with column IDs (COL_*) above.
+         */
+        static final String[] QUERY_PROJECTION = {
+            ImageColumns._ID,           // 0, int
+            ImageColumns.TITLE,         // 1, string
+            ImageColumns.MIME_TYPE,     // 2, string
+            ImageColumns.DATE_TAKEN,    // 3, int
+            ImageColumns.DATE_MODIFIED, // 4, int
+            ImageColumns.DATA,          // 5, string
+            ImageColumns.ORIENTATION,   // 6, int, 0, 90, 180, 270
+            ImageColumns.WIDTH,         // 7, int
+            ImageColumns.HEIGHT,        // 8, int
+        };
+
+        private static final int mSupportedUIActions =
+                FilmStripView.ImageData.ACTION_DEMOTE
+                | FilmStripView.ImageData.ACTION_PROMOTE;
+        private static final int mSupportedDataActions =
+                LocalData.ACTION_DELETE;
+
+        /** 32K buffer. */
+        private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024];
+
+        /** from MediaStore, can only be 0, 90, 180, 270 */
+        public int orientation;
+
+        static Photo buildFromCursor(Cursor c) {
+            Photo d = new Photo();
+            d.id = c.getLong(COL_ID);
+            d.title = c.getString(COL_TITLE);
+            d.mimeType = c.getString(COL_MIME_TYPE);
+            d.dateTaken = c.getLong(COL_DATE_TAKEN);
+            d.dateModified = c.getLong(COL_DATE_MODIFIED);
+            d.path = c.getString(COL_DATA);
+            d.orientation = c.getInt(COL_ORIENTATION);
+            d.width = c.getInt(COL_WIDTH);
+            d.height = c.getInt(COL_HEIGHT);
+            if (d.width <= 0 || d.height <= 0) {
+                Log.w(TAG, "Warning! zero dimension for "
+                        + d.path + ":" + d.width + "x" + d.height);
+                BitmapFactory.Options opts = new BitmapFactory.Options();
+                opts.inJustDecodeBounds = true;
+                BitmapFactory.decodeFile(d.path, opts);
+                if (opts.outWidth != -1 && opts.outHeight != -1)  {
+                    d.width = opts.outWidth;
+                    d.height = opts.outHeight;
+                } else {
+                    Log.w(TAG, "Warning! dimension decode failed for " + d.path);
+                    Bitmap b = BitmapFactory.decodeFile(d.path);
+                    if (b == null) {
+                        return null;
+                    }
+                    d.width = b.getWidth();
+                    d.height = b.getHeight();
+                }
+            }
+            if (d.orientation == 90 || d.orientation == 270) {
+                int b = d.width;
+                d.width = d.height;
+                d.height = b;
+            }
+            return d;
+        }
+
+        @Override
+        public String toString() {
+            return "Photo:" + ",data=" + path + ",mimeType=" + mimeType
+                    + "," + width + "x" + height + ",orientation=" + orientation
+                    + ",date=" + new Date(dateTaken);
+        }
+
+        @Override
+        public int getType() {
+            return TYPE_PHOTO;
+        }
+
+        @Override
+        public boolean isUIActionSupported(int action) {
+            return ((action & mSupportedUIActions) == action);
+        }
+
+        @Override
+        public boolean isDataActionSupported(int action) {
+            return ((action & mSupportedDataActions) == action);
+        }
+
+        @Override
+        public boolean delete(Context c) {
+            ContentResolver cr = c.getContentResolver();
+            cr.delete(CONTENT_URI, ImageColumns._ID + "=" + id, null);
+            return super.delete(c);
+        }
+
+        @Override
+        protected BitmapLoadTask getBitmapLoadTask(
+                ImageView v, int decodeWidth, int decodeHeight) {
+            return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight);
+        }
+
+        private final class PhotoBitmapLoadTask extends BitmapLoadTask {
+            private int mDecodeWidth;
+            private int mDecodeHeight;
+
+            public PhotoBitmapLoadTask(ImageView v, int decodeWidth, int decodeHeight) {
+                super(v);
+                mDecodeWidth = decodeWidth;
+                mDecodeHeight = decodeHeight;
+            }
+
+            @Override
+            protected Bitmap doInBackground(Void... v) {
+                BitmapFactory.Options opts = null;
+                Bitmap b;
+                int sample = 1;
+                while (mDecodeWidth * sample < width
+                        || mDecodeHeight * sample < height) {
+                    sample *= 2;
+                }
+                opts = new BitmapFactory.Options();
+                opts.inSampleSize = sample;
+                opts.inTempStorage = DECODE_TEMP_STORAGE;
+                if (isCancelled() || !isUsing()) {
+                    return null;
+                }
+                b = BitmapFactory.decodeFile(path, opts);
+                if (orientation != 0) {
+                    if (isCancelled() || !isUsing()) {
+                        return null;
+                    }
+                    Matrix m = new Matrix();
+                    m.setRotate(orientation);
+                    b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+                }
+                return b;
+            }
+        }
+    }
+
+    static class Video extends LocalMediaData {
+        public static final int COL_ID = 0;
+        public static final int COL_TITLE = 1;
+        public static final int COL_MIME_TYPE = 2;
+        public static final int COL_DATE_TAKEN = 3;
+        public static final int COL_DATE_MODIFIED = 4;
+        public static final int COL_DATA = 5;
+        public static final int COL_WIDTH = 6;
+        public static final int COL_HEIGHT = 7;
+        public static final int COL_RESOLUTION = 8;
+
+        static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+
+        private static final int mSupportedUIActions =
+                FilmStripView.ImageData.ACTION_DEMOTE
+                | FilmStripView.ImageData.ACTION_PROMOTE;
+        private static final int mSupportedDataActions =
+                LocalData.ACTION_DELETE
+                | LocalData.ACTION_PLAY;
+
+        static final String QUERY_ORDER = VideoColumns.DATE_TAKEN + " DESC, "
+                + VideoColumns._ID + " DESC";
+        /**
+         * These values should be kept in sync with column IDs (COL_*) above.
+         */
+        static final String[] QUERY_PROJECTION = {
+            VideoColumns._ID,           // 0, int
+            VideoColumns.TITLE,         // 1, string
+            VideoColumns.MIME_TYPE,     // 2, string
+            VideoColumns.DATE_TAKEN,    // 3, int
+            VideoColumns.DATE_MODIFIED, // 4, int
+            VideoColumns.DATA,          // 5, string
+            VideoColumns.WIDTH,         // 6, int
+            VideoColumns.HEIGHT,        // 7, int
+            VideoColumns.RESOLUTION     // 8, string
+        };
+
+        private Uri mPlayUri;
+
+        static Video buildFromCursor(Cursor c) {
+            Video d = new Video();
+            d.id = c.getLong(COL_ID);
+            d.title = c.getString(COL_TITLE);
+            d.mimeType = c.getString(COL_MIME_TYPE);
+            d.dateTaken = c.getLong(COL_DATE_TAKEN);
+            d.dateModified = c.getLong(COL_DATE_MODIFIED);
+            d.path = c.getString(COL_DATA);
+            d.width = c.getInt(COL_WIDTH);
+            d.height = c.getInt(COL_HEIGHT);
+            d.mPlayUri = CONTENT_URI.buildUpon()
+                    .appendPath(String.valueOf(d.id)).build();
+            MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+            retriever.setDataSource(d.path);
+            String rotation = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+            if (d.width == 0 || d.height == 0) {
+                d.width = Integer.parseInt(retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
+                d.height = Integer.parseInt(retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
+            }
+            retriever.release();
+            if (rotation != null
+                    && (rotation.equals("90") || rotation.equals("270"))) {
+                int b = d.width;
+                d.width = d.height;
+                d.height = b;
+            }
+            return d;
+        }
+
+        @Override
+        public String toString() {
+            return "Video:" + ",data=" + path + ",mimeType=" + mimeType
+                    + "," + width + "x" + height + ",date=" + new Date(dateTaken);
+        }
+
+        @Override
+        public int getType() {
+            return TYPE_PHOTO;
+        }
+
+        @Override
+        public boolean isUIActionSupported(int action) {
+            return ((action & mSupportedUIActions) == action);
+        }
+
+        @Override
+        public boolean isDataActionSupported(int action) {
+            return ((action & mSupportedDataActions) == action);
+        }
+
+        @Override
+        public boolean delete(Context ctx) {
+            ContentResolver cr = ctx.getContentResolver();
+            cr.delete(CONTENT_URI, VideoColumns._ID + "=" + id, null);
+            return super.delete(ctx);
+        }
+
+        @Override
+        public View getView(final Context ctx,
+                int decodeWidth, int decodeHeight, Drawable placeHolder) {
+
+            // ImageView for the bitmap.
+            ImageView iv = new ImageView(ctx);
+            iv.setLayoutParams(new FrameLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
+            fillImageView(ctx, iv, decodeWidth, decodeHeight, placeHolder);
+
+            // ImageView for the play icon.
+            ImageView icon = new ImageView(ctx);
+            icon.setImageResource(R.drawable.ic_control_play);
+            icon.setScaleType(ImageView.ScaleType.CENTER);
+            icon.setLayoutParams(new FrameLayout.LayoutParams(
+                    ViewGroup.LayoutParams.WRAP_CONTENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
+            icon.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    Util.playVideo(ctx, mPlayUri, title);
+                }
+            });
+
+            FrameLayout f = new FrameLayout(ctx);
+            f.addView(iv);
+            f.addView(icon);
+            return f;
+        }
+
+        @Override
+        protected BitmapLoadTask getBitmapLoadTask(
+                ImageView v, int decodeWidth, int decodeHeight) {
+            return new VideoBitmapLoadTask(v);
+        }
+
+        private final class VideoBitmapLoadTask extends BitmapLoadTask {
+
+            public VideoBitmapLoadTask(ImageView v) {
+                super(v);
+            }
+
+            @Override
+            protected Bitmap doInBackground(Void... v) {
+                if (isCancelled() || !isUsing()) {
+                    return null;
+                }
+                android.media.MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+                retriever.setDataSource(path);
+                byte[] data = retriever.getEmbeddedPicture();
+                Bitmap bitmap = null;
+                if (isCancelled() || !isUsing()) {
+                    retriever.release();
+                    return null;
+                }
+                if (data != null) {
+                    bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+                }
+                if (bitmap == null) {
+                    bitmap = retriever.getFrameAtTime();
+                }
+                retriever.release();
+                return bitmap;
+            }
+        }
+    }
+
+    /**
+     * A LocalData that does nothing but only shows a view.
+     */
+    public static class LocalViewData implements LocalData {
+        private int mWidth;
+        private int mHeight;
+        private View mView;
+        private long mDateTaken;
+        private long mDateModified;
+
+        public LocalViewData(View v,
+                int width, int height,
+                int dateTaken, int dateModified) {
+            mView = v;
+            mWidth = width;
+            mHeight = height;
+            mDateTaken = dateTaken;
+            mDateModified = dateModified;
+        }
+
+        @Override
+        public long getDateTaken() {
+            return mDateTaken;
+        }
+
+        @Override
+        public long getDateModified() {
+            return mDateModified;
+        }
+
+        @Override
+        public String getTitle() {
+            return "";
+        }
+
+        @Override
+        public int getWidth() {
+            return mWidth;
+        }
+
+        @Override
+        public int getHeight() {
+            return mHeight;
+        }
+
+        @Override
+        public int getType() {
+            return FilmStripView.ImageData.TYPE_PHOTO;
+        }
+
+        @Override
+        public String getPath() {
+            return "";
+        }
+
+        @Override
+        public boolean isUIActionSupported(int action) {
+            return false;
+        }
+
+        @Override
+        public boolean isDataActionSupported(int action) {
+            return false;
+        }
+
+        @Override
+        public boolean delete(Context c) {
+            return false;
+        }
+
+        @Override
+        public View getView(Context c, int width, int height, Drawable placeHolder) {
+            return mView;
+        }
+
+        @Override
+        public void prepare() {
+            // do nothing.
+        }
+
+        @Override
+        public void recycle() {
+            // do nothing.
+        }
+
+        @Override
+        public void isPhotoSphere(Context context, PanoramaSupportCallback callback) {
+            // Not a photo sphere panorama.
+            callback.panoramaInfoAvailable(false, false);
+        }
+
+        @Override
+        public void viewPhotoSphere(PanoramaViewHelper helper) {
+            // do nothing.
+        }
+
+        @Override
+        public void onFullScreen(boolean fullScreen) {
+            // do nothing.
+        }
+
+        @Override
+        public boolean canSwipeInFullScreen() {
+            return true;
+        }
+    }
+}
+
diff --git a/src/com/android/camera/data/LocalDataAdapter.java b/src/com/android/camera/data/LocalDataAdapter.java
new file mode 100644
index 0000000..3b4f07d
--- /dev/null
+++ b/src/com/android/camera/data/LocalDataAdapter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import static com.android.camera.ui.FilmStripView.DataAdapter;
+
+/**
+ * An interface which extends {@link DataAdapter} and defines operations on
+ * the data in the local camera folder.
+ */
+public interface LocalDataAdapter extends DataAdapter {
+
+    /**
+     * Request for loading the local data.
+     *
+     * @param resolver  {@link ContentResolver} used for data loading.
+     */
+    public void requestLoad(ContentResolver resolver);
+
+    /**
+     * Remove the data in the local camera folder.
+     *
+     * @param context       {@link Context} used to remove the data.
+     * @param dataID  ID of data to be deleted.
+     */
+    public void removeData(Context context, int dataID);
+
+    /**
+     * Add new local video data.
+     *
+     * @param resolver  {@link ContentResolver} used to add the data.
+     * @param uri       {@link Uri} of the video.
+     */
+    public void addNewVideo(ContentResolver resolver, Uri uri);
+
+    /**
+     * Adds new local photo data.
+     *
+     * @param resolver  {@link ContentResolver} used to add the data.
+     * @param uri       {@link Uri} of the photo.
+     */
+    public void addNewPhoto(ContentResolver resolver, Uri uri);
+
+    /**
+     * Finds the {@link LocalData} of the specified content Uri.
+     *
+     * @param Uri  The content Uri of the {@link LocalData}.
+     * @return     The index of the data. {@code -1} if not found.
+     */
+    public int findDataByContentUri(Uri uri);
+
+    /**
+     * Clears all the data currently loaded.
+     */
+    public void flush();
+
+    /**
+     * Executes the deletion task. Delete the data waiting in the deletion queue.
+     *
+     * @param context The {@link Context} from the caller.
+     * @return        {@code true} if task has been executed, {@code false}
+     *                otherwise.
+     */
+    public boolean executeDeletion(Context context);
+
+    /**
+     * Undo a deletion. If there is any data waiting to be deleted in the queue,
+     * move it out of the deletion queue.
+     *
+     * @return {@code true} if there are items in the queue, {@code false} otherwise.
+     */
+    public boolean undoDataRemoval();
+}
diff --git a/src/com/android/camera/data/PanoramaMetadataLoader.java b/src/com/android/camera/data/PanoramaMetadataLoader.java
new file mode 100644
index 0000000..21b5f8a
--- /dev/null
+++ b/src/com/android/camera/data/PanoramaMetadataLoader.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.data;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+
+import java.util.ArrayList;
+
+/**
+ * This class breaks out the off-thread panorama support.
+ */
+public class PanoramaMetadataLoader {
+    /**
+     * Classes implementing this interface can get information about loaded
+     * photo sphere metadata.
+     */
+    public static interface PanoramaMetadataCallback {
+        /**
+         * Called with the loaded metadata or <code>null</code>.
+         */
+        public void onPanoramaMetadataLoaded(PanoramaMetadata metadata);
+    }
+
+    private PanoramaMetadata mPanoramaMetadata;
+    private ArrayList<PanoramaMetadataCallback> mCallbacksWaiting;
+    private Uri mMediaUri;
+
+    /**
+     * Instantiated the meta data loader for the image resource with the given
+     * URI.
+     */
+    public PanoramaMetadataLoader(Uri uri) {
+        mMediaUri = uri;
+    }
+
+    /**
+     * Asynchronously extract and return panorama metadata from the item with
+     * the given URI.
+     * <p>
+     * NOTE: This call is backed by a cache to speed up successive calls, which
+     * will return immediately. Use {@link #clearCachedValues()} is called.
+     */
+    public synchronized void getPanoramaMetadata(final Context context,
+            PanoramaMetadataCallback callback) {
+        if (mPanoramaMetadata != null) {
+            // Return the cached data right away, no need to fetch it again.
+            callback.onPanoramaMetadataLoaded(mPanoramaMetadata);
+        } else {
+            if (mCallbacksWaiting == null) {
+                mCallbacksWaiting = new ArrayList<PanoramaMetadataCallback>();
+
+                // TODO: Don't create a new thread each time, use a pool or
+                // single instance.
+                (new Thread() {
+                    @Override
+                    public void run() {
+                        onLoadingDone(LightCycleHelper.getPanoramaMetadata(context,
+                                mMediaUri));
+                    }
+                }).start();
+            }
+            mCallbacksWaiting.add(callback);
+        }
+    }
+
+    /**
+     * Clear cached value and stop all running loading threads.
+     */
+    public synchronized void clearCachedValues() {
+        if (mPanoramaMetadata != null) {
+            mPanoramaMetadata = null;
+        }
+
+        // TODO: Cancel running loading thread if active.
+     }
+
+    private synchronized void onLoadingDone(PanoramaMetadata metadata) {
+        mPanoramaMetadata = metadata;
+        if (mPanoramaMetadata == null) {
+            // Error getting panorama data from file. Treat as not panorama.
+            mPanoramaMetadata = LightCycleHelper.NOT_PANORAMA;
+        }
+        for (PanoramaMetadataCallback cb : mCallbacksWaiting) {
+            cb.onPanoramaMetadataLoaded(mPanoramaMetadata);
+        }
+        mCallbacksWaiting = null;
+    }
+}
diff --git a/src/com/android/camera/drawable/TextDrawable.java b/src/com/android/camera/drawable/TextDrawable.java
new file mode 100644
index 0000000..60d8719
--- /dev/null
+++ b/src/com/android/camera/drawable/TextDrawable.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 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.drawable;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+
+
+public class TextDrawable extends Drawable {
+
+    private static final int DEFAULT_COLOR = Color.WHITE;
+    private static final int DEFAULT_TEXTSIZE = 15;
+
+    private Paint mPaint;
+    private CharSequence mText;
+    private int mIntrinsicWidth;
+    private int mIntrinsicHeight;
+    private boolean mUseDropShadow;
+
+    public TextDrawable(Resources res) {
+        this(res, "");
+    }
+
+    public TextDrawable(Resources res, CharSequence text) {
+        mText = text;
+        updatePaint();
+        float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
+                DEFAULT_TEXTSIZE, res.getDisplayMetrics());
+        mPaint.setTextSize(textSize);
+        mIntrinsicWidth = (int) (mPaint.measureText(mText, 0, mText.length()) + .5);
+        mIntrinsicHeight = mPaint.getFontMetricsInt(null);
+    }
+
+    private void updatePaint() {
+        if (mPaint == null) {
+            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        }
+        mPaint.setColor(DEFAULT_COLOR);
+        mPaint.setTextAlign(Align.CENTER);
+        if (mUseDropShadow) {
+            mPaint.setTypeface(Typeface.DEFAULT_BOLD);
+            mPaint.setShadowLayer(10, 0, 0, 0xff000000);
+        } else {
+            mPaint.setTypeface(Typeface.DEFAULT);
+            mPaint.setShadowLayer(0, 0, 0, 0);
+        }
+    }
+
+    public void setText(CharSequence txt) {
+        mText = txt;
+        if (txt == null) {
+            mIntrinsicWidth = 0;
+            mIntrinsicHeight = 0;
+        } else {
+            mIntrinsicWidth = (int) (mPaint.measureText(mText, 0, mText.length()) + .5);
+            mIntrinsicHeight = mPaint.getFontMetricsInt(null);
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (mText != null) {
+            Rect bounds = getBounds();
+            canvas.drawText(mText, 0, mText.length(),
+                    bounds.centerX(), bounds.centerY(), mPaint);
+        }
+    }
+
+    public void setDropShadow(boolean shadow) {
+        mUseDropShadow = shadow;
+        updatePaint();
+    }
+
+    @Override
+    public int getOpacity() {
+        return mPaint.getAlpha();
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mIntrinsicWidth;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mIntrinsicHeight;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter filter) {
+        mPaint.setColorFilter(filter);
+    }
+
+}
diff --git a/src/com/android/camera/ui/AbstractSettingPopup.java b/src/com/android/camera/ui/AbstractSettingPopup.java
new file mode 100644
index 0000000..783b6c7
--- /dev/null
+++ b/src/com/android/camera/ui/AbstractSettingPopup.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+// A popup window that shows one or more camera settings.
+abstract public class AbstractSettingPopup extends RotateLayout {
+    protected ViewGroup mSettingList;
+    protected TextView mTitle;
+
+    public AbstractSettingPopup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mTitle = (TextView) findViewById(R.id.title);
+        mSettingList = (ViewGroup) findViewById(R.id.settingList);
+    }
+
+    abstract public void reloadPreference();
+}
diff --git a/src/com/android/camera/ui/CameraControls.java b/src/com/android/camera/ui/CameraControls.java
new file mode 100644
index 0000000..7fa6890
--- /dev/null
+++ b/src/com/android/camera/ui/CameraControls.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+public class CameraControls extends RotatableLayout {
+
+    private static final String TAG = "CAM_Controls";
+
+    private View mBackgroundView;
+    private View mShutter;
+    private View mSwitcher;
+    private View mMenu;
+    private View mIndicators;
+    private View mPreview;
+
+    public CameraControls(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public CameraControls(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        super.onFinishInflate();
+        mBackgroundView = findViewById(R.id.blocker);
+        mSwitcher = findViewById(R.id.camera_switcher);
+        mShutter = findViewById(R.id.shutter_button);
+        mMenu = findViewById(R.id.menu);
+        mIndicators = findViewById(R.id.on_screen_indicators);
+        mPreview = findViewById(R.id.preview_thumb);
+    }
+
+    @Override
+    public void onLayout(boolean changed, int l, int t, int r, int b) {
+        int orientation = getResources().getConfiguration().orientation;
+        int size = getResources().getDimensionPixelSize(R.dimen.camera_controls_size);
+        int rotation = getUnifiedRotation();
+        adjustBackground();
+        // As l,t,r,b are positions relative to parents, we need to convert them
+        // to child's coordinates
+        r = r - l;
+        b = b - t;
+        l = 0;
+        t = 0;
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+            v.layout(l, t, r, b);
+        }
+        Rect shutter = new Rect();
+        topRight(mPreview, l, t, r, b);
+        if (size > 0) {
+            // restrict controls to size
+            switch (rotation) {
+            case 0:
+            case 180:
+                l = (l + r - size) / 2;
+                r = l + size;
+                break;
+            case 90:
+            case 270:
+                t = (t + b - size) / 2;
+                b = t + size;
+                break;
+            }
+        }
+        center(mShutter, l, t, r, b, orientation, rotation, shutter);
+        center(mBackgroundView, l, t, r, b, orientation, rotation, new Rect());
+        toLeft(mSwitcher, shutter, rotation);
+        toRight(mMenu, shutter, rotation);
+        toRight(mIndicators, shutter, rotation);
+        View retake = findViewById(R.id.btn_retake);
+        if (retake != null) {
+            center(retake, shutter, rotation);
+            View cancel = findViewById(R.id.btn_cancel);
+            toLeft(cancel, shutter, rotation);
+            View done = findViewById(R.id.btn_done);
+            toRight(done, shutter, rotation);
+        }
+    }
+
+    private void center(View v, int l, int t, int r, int b, int orientation, int rotation, Rect result) {
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+        int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+        int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+        switch (rotation) {
+        case 0:
+            // phone portrait; controls bottom
+            result.left = (r + l) / 2 - tw / 2 + lp.leftMargin;
+            result.right = (r + l) / 2 + tw / 2 - lp.rightMargin;
+            result.bottom = b - lp.bottomMargin;
+            result.top = b - th + lp.topMargin;
+            break;
+        case 90:
+            // phone landscape: controls right
+            result.right = r - lp.rightMargin;
+            result.left = r - tw + lp.leftMargin;
+            result.top = (b + t) / 2 - th / 2 + lp.topMargin;
+            result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin;
+            break;
+        case 180:
+            // phone upside down: controls top
+            result.left = (r + l) / 2 - tw / 2 + lp.leftMargin;
+            result.right = (r + l) / 2 + tw / 2 - lp.rightMargin;
+            result.top = t + lp.topMargin;
+            result.bottom = t + th - lp.bottomMargin;
+            break;
+        case 270:
+            // reverse landscape: controls left
+            result.left = l + lp.leftMargin;
+            result.right = l + tw - lp.rightMargin;
+            result.top = (b + t) / 2 - th / 2 + lp.topMargin;
+            result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin;
+            break;
+        }
+        v.layout(result.left, result.top, result.right, result.bottom);
+    }
+
+    private void center(View v, Rect other, int rotation) {
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+        int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+        int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+        int cx = (other.left + other.right) / 2;
+        int cy = (other.top + other.bottom) / 2;
+        v.layout(cx - tw / 2 + lp.leftMargin,
+                cy - th / 2 + lp.topMargin,
+                cx + tw / 2 - lp.rightMargin,
+                cy + th / 2 - lp.bottomMargin);
+    }
+
+    private void toLeft(View v, Rect other, int rotation) {
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+        int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+        int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+        int cx = (other.left + other.right) / 2;
+        int cy = (other.top + other.bottom) / 2;
+        int l = 0, r = 0, t = 0, b = 0;
+        switch (rotation) {
+        case 0:
+            // portrait, to left of anchor at bottom
+            l = other.left - tw + lp.leftMargin;
+            r = other.left - lp.rightMargin;
+            t = cy - th / 2 + lp.topMargin;
+            b = cy + th / 2 - lp.bottomMargin;
+            break;
+        case 90:
+            // phone landscape: below anchor on right
+            l = cx - tw / 2 + lp.leftMargin;
+            r = cx + tw / 2 - lp.rightMargin;
+            t = other.bottom + lp.topMargin;
+            b = other.bottom + th - lp.bottomMargin;
+            break;
+        case 180:
+            // phone upside down: right of anchor at top
+            l = other.right + lp.leftMargin;
+            r = other.right + tw - lp.rightMargin;
+            t = cy - th / 2 + lp.topMargin;
+            b = cy + th / 2 - lp.bottomMargin;
+            break;
+        case 270:
+            // reverse landscape: above anchor on left
+            l = cx - tw / 2 + lp.leftMargin;
+            r = cx + tw / 2 - lp.rightMargin;
+            t = other.top - th + lp.topMargin;
+            b = other.top - lp.bottomMargin;
+            break;
+        }
+        v.layout(l, t, r, b);
+    }
+
+    private void toRight(View v, Rect other, int rotation) {
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+        int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+        int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+        int cx = (other.left + other.right) / 2;
+        int cy = (other.top + other.bottom) / 2;
+        int l = 0, r = 0, t = 0, b = 0;
+        switch (rotation) {
+        case 0:
+            l = other.right + lp.leftMargin;
+            r = other.right + tw - lp.rightMargin;
+            t = cy - th / 2 + lp.topMargin;
+            b = cy + th / 2 - lp.bottomMargin;
+            break;
+        case 90:
+            l = cx - tw / 2 + lp.leftMargin;
+            r = cx + tw / 2 - lp.rightMargin;
+            t = other.top - th + lp.topMargin;
+            b = other.top - lp.bottomMargin;
+            break;
+        case 180:
+            l = other.left - tw + lp.leftMargin;
+            r = other.left - lp.rightMargin;
+            t = cy - th / 2 + lp.topMargin;
+            b = cy + th / 2 - lp.bottomMargin;
+            break;
+        case 270:
+            l = cx - tw / 2 + lp.leftMargin;
+            r = cx + tw / 2 - lp.rightMargin;
+            t = other.bottom + lp.topMargin;
+            b = other.bottom + th - lp.bottomMargin;
+            break;
+        }
+        v.layout(l, t, r, b);
+    }
+
+    private void topRight(View v, int l, int t, int r, int b) {
+        // layout using the specific margins; the rotation code messes up the others
+        int mt = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_top);
+        int mr = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_right);
+        v.layout(r - v.getMeasuredWidth() - mr, t + mt, r - mr, t + mt + v.getMeasuredHeight());
+    }
+
+    private void adjustBackground() {
+        int rotation = getUnifiedRotation();
+        // remove current drawable and reset rotation
+        mBackgroundView.setBackgroundDrawable(null);
+        mBackgroundView.setRotationX(0);
+        mBackgroundView.setRotationY(0);
+        // if the switcher background is top aligned we need to flip the background
+        // drawable vertically; if left aligned, flip horizontally
+        switch (rotation) {
+            case 180:
+                mBackgroundView.setRotationX(180);
+                break;
+            case 270:
+                mBackgroundView.setRotationY(180);
+                break;
+            default:
+                break;
+        }
+        mBackgroundView.setBackgroundResource(R.drawable.switcher_bg);
+    }
+
+}
diff --git a/src/com/android/camera/ui/CameraRootView.java b/src/com/android/camera/ui/CameraRootView.java
new file mode 100644
index 0000000..adda706
--- /dev/null
+++ b/src/com/android/camera/ui/CameraRootView.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.common.ApiHelper;
+
+public class CameraRootView extends FrameLayout {
+
+    private int mTopMargin = 0;
+    private int mBottomMargin = 0;
+    private int mLeftMargin = 0;
+    private int mRightMargin = 0;
+    private Rect mCurrentInsets;
+    private int mOffset = 0;
+    private Object mDisplayListener;
+    private MyDisplayListener mListener;
+    public interface MyDisplayListener {
+        public void onDisplayChanged();
+    }
+
+    public CameraRootView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initDisplayListener();
+        setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+    }
+
+    @Override
+    protected boolean fitSystemWindows(Rect insets) {
+        super.fitSystemWindows(insets);
+        mCurrentInsets = insets;
+        // insets include status bar, navigation bar, etc
+        // In this case, we are only concerned with the size of nav bar
+        if (mOffset > 0) return true;
+
+        if (insets.bottom > 0) {
+            mOffset = insets.bottom;
+        } else if (insets.right > 0) {
+            mOffset = insets.right;
+        }
+        return true;
+    }
+
+    public void initDisplayListener() {
+        if (ApiHelper.HAS_DISPLAY_LISTENER) {
+            mDisplayListener = new DisplayListener() {
+
+                @Override
+                public void onDisplayAdded(int arg0) {}
+
+                @Override
+                public void onDisplayChanged(int arg0) {
+                    mListener.onDisplayChanged();
+                }
+
+                @Override
+                public void onDisplayRemoved(int arg0) {}
+            };
+        }
+    }
+
+    public void setDisplayChangeListener(MyDisplayListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (ApiHelper.HAS_DISPLAY_LISTENER) {
+            ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE))
+            .registerDisplayListener((DisplayListener) mDisplayListener, null);
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow () {
+        super.onDetachedFromWindow();
+        if (ApiHelper.HAS_DISPLAY_LISTENER) {
+            ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE))
+            .unregisterDisplayListener((DisplayListener) mDisplayListener);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int rotation = Util.getDisplayRotation((Activity) getContext());
+        // all the layout code assumes camera device orientation to be portrait
+        // adjust rotation for landscape
+        int orientation = getResources().getConfiguration().orientation;
+        int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT
+                : Configuration.ORIENTATION_LANDSCAPE;
+        if (camOrientation != orientation) {
+            rotation = (rotation + 90) % 360;
+        }
+        // calculate margins
+        mLeftMargin = 0;
+        mRightMargin = 0;
+        mBottomMargin = 0;
+        mTopMargin = 0;
+        switch (rotation) {
+            case 0:
+                mBottomMargin += mOffset;
+                break;
+            case 90:
+                mRightMargin += mOffset;
+                break;
+            case 180:
+                mTopMargin += mOffset;
+                break;
+            case 270:
+                mLeftMargin += mOffset;
+                break;
+        }
+        if (mCurrentInsets != null) {
+            if (mCurrentInsets.right > 0) {
+                // navigation bar on the right
+                mRightMargin = mRightMargin > 0 ? mRightMargin : mCurrentInsets.right;
+            } else {
+                // navigation bar on the bottom
+                mBottomMargin = mBottomMargin > 0 ? mBottomMargin : mCurrentInsets.bottom;
+            }
+        }
+        // make sure all the children are resized
+        super.onMeasure(widthMeasureSpec - mLeftMargin - mRightMargin,
+                heightMeasureSpec - mTopMargin - mBottomMargin);
+        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    public void onLayout(boolean changed, int l, int t, int r, int b) {
+        r -= l;
+        b -= t;
+        l = 0;
+        t = 0;
+        int orientation = getResources().getConfiguration().orientation;
+        // Lay out children
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+            if (v instanceof CameraControls) {
+                // Lay out camera controls to center on the short side of the screen
+                // so that they stay in place during rotation
+                int width = v.getMeasuredWidth();
+                int height = v.getMeasuredHeight();
+                if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+                    int left = (l + r - width) / 2;
+                    v.layout(left, t + mTopMargin, left + width, b - mBottomMargin);
+                } else {
+                    int top = (t + b - height) / 2;
+                    v.layout(l + mLeftMargin, top, r - mRightMargin, top + height);
+                }
+            } else {
+                v.layout(l + mLeftMargin, t + mTopMargin, r - mRightMargin, b - mBottomMargin);
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java
new file mode 100644
index 0000000..6e43215
--- /dev/null
+++ b/src/com/android/camera/ui/CameraSwitcher.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.LinearLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.UsageStatistics;
+
+public class CameraSwitcher extends RotateImageView
+        implements OnClickListener, OnTouchListener {
+
+    private static final String TAG = "CAM_Switcher";
+    private static final int SWITCHER_POPUP_ANIM_DURATION = 200;
+
+    public static final int PHOTO_MODULE_INDEX = 0;
+    public static final int VIDEO_MODULE_INDEX = 1;
+    public static final int LIGHTCYCLE_MODULE_INDEX = 2;
+    public static final int REFOCUS_MODULE_INDEX = 3;
+    private static final int[] DRAW_IDS = {
+            R.drawable.ic_switch_camera,
+            R.drawable.ic_switch_video,
+            R.drawable.ic_switch_photosphere,
+            R.drawable.ic_switch_refocus
+    };
+    public interface CameraSwitchListener {
+        public void onCameraSelected(int i);
+        public void onShowSwitcherPopup();
+    }
+
+    private CameraSwitchListener mListener;
+    private int mCurrentIndex;
+    private int[] mModuleIds;
+    private int[] mDrawIds;
+    private int mItemSize;
+    private View mPopup;
+    private View mParent;
+    private boolean mShowingPopup;
+    private boolean mNeedsAnimationSetup;
+    private Drawable mIndicator;
+
+    private float mTranslationX = 0;
+    private float mTranslationY = 0;
+
+    private AnimatorListener mHideAnimationListener;
+    private AnimatorListener mShowAnimationListener;
+
+    public CameraSwitcher(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public CameraSwitcher(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size);
+        setOnClickListener(this);
+        mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator);
+        initializeDrawables(context);
+    }
+
+    public void initializeDrawables(Context context) {
+        int totaldrawid = (LightCycleHelper.hasLightCycleCapture(context)
+                ? DRAW_IDS.length : DRAW_IDS.length - 1);
+
+        int[] drawids = new int[totaldrawid];
+        int[] moduleids = new int[totaldrawid];
+        int ix = 0;
+        for (int i = 0; i < DRAW_IDS.length; i++) {
+            if (i == LIGHTCYCLE_MODULE_INDEX && !LightCycleHelper.hasLightCycleCapture(context)) {
+                continue; // not enabled, so don't add to UI
+            }
+            moduleids[ix] = i;
+            drawids[ix++] = DRAW_IDS[i];
+        }
+        setIds(moduleids, drawids);
+    }
+
+    public void setIds(int[] moduleids, int[] drawids) {
+        mDrawIds = drawids;
+        mModuleIds = moduleids;
+    }
+
+    public void setCurrentIndex(int i) {
+        mCurrentIndex = i;
+        setImageResource(mDrawIds[i]);
+    }
+
+    public void setSwitchListener(CameraSwitchListener l) {
+        mListener = l;
+    }
+
+    @Override
+    public void onClick(View v) {
+        showSwitcher();
+        mListener.onShowSwitcherPopup();
+    }
+
+    private void onCameraSelected(int ix) {
+        hidePopup();
+        if ((ix != mCurrentIndex) && (mListener != null)) {
+            UsageStatistics.onEvent("CameraModeSwitch", null, null);
+            UsageStatistics.setPendingTransitionCause(
+                    UsageStatistics.TRANSITION_MENU_TAP);
+            setCurrentIndex(ix);
+            mListener.onCameraSelected(mModuleIds[ix]);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        mIndicator.setBounds(getDrawable().getBounds());
+        mIndicator.draw(canvas);
+    }
+
+    private void initPopup() {
+        mParent = LayoutInflater.from(getContext()).inflate(R.layout.switcher_popup,
+                (ViewGroup) getParent());
+        LinearLayout content = (LinearLayout) mParent.findViewById(R.id.content);
+        mPopup = content;
+        // Set the gravity of the popup, so that it shows up at the right position
+        // on screen
+        LayoutParams lp = ((LayoutParams) mPopup.getLayoutParams());
+        lp.gravity = ((LayoutParams) mParent.findViewById(R.id.camera_switcher)
+                .getLayoutParams()).gravity;
+        mPopup.setLayoutParams(lp);
+
+        mPopup.setVisibility(View.INVISIBLE);
+        mNeedsAnimationSetup = true;
+        for (int i = mDrawIds.length - 1; i >= 0; i--) {
+            RotateImageView item = new RotateImageView(getContext());
+            item.setImageResource(mDrawIds[i]);
+            item.setBackgroundResource(R.drawable.bg_pressed);
+            final int index = i;
+            item.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (showsPopup()) onCameraSelected(index);
+                }
+            });
+            switch (mDrawIds[i]) {
+                case R.drawable.ic_switch_camera:
+                    item.setContentDescription(getContext().getResources().getString(
+                            R.string.accessibility_switch_to_camera));
+                    break;
+                case R.drawable.ic_switch_video:
+                    item.setContentDescription(getContext().getResources().getString(
+                            R.string.accessibility_switch_to_video));
+                    break;
+                case R.drawable.ic_switch_photosphere:
+                    item.setContentDescription(getContext().getResources().getString(
+                            R.string.accessibility_switch_to_new_panorama));
+                    break;
+                case R.drawable.ic_switch_refocus:
+                    item.setContentDescription(getContext().getResources().getString(
+                            R.string.accessibility_switch_to_refocus));
+                    break;
+                default:
+                    break;
+            }
+            content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize));
+        }
+        mPopup.measure(MeasureSpec.makeMeasureSpec(mParent.getWidth(), MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(mParent.getHeight(), MeasureSpec.AT_MOST));
+    }
+
+    public boolean showsPopup() {
+        return mShowingPopup;
+    }
+
+    public boolean isInsidePopup(MotionEvent evt) {
+        if (!showsPopup()) return false;
+        int topLeft[] = new int[2];
+        mPopup.getLocationOnScreen(topLeft);
+        int left = topLeft[0];
+        int top = topLeft[1];
+        int bottom = top + mPopup.getHeight();
+        int right = left + mPopup.getWidth();
+        return evt.getX() >= left && evt.getX() < right
+                && evt.getY() >= top && evt.getY() < bottom;
+    }
+
+    private void hidePopup() {
+        mShowingPopup = false;
+        setVisibility(View.VISIBLE);
+        if (mPopup != null && !animateHidePopup()) {
+            mPopup.setVisibility(View.INVISIBLE);
+        }
+        mParent.setOnTouchListener(null);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        if (showsPopup()) {
+            ((ViewGroup) mParent).removeView(mPopup);
+            mPopup = null;
+            initPopup();
+            mPopup.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private void showSwitcher() {
+        mShowingPopup = true;
+        if (mPopup == null) {
+            initPopup();
+        }
+        layoutPopup();
+        mPopup.setVisibility(View.VISIBLE);
+        if (!animateShowPopup()) {
+            setVisibility(View.INVISIBLE);
+        }
+        mParent.setOnTouchListener(this);
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        closePopup();
+        return true;
+    }
+
+    public void closePopup() {
+        if (showsPopup()) {
+            hidePopup();
+        }
+    }
+
+    @Override
+    public void setOrientation(int degree, boolean animate) {
+        super.setOrientation(degree, animate);
+        ViewGroup content = (ViewGroup) mPopup;
+        if (content == null) return;
+        for (int i = 0; i < content.getChildCount(); i++) {
+            RotateImageView iv = (RotateImageView) content.getChildAt(i);
+            iv.setOrientation(degree, animate);
+        }
+    }
+
+    private void layoutPopup() {
+        int orientation = Util.getDisplayRotation((Activity) getContext());
+        int w = mPopup.getMeasuredWidth();
+        int h = mPopup.getMeasuredHeight();
+        if (orientation == 0) {
+            mPopup.layout(getRight() - w, getBottom() - h, getRight(), getBottom());
+            mTranslationX = 0;
+            mTranslationY = h / 3;
+        } else if (orientation == 90) {
+            mTranslationX = w / 3;
+            mTranslationY = - h / 3;
+            mPopup.layout(getRight() - w, getTop(), getRight(), getTop() + h);
+        } else if (orientation == 180) {
+            mTranslationX = - w / 3;
+            mTranslationY = - h / 3;
+            mPopup.layout(getLeft(), getTop(), getLeft() + w, getTop() + h);
+        } else {
+            mTranslationX = - w / 3;
+            mTranslationY = h - getHeight();
+            mPopup.layout(getLeft(), getBottom() - h, getLeft() + w, getBottom());
+        }
+    }
+
+    @Override
+    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (mPopup != null) {
+            layoutPopup();
+        }
+    }
+
+    private void popupAnimationSetup() {
+        if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+            return;
+        }
+        layoutPopup();
+        mPopup.setScaleX(0.3f);
+        mPopup.setScaleY(0.3f);
+        mPopup.setTranslationX(mTranslationX);
+        mPopup.setTranslationY(mTranslationY);
+        mNeedsAnimationSetup = false;
+    }
+
+    private boolean animateHidePopup() {
+        if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+            return false;
+        }
+        if (mHideAnimationListener == null) {
+            mHideAnimationListener = new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    // Verify that we weren't canceled
+                    if (!showsPopup() && mPopup != null) {
+                        mPopup.setVisibility(View.INVISIBLE);
+                        ((ViewGroup) mParent).removeView(mPopup);
+                        mPopup = null;
+                    }
+                }
+            };
+        }
+        mPopup.animate()
+                .alpha(0f)
+                .scaleX(0.3f).scaleY(0.3f)
+                .translationX(mTranslationX)
+                .translationY(mTranslationY)
+                .setDuration(SWITCHER_POPUP_ANIM_DURATION)
+                .setListener(mHideAnimationListener);
+        animate().alpha(1f).setDuration(SWITCHER_POPUP_ANIM_DURATION)
+                .setListener(null);
+        return true;
+    }
+
+    private boolean animateShowPopup() {
+        if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+            return false;
+        }
+        if (mNeedsAnimationSetup) {
+            popupAnimationSetup();
+        }
+        if (mShowAnimationListener == null) {
+            mShowAnimationListener = new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    // Verify that we weren't canceled
+                    if (showsPopup()) {
+                        setVisibility(View.INVISIBLE);
+                        // request layout to make sure popup is laid out correctly on ICS
+                        mPopup.requestLayout();
+                    }
+                }
+            };
+        }
+        mPopup.animate()
+                .alpha(1f)
+                .scaleX(1f).scaleY(1f)
+                .translationX(0)
+                .translationY(0)
+                .setDuration(SWITCHER_POPUP_ANIM_DURATION)
+                .setListener(null);
+        animate().alpha(0f).setDuration(SWITCHER_POPUP_ANIM_DURATION)
+                .setListener(mShowAnimationListener);
+        return true;
+    }
+}
diff --git a/src/com/android/camera/ui/CheckedLinearLayout.java b/src/com/android/camera/ui/CheckedLinearLayout.java
new file mode 100644
index 0000000..4e77504
--- /dev/null
+++ b/src/com/android/camera/ui/CheckedLinearLayout.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.LinearLayout;
+
+public class CheckedLinearLayout extends LinearLayout implements Checkable {
+    private static final int[] CHECKED_STATE_SET = {
+        android.R.attr.state_checked
+    };
+    private boolean mChecked;
+
+    public CheckedLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean isChecked() {
+        return mChecked;
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        if (mChecked != checked) {
+            mChecked = checked;
+            refreshDrawableState();
+        }
+    }
+
+    @Override
+    public void toggle() {
+        setChecked(!mChecked);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+        if (mChecked) {
+            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+        }
+        return drawableState;
+    }
+}
diff --git a/src/com/android/camera/ui/CountDownView.java b/src/com/android/camera/ui/CountDownView.java
new file mode 100644
index 0000000..907d335
--- /dev/null
+++ b/src/com/android/camera/ui/CountDownView.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import java.util.Locale;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.SoundPool;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+public class CountDownView extends FrameLayout {
+
+    private static final String TAG = "CAM_CountDownView";
+    private static final int SET_TIMER_TEXT = 1;
+    private TextView mRemainingSecondsView;
+    private int mRemainingSecs = 0;
+    private OnCountDownFinishedListener mListener;
+    private Animation mCountDownAnim;
+    private SoundPool mSoundPool;
+    private int mBeepTwice;
+    private int mBeepOnce;
+    private boolean mPlaySound;
+    private final Handler mHandler = new MainHandler();
+
+    public CountDownView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mCountDownAnim = AnimationUtils.loadAnimation(context, R.anim.count_down_exit);
+        // Load the beeps
+        mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
+        mBeepOnce = mSoundPool.load(context, R.raw.beep_once, 1);
+        mBeepTwice = mSoundPool.load(context, R.raw.beep_twice, 1);
+    }
+
+    public boolean isCountingDown() {
+        return mRemainingSecs > 0;
+    };
+
+    public interface OnCountDownFinishedListener {
+        public void onCountDownFinished();
+    }
+
+    private void remainingSecondsChanged(int newVal) {
+        mRemainingSecs = newVal;
+        if (newVal == 0) {
+            // Countdown has finished
+            setVisibility(View.INVISIBLE);
+            mListener.onCountDownFinished();
+        } else {
+            Locale locale = getResources().getConfiguration().locale;
+            String localizedValue = String.format(locale, "%d", newVal);
+            mRemainingSecondsView.setText(localizedValue);
+            // Fade-out animation
+            mCountDownAnim.reset();
+            mRemainingSecondsView.clearAnimation();
+            mRemainingSecondsView.startAnimation(mCountDownAnim);
+
+            // Play sound effect for the last 3 seconds of the countdown
+            if (mPlaySound) {
+                if (newVal == 1) {
+                    mSoundPool.play(mBeepTwice, 1.0f, 1.0f, 0, 0, 1.0f);
+                } else if (newVal <= 3) {
+                    mSoundPool.play(mBeepOnce, 1.0f, 1.0f, 0, 0, 1.0f);
+                }
+            }
+            // Schedule the next remainingSecondsChanged() call in 1 second
+            mHandler.sendEmptyMessageDelayed(SET_TIMER_TEXT, 1000);
+        }
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mRemainingSecondsView = (TextView) findViewById(R.id.remaining_seconds);
+    }
+
+    public void setCountDownFinishedListener(OnCountDownFinishedListener listener) {
+        mListener = listener;
+    }
+
+    public void startCountDown(int sec, boolean playSound) {
+        if (sec <= 0) {
+            Log.w(TAG, "Invalid input for countdown timer: " + sec + " seconds");
+            return;
+        }
+        setVisibility(View.VISIBLE);
+        mPlaySound = playSound;
+        remainingSecondsChanged(sec);
+    }
+
+    public void cancelCountDown() {
+        if (mRemainingSecs > 0) {
+            mRemainingSecs = 0;
+            mHandler.removeMessages(SET_TIMER_TEXT);
+            setVisibility(View.INVISIBLE);
+        }
+    }
+
+    private class MainHandler extends Handler {
+        @Override
+        public void handleMessage(Message message) {
+            if (message.what == SET_TIMER_TEXT) {
+                remainingSecondsChanged(mRemainingSecs -1);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/camera/ui/CountdownTimerPopup.java b/src/com/android/camera/ui/CountdownTimerPopup.java
new file mode 100644
index 0000000..7c3572b
--- /dev/null
+++ b/src/com/android/camera/ui/CountdownTimerPopup.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.NumberPicker;
+import android.widget.NumberPicker.OnValueChangeListener;
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+import java.util.Locale;
+
+/**
+ * This is a popup window that allows users to specify a countdown timer
+ */
+
+public class CountdownTimerPopup extends AbstractSettingPopup {
+    private static final String TAG = "TimerSettingPopup";
+    private NumberPicker mNumberSpinner;
+    private String[] mDurations;
+    private ListPreference mTimer;
+    private ListPreference mBeep;
+    private Listener mListener;
+    private Button mConfirmButton;
+    private View mPickerTitle;
+    private CheckBox mTimerSound;
+    private View mSoundTitle;
+
+    static public interface Listener {
+        public void onListPrefChanged(ListPreference pref);
+    }
+
+    public void setSettingChangedListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public CountdownTimerPopup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void initialize(ListPreference timer, ListPreference beep) {
+        mTimer = timer;
+        mBeep = beep;
+        // Set title.
+        mTitle.setText(mTimer.getTitle());
+
+        // Duration
+        CharSequence[] entries = mTimer.getEntryValues();
+        mDurations = new String[entries.length];
+        Locale locale = getResources().getConfiguration().locale;
+        mDurations[0] = getResources().getString(R.string.setting_off); // Off
+        for (int i = 1; i < entries.length; i++)
+            mDurations[i] =  String.format(locale, "%d", Integer.parseInt(entries[i].toString()));
+        int durationCount = mDurations.length;
+        mNumberSpinner = (NumberPicker) findViewById(R.id.duration);
+        mNumberSpinner.setMinValue(0);
+        mNumberSpinner.setMaxValue(durationCount - 1);
+        mNumberSpinner.setDisplayedValues(mDurations);
+        mNumberSpinner.setWrapSelectorWheel(false);
+        mNumberSpinner.setOnValueChangedListener(new OnValueChangeListener() {
+            @Override
+            public void onValueChange(NumberPicker picker, int oldValue, int newValue) {
+                setTimeSelectionEnabled(newValue != 0);
+            }
+        });
+        mConfirmButton = (Button) findViewById(R.id.timer_set_button);
+        mPickerTitle = findViewById(R.id.set_time_interval_title);
+
+        // Disable focus on the spinners to prevent keyboard from coming up
+        mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
+
+        mConfirmButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                updateInputState();
+            }
+        });
+        mTimerSound = (CheckBox) findViewById(R.id.sound_check_box);
+        mSoundTitle = findViewById(R.id.beep_title);
+    }
+
+    private void restoreSetting() {
+        int index = mTimer.findIndexOfValue(mTimer.getValue());
+        if (index == -1) {
+            Log.e(TAG, "Invalid preference value.");
+            mTimer.print();
+            throw new IllegalArgumentException();
+        } else {
+            setTimeSelectionEnabled(index != 0);
+            mNumberSpinner.setValue(index);
+        }
+        boolean checked = mBeep.findIndexOfValue(mBeep.getValue()) != 0;
+        mTimerSound.setChecked(checked);
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        if (visibility == View.VISIBLE) {
+            if (getVisibility() != View.VISIBLE) {
+                // Set the number pickers and on/off switch to be consistent
+                // with the preference
+                restoreSetting();
+            }
+        }
+        super.setVisibility(visibility);
+    }
+
+    protected void setTimeSelectionEnabled(boolean enabled) {
+        mPickerTitle.setVisibility(enabled ? VISIBLE : INVISIBLE);
+        mTimerSound.setEnabled(enabled);
+        mSoundTitle.setEnabled(enabled);
+    }
+
+    @Override
+    public void reloadPreference() {
+    }
+
+    private void updateInputState() {
+        mTimer.setValueIndex(mNumberSpinner.getValue());
+        mBeep.setValueIndex(mTimerSound.isChecked() ? 1 : 0);
+        if (mListener != null) {
+            mListener.onListPrefChanged(mTimer);
+            mListener.onListPrefChanged(mBeep);
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/EffectSettingPopup.java b/src/com/android/camera/ui/EffectSettingPopup.java
new file mode 100644
index 0000000..568781a
--- /dev/null
+++ b/src/com/android/camera/ui/EffectSettingPopup.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.SimpleAdapter;
+
+import com.android.camera.IconListPreference;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+// A popup window that shows video effect setting. It has two grid view.
+// One shows the goofy face effects. The other shows the background replacer
+// effects.
+public class EffectSettingPopup extends AbstractSettingPopup implements
+        AdapterView.OnItemClickListener, View.OnClickListener {
+    private static final String TAG = "EffectSettingPopup";
+    private String mNoEffect;
+    private IconListPreference mPreference;
+    private Listener mListener;
+    private View mClearEffects;
+    private GridView mSillyFacesGrid;
+    private GridView mBackgroundGrid;
+
+    // Data for silly face items. (text, image, and preference value)
+    ArrayList<HashMap<String, Object>> mSillyFacesItem =
+            new ArrayList<HashMap<String, Object>>();
+
+    // Data for background replacer items. (text, image, and preference value)
+    ArrayList<HashMap<String, Object>> mBackgroundItem =
+            new ArrayList<HashMap<String, Object>>();
+
+
+    static public interface Listener {
+        public void onSettingChanged();
+    }
+
+    public EffectSettingPopup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mNoEffect = context.getString(R.string.pref_video_effect_default);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mClearEffects = findViewById(R.id.clear_effects);
+        mClearEffects.setOnClickListener(this);
+        mSillyFacesGrid = (GridView) findViewById(R.id.effect_silly_faces);
+        mBackgroundGrid = (GridView) findViewById(R.id.effect_background);
+    }
+
+    public void initialize(IconListPreference preference) {
+        mPreference = preference;
+        Context context = getContext();
+        CharSequence[] entries = mPreference.getEntries();
+        CharSequence[] entryValues = mPreference.getEntryValues();
+        int[] iconIds = mPreference.getImageIds();
+        if (iconIds == null) {
+            iconIds = mPreference.getLargeIconIds();
+        }
+
+        // Set title.
+        mTitle.setText(mPreference.getTitle());
+
+        for(int i = 0; i < entries.length; ++i) {
+            String value = entryValues[i].toString();
+            if (value.equals(mNoEffect)) continue;  // no effect, skip it.
+            HashMap<String, Object> map = new HashMap<String, Object>();
+            map.put("value", value);
+            map.put("text", entries[i].toString());
+            if (iconIds != null) map.put("image", iconIds[i]);
+            if (value.startsWith("goofy_face")) {
+                mSillyFacesItem.add(map);
+            } else if (value.startsWith("backdropper")) {
+                mBackgroundItem.add(map);
+            }
+        }
+
+        boolean hasSillyFaces = mSillyFacesItem.size() > 0;
+        boolean hasBackground = mBackgroundItem.size() > 0;
+
+        // Initialize goofy face if it is supported.
+        if (hasSillyFaces) {
+            findViewById(R.id.effect_silly_faces_title).setVisibility(View.VISIBLE);
+            findViewById(R.id.effect_silly_faces_title_separator).setVisibility(View.VISIBLE);
+            mSillyFacesGrid.setVisibility(View.VISIBLE);
+            SimpleAdapter sillyFacesItemAdapter = new SimpleAdapter(context,
+                    mSillyFacesItem, R.layout.effect_setting_item,
+                    new String[] {"text", "image"},
+                    new int[] {R.id.text, R.id.image});
+            mSillyFacesGrid.setAdapter(sillyFacesItemAdapter);
+            mSillyFacesGrid.setOnItemClickListener(this);
+        }
+
+        if (hasSillyFaces && hasBackground) {
+            findViewById(R.id.effect_background_separator).setVisibility(View.VISIBLE);
+        }
+
+        // Initialize background replacer if it is supported.
+        if (hasBackground) {
+            findViewById(R.id.effect_background_title).setVisibility(View.VISIBLE);
+            findViewById(R.id.effect_background_title_separator).setVisibility(View.VISIBLE);
+            mBackgroundGrid.setVisibility(View.VISIBLE);
+            SimpleAdapter backgroundItemAdapter = new SimpleAdapter(context,
+                    mBackgroundItem, R.layout.effect_setting_item,
+                    new String[] {"text", "image"},
+                    new int[] {R.id.text, R.id.image});
+            mBackgroundGrid.setAdapter(backgroundItemAdapter);
+            mBackgroundGrid.setOnItemClickListener(this);
+        }
+
+        reloadPreference();
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        if (visibility == View.VISIBLE) {
+            if (getVisibility() != View.VISIBLE) {
+                // Do not show or hide "Clear effects" button when the popup
+                // is already visible. Otherwise it looks strange.
+                boolean noEffect = mPreference.getValue().equals(mNoEffect);
+                mClearEffects.setVisibility(noEffect ? View.GONE : View.VISIBLE);
+            }
+            reloadPreference();
+        }
+        super.setVisibility(visibility);
+    }
+
+    // The value of the preference may have changed. Update the UI.
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    @Override
+    public void reloadPreference() {
+        mBackgroundGrid.setItemChecked(mBackgroundGrid.getCheckedItemPosition(), false);
+        mSillyFacesGrid.setItemChecked(mSillyFacesGrid.getCheckedItemPosition(), false);
+
+        String value = mPreference.getValue();
+        if (value.equals(mNoEffect)) return;
+
+        for (int i = 0; i < mSillyFacesItem.size(); i++) {
+            if (value.equals(mSillyFacesItem.get(i).get("value"))) {
+                mSillyFacesGrid.setItemChecked(i, true);
+                return;
+            }
+        }
+
+        for (int i = 0; i < mBackgroundItem.size(); i++) {
+            if (value.equals(mBackgroundItem.get(i).get("value"))) {
+                mBackgroundGrid.setItemChecked(i, true);
+                return;
+            }
+        }
+
+        Log.e(TAG, "Invalid preference value: " + value);
+        mPreference.print();
+    }
+
+    public void setSettingChangedListener(Listener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view,
+            int index, long id) {
+        String value;
+        if (parent == mSillyFacesGrid) {
+            value = (String) mSillyFacesItem.get(index).get("value");
+        } else if (parent == mBackgroundGrid) {
+            value = (String) mBackgroundItem.get(index).get("value");
+        } else {
+            return;
+        }
+
+        // Tapping the selected effect will deselect it (clear effects).
+        if (value.equals(mPreference.getValue())) {
+            mPreference.setValue(mNoEffect);
+        } else {
+            mPreference.setValue(value);
+        }
+        reloadPreference();
+        if (mListener != null) mListener.onSettingChanged();
+    }
+
+    @Override
+    public void onClick(View v) {
+        // Clear the effect.
+        mPreference.setValue(mNoEffect);
+        reloadPreference();
+        if (mListener != null) mListener.onSettingChanged();
+    }
+}
diff --git a/src/com/android/camera/ui/ExpandedGridView.java b/src/com/android/camera/ui/ExpandedGridView.java
new file mode 100644
index 0000000..13cf58f
--- /dev/null
+++ b/src/com/android/camera/ui/ExpandedGridView.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+public class ExpandedGridView extends GridView {
+    public ExpandedGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // If UNSPECIFIED is passed to GridView, it will show only one row.
+        // Here GridView is put in a ScrollView, so pass it a very big size with
+        // AT_MOST to show all the rows.
+        heightMeasureSpec = MeasureSpec.makeMeasureSpec(65536, MeasureSpec.AT_MOST);
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+}
diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java
new file mode 100644
index 0000000..7d66dc0
--- /dev/null
+++ b/src/com/android/camera/ui/FaceView.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.hardware.Camera.Face;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import com.android.camera.PhotoUI;
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+public class FaceView extends View
+    implements FocusIndicator, Rotatable,
+    PhotoUI.SurfaceTextureSizeChangedListener {
+    private static final String TAG = "CAM FaceView";
+    private final boolean LOGV = false;
+    // The value for android.hardware.Camera.setDisplayOrientation.
+    private int mDisplayOrientation;
+    // The orientation compensation for the face indicator to make it look
+    // correctly in all device orientations. Ex: if the value is 90, the
+    // indicator should be rotated 90 degrees counter-clockwise.
+    private int mOrientation;
+    private boolean mMirror;
+    private boolean mPause;
+    private Matrix mMatrix = new Matrix();
+    private RectF mRect = new RectF();
+    // As face detection can be flaky, we add a layer of filtering on top of it
+    // to avoid rapid changes in state (eg, flickering between has faces and
+    // not having faces)
+    private Face[] mFaces;
+    private Face[] mPendingFaces;
+    private int mColor;
+    private final int mFocusingColor;
+    private final int mFocusedColor;
+    private final int mFailColor;
+    private Paint mPaint;
+    private volatile boolean mBlocked;
+
+    private int mUncroppedWidth;
+    private int mUncroppedHeight;
+    private static final int MSG_SWITCH_FACES = 1;
+    private static final int SWITCH_DELAY = 70;
+    private boolean mStateSwitchPending = false;
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+            case MSG_SWITCH_FACES:
+                mStateSwitchPending = false;
+                mFaces = mPendingFaces;
+                invalidate();
+                break;
+            }
+        }
+    };
+
+    public FaceView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        Resources res = getResources();
+        mFocusingColor = res.getColor(R.color.face_detect_start);
+        mFocusedColor = res.getColor(R.color.face_detect_success);
+        mFailColor = res.getColor(R.color.face_detect_fail);
+        mColor = mFocusingColor;
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Style.STROKE);
+        mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke));
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight) {
+        mUncroppedWidth = uncroppedWidth;
+        mUncroppedHeight = uncroppedHeight;
+    }
+
+    public void setFaces(Face[] faces) {
+        if (LOGV) Log.v(TAG, "Num of faces=" + faces.length);
+        if (mPause) return;
+        if (mFaces != null) {
+            if ((faces.length > 0 && mFaces.length == 0)
+                    || (faces.length == 0 && mFaces.length > 0)) {
+                mPendingFaces = faces;
+                if (!mStateSwitchPending) {
+                    mStateSwitchPending = true;
+                    mHandler.sendEmptyMessageDelayed(MSG_SWITCH_FACES, SWITCH_DELAY);
+                }
+                return;
+            }
+        }
+        if (mStateSwitchPending) {
+            mStateSwitchPending = false;
+            mHandler.removeMessages(MSG_SWITCH_FACES);
+        }
+        mFaces = faces;
+        invalidate();
+    }
+
+    public void setDisplayOrientation(int orientation) {
+        mDisplayOrientation = orientation;
+        if (LOGV) Log.v(TAG, "mDisplayOrientation=" + orientation);
+    }
+
+    @Override
+    public void setOrientation(int orientation, boolean animation) {
+        mOrientation = orientation;
+        invalidate();
+    }
+
+    public void setMirror(boolean mirror) {
+        mMirror = mirror;
+        if (LOGV) Log.v(TAG, "mMirror=" + mirror);
+    }
+
+    public boolean faceExists() {
+        return (mFaces != null && mFaces.length > 0);
+    }
+
+    @Override
+    public void showStart() {
+        mColor = mFocusingColor;
+        invalidate();
+    }
+
+    // Ignore the parameter. No autofocus animation for face detection.
+    @Override
+    public void showSuccess(boolean timeout) {
+        mColor = mFocusedColor;
+        invalidate();
+    }
+
+    // Ignore the parameter. No autofocus animation for face detection.
+    @Override
+    public void showFail(boolean timeout) {
+        mColor = mFailColor;
+        invalidate();
+    }
+
+    @Override
+    public void clear() {
+        // Face indicator is displayed during preview. Do not clear the
+        // drawable.
+        mColor = mFocusingColor;
+        mFaces = null;
+        invalidate();
+    }
+
+    public void pause() {
+        mPause = true;
+    }
+
+    public void resume() {
+        mPause = false;
+    }
+
+    public void setBlockDraw(boolean block) {
+        mBlocked = block;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (!mBlocked && (mFaces != null) && (mFaces.length > 0)) {
+            int rw, rh;
+            rw = mUncroppedWidth;
+            rh = mUncroppedHeight;
+            // Prepare the matrix.
+            if (((rh > rw) && ((mDisplayOrientation == 0) || (mDisplayOrientation == 180)))
+                    || ((rw > rh) && ((mDisplayOrientation == 90) || (mDisplayOrientation == 270)))) {
+                int temp = rw;
+                rw = rh;
+                rh = temp;
+            }
+            Util.prepareMatrix(mMatrix, mMirror, mDisplayOrientation, rw, rh);
+            int dx = (getWidth() - rw) / 2;
+            int dy = (getHeight() - rh) / 2;
+
+            // Focus indicator is directional. Rotate the matrix and the canvas
+            // so it looks correctly in all orientations.
+            canvas.save();
+            mMatrix.postRotate(mOrientation); // postRotate is clockwise
+            canvas.rotate(-mOrientation); // rotate is counter-clockwise (for canvas)
+            for (int i = 0; i < mFaces.length; i++) {
+                // Filter out false positives.
+                if (mFaces[i].score < 50) continue;
+
+                // Transform the coordinates.
+                mRect.set(mFaces[i].rect);
+                if (LOGV) Util.dumpRect(mRect, "Original rect");
+                mMatrix.mapRect(mRect);
+                if (LOGV) Util.dumpRect(mRect, "Transformed rect");
+                mPaint.setColor(mColor);
+                mRect.offset(dx, dy);
+                canvas.drawOval(mRect, mPaint);
+            }
+            canvas.restore();
+        }
+        super.onDraw(canvas);
+    }
+}
diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
new file mode 100644
index 0000000..f870b58
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector.
+public class FilmStripGestureRecognizer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilmStripGestureRecognizer";
+
+    public interface Listener {
+        boolean onSingleTapUp(float x, float y);
+        boolean onDoubleTap(float x, float y);
+        boolean onScroll(float x, float y, float dx, float dy);
+        boolean onFling(float velocityX, float velocityY);
+        boolean onScaleBegin(float focusX, float focusY);
+        boolean onScale(float focusX, float focusY, float scale);
+        boolean onDown(float x, float y);
+        boolean onUp(float x, float y);
+        void onScaleEnd();
+    }
+
+    private final GestureDetector mGestureDetector;
+    private final ScaleGestureDetector mScaleDetector;
+    private final Listener mListener;
+
+    public FilmStripGestureRecognizer(Context context, Listener listener) {
+        mListener = listener;
+        mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+                null, true /* ignoreMultitouch */);
+        mScaleDetector = new ScaleGestureDetector(
+                context, new MyScaleListener());
+    }
+
+    public void onTouchEvent(MotionEvent event) {
+        mGestureDetector.onTouchEvent(event);
+        mScaleDetector.onTouchEvent(event);
+        if (event.getAction() == MotionEvent.ACTION_UP) {
+            mListener.onUp(event.getX(), event.getY());
+        }
+    }
+
+    private class MyGestureListener
+                extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            return mListener.onSingleTapUp(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            return mListener.onDoubleTap(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onScroll(
+                MotionEvent e1, MotionEvent e2, float dx, float dy) {
+            return mListener.onScroll(e2.getX(), e2.getY(), dx, dy);
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                float velocityY) {
+            return mListener.onFling(velocityX, velocityY);
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            mListener.onDown(e.getX(), e.getY());
+            return super.onDown(e);
+        }
+    }
+
+    private class MyScaleListener
+            extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            return mListener.onScaleBegin(
+                    detector.getFocusX(), detector.getFocusY());
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            return mListener.onScale(detector.getFocusX(),
+                    detector.getFocusY(), detector.getScaleFactor());
+        }
+
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            mListener.onScaleEnd();
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java
new file mode 100644
index 0000000..8a1a85a
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripView.java
@@ -0,0 +1,1720 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.Scroller;
+
+import com.android.camera.ui.FilmStripView.ImageData.PanoramaSupportCallback;
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+public class FilmStripView extends ViewGroup {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilmStripView";
+
+    private static final int BUFFER_SIZE = 5;
+    private static final int DURATION_GEOMETRY_ADJUST = 200;
+    private static final float FILM_STRIP_SCALE = 0.6f;
+    private static final float FULLSCREEN_SCALE = 1f;
+    // Only check for intercepting touch events within first 500ms
+    private static final int SWIPE_TIME_OUT = 500;
+
+    private Context mContext;
+    private FilmStripGestureRecognizer mGestureRecognizer;
+    private DataAdapter mDataAdapter;
+    private int mViewGap;
+    private final Rect mDrawArea = new Rect();
+
+    private final int mCurrentInfo = (BUFFER_SIZE - 1) / 2;
+    private float mScale;
+    private MyController mController;
+    private int mCenterX = -1;
+    private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE];
+
+    private Listener mListener;
+
+    private MotionEvent mDown;
+    private boolean mCheckToIntercept = true;
+    private View mCameraView;
+    private int mSlop;
+    private TimeInterpolator mViewAnimInterpolator;
+
+    private ImageButton mViewPhotoSphereButton;
+    private PanoramaViewHelper mPanoramaViewHelper;
+    private long mLastItemId = -1;
+
+    // This is used to resolve the misalignment problem when the device
+    // orientation is changed. If the current item is in fullscreen, it might
+    // be shifted because mCenterX is not adjusted with the orientation.
+    // Set this to true when onSizeChanged is called to make sure we adjust
+    // mCenterX accordingly.
+    private boolean mAnchorPending;
+
+    /**
+     * Common interface for all images in the filmstrip.
+     */
+    public interface ImageData {
+        /**
+         * Interface that is used to tell the caller whether an image is a photo
+         * sphere.
+         */
+        public static interface PanoramaSupportCallback {
+            /**
+             * Called then photo sphere info has been loaded.
+             *
+             * @param isPanorama whether the image is a valid photo sphere
+             * @param isPanorama360 whether the photo sphere is a full 360
+             *            degree horizontal panorama
+             */
+            void panoramaInfoAvailable(boolean isPanorama,
+                    boolean isPanorama360);
+        }
+
+        // Image data types.
+        public static final int TYPE_NONE = 0;
+        public static final int TYPE_CAMERA_PREVIEW = 1;
+        public static final int TYPE_PHOTO = 2;
+        public static final int TYPE_VIDEO = 3;
+
+        // Actions allowed to be performed on the image data.
+        // The actions are defined bit-wise so we can use bit operations like
+        // | and &.
+        public static final int ACTION_NONE = 0;
+        public static final int ACTION_PROMOTE = 1;
+        public static final int ACTION_DEMOTE = (1 << 1);
+
+        /**
+         * SIZE_FULL can be returned by {@link ImageData#getWidth()} and
+         * {@link ImageData#getHeight()}.
+         * When SIZE_FULL is returned for width/height, it means the the
+         * width or height will be disregarded when deciding the view size
+         * of this ImageData, just use full screen size.
+         */
+        public static final int SIZE_FULL = -2;
+
+        /**
+         * Returns the width of the image. The final layout of the view returned
+         * by {@link DataAdapter#getView(android.content.Context, int)} will
+         * preserve the aspect ratio of
+         * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and
+         * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}.
+         */
+        public int getWidth();
+
+
+        /**
+         * Returns the width of the image. The final layout of the view returned
+         * by {@link DataAdapter#getView(android.content.Context, int)} will
+         * preserve the aspect ratio of
+         * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and
+         * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}.
+         */
+        public int getHeight();
+
+        /** Returns the image data type */
+        public int getType();
+
+        /**
+         * Checks if the UI action is supported.
+         *
+         * @param action The UI actions to check.
+         * @return       {@code false} if at least one of the actions is not
+         *               supported. {@code true} otherwise.
+         */
+        public boolean isUIActionSupported(int action);
+
+        /**
+         * Gives the data a hint when its view is going to be displayed.
+         * {@code FilmStripView} should always call this function before
+         * showing its corresponding view every time.
+         */
+        public void prepare();
+
+        /**
+         * Gives the data a hint when its view is going to be removed from the
+         * view hierarchy. {@code FilmStripView} should always call this
+         * function after its corresponding view is removed from the view
+         * hierarchy.
+         */
+        public void recycle();
+
+        /**
+         * Asynchronously checks if the image is a photo sphere. Notified the
+         * callback when the results are available.
+         */
+        public void isPhotoSphere(Context context, PanoramaSupportCallback callback);
+
+        /**
+         * If the item is a valid photo sphere panorama, this method will launch
+         * the viewer.
+         */
+        public void viewPhotoSphere(PanoramaViewHelper helper);
+    }
+
+    /**
+     * An interfaces which defines the interactions between the
+     * {@link ImageData} and the {@link FilmStripView}.
+     */
+    public interface DataAdapter {
+        /**
+         * An interface which defines the update report used to return to
+         * the {@link com.android.camera.ui.FilmStripView.Listener}.
+         */
+        public interface UpdateReporter {
+            /** Checks if the data of dataID is removed. */
+            public boolean isDataRemoved(int dataID);
+
+            /** Checks if the data of dataID is updated. */
+            public boolean isDataUpdated(int dataID);
+        }
+
+        /**
+         * An interface which defines the listener for UI actions over
+         * {@link ImageData}.
+         */
+        public interface Listener {
+            // Called when the whole data loading is done. No any assumption
+            // on previous data.
+            public void onDataLoaded();
+
+            // Only some of the data is changed. The listener should check
+            // if any thing needs to be updated.
+            public void onDataUpdated(UpdateReporter reporter);
+
+            public void onDataInserted(int dataID, ImageData data);
+
+            public void onDataRemoved(int dataID, ImageData data);
+        }
+
+        /** Returns the total number of image data */
+        public int getTotalNumber();
+
+        /**
+         * Returns the view to visually present the image data.
+         *
+         * @param context  The {@link Context} to create the view.
+         * @param dataID   The ID of the image data to be presented.
+         * @return         The view representing the image data. Null if
+         *                 unavailable or the {@code dataID} is out of range.
+         */
+        public View getView(Context context, int dataID);
+
+        /**
+         * Returns the {@link ImageData} specified by the ID.
+         *
+         * @param dataID The ID of the {@link ImageData}.
+         * @return       The specified {@link ImageData}. Null if not available.
+         */
+        public ImageData getImageData(int dataID);
+
+        /**
+         * Suggests the data adapter the maximum possible size of the layout
+         * so the {@link DataAdapter} can optimize the view returned for the
+         * {@link ImageData}.
+         *
+         * @param w Maximum width.
+         * @param h Maximum height.
+         */
+        public void suggestViewSizeBound(int w, int h);
+
+        /**
+         * Sets the listener for FilmStripView UI actions over the ImageData.
+         *
+         * @param listener The listener to use.
+         */
+        public void setListener(Listener listener);
+
+        /**
+         * The callback when the item enters/leaves full-screen.
+         * TODO: Call this function actually.
+         *
+         * @param dataID      The ID of the image data.
+         * @param fullScreen  {@code true} if the data is entering full-screen.
+         *                    {@code false} otherwise.
+         */
+        public void onDataFullScreen(int dataID, boolean fullScreen);
+
+        /**
+         * The callback when the item is centered/off-centered.
+         * TODO: Calls this function actually.
+         *
+         * @param dataID      The ID of the image data.
+         * @param centered    {@code true} if the data is centered.
+         *                    {@code false} otherwise.
+         */
+        public void onDataCentered(int dataID, boolean centered);
+
+        /**
+         * Returns {@code true} if the view of the data can be moved by swipe
+         * gesture when in full-screen.
+         *
+         * @param dataID The ID of the data.
+         * @return       {@code true} if the view can be moved,
+         *               {@code false} otherwise.
+         */
+        public boolean canSwipeInFullScreen(int dataID);
+    }
+
+    /**
+     * An interface which defines the FilmStripView UI action listener.
+     */
+    public interface Listener {
+        /**
+         * Callback when the data is promoted.
+         *
+         * @param dataID The ID of the promoted data.
+         */
+        public void onDataPromoted(int dataID);
+
+        /**
+         * Callback when the data is demoted.
+         *
+         * @param dataID The ID of the demoted data.
+         */
+        public void onDataDemoted(int dataID);
+
+        public void onDataFullScreenChange(int dataID, boolean full);
+
+        /**
+         * Callback when entering/leaving camera mode.
+         *
+         * @param toCamera {@code true} if entering camera mode. Otherwise,
+         *                 {@code false}
+         */
+        public void onSwitchMode(boolean toCamera);
+    }
+
+    /**
+     * An interface which defines the controller of {@link FilmStripView}.
+     */
+    public interface Controller {
+        public boolean isScalling();
+
+        public void scroll(float deltaX);
+
+        public void fling(float velocity);
+
+        public void scrollTo(int position, int duration, boolean interruptible);
+
+        public boolean stopScrolling();
+
+        public boolean isScrolling();
+
+        public void lockAtCurrentView();
+
+        public void unlockPosition();
+
+        public void gotoCameraFullScreen();
+
+        public void gotoFilmStrip();
+
+        public void gotoFullScreen();
+    }
+
+    /**
+     * A helper class to tract and calculate the view coordination.
+     */
+    private static class ViewInfo {
+        private int mDataID;
+        /** The position of the left of the view in the whole filmstrip. */
+        private int mLeftPosition;
+        private View mView;
+        private RectF mViewArea;
+
+        public ViewInfo(int id, View v) {
+            v.setPivotX(0f);
+            v.setPivotY(0f);
+            mDataID = id;
+            mView = v;
+            mLeftPosition = -1;
+            mViewArea = new RectF();
+        }
+
+        public int getID() {
+            return mDataID;
+        }
+
+        public void setID(int id) {
+            mDataID = id;
+        }
+
+        public void setLeftPosition(int pos) {
+            mLeftPosition = pos;
+        }
+
+        public int getLeftPosition() {
+            return mLeftPosition;
+        }
+
+        public float getTranslationY(float scale) {
+            return mView.getTranslationY() / scale;
+        }
+
+        public float getTranslationX(float scale) {
+            return mView.getTranslationX();
+        }
+
+        public void setTranslationY(float transY, float scale) {
+            mView.setTranslationY(transY * scale);
+        }
+
+        public void setTranslationX(float transX, float scale) {
+            mView.setTranslationX(transX * scale);
+        }
+
+        public void translateXBy(float transX, float scale) {
+            mView.setTranslationX(mView.getTranslationX() + transX * scale);
+        }
+
+        public int getCenterX() {
+            return mLeftPosition + mView.getWidth() / 2;
+        }
+
+        public View getView() {
+            return mView;
+        }
+
+        private void layoutAt(int left, int top) {
+            mView.layout(left, top, left + mView.getMeasuredWidth(),
+                    top + mView.getMeasuredHeight());
+        }
+
+        public void layoutIn(Rect drawArea, int refCenter, float scale) {
+            // drawArea is where to layout in.
+            // refCenter is the absolute horizontal position of the center of
+            // drawArea.
+            int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale);
+            int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
+            layoutAt(left, top);
+            mView.setScaleX(scale);
+            mView.setScaleY(scale);
+
+            // update mViewArea for touch detection.
+            int l = mView.getLeft();
+            int t = mView.getTop();
+            mViewArea.set(l, t,
+                    l + mView.getWidth() * scale,
+                    t + mView.getHeight() * scale);
+        }
+
+        public boolean areaContains(float x, float y) {
+            return mViewArea.contains(x, y);
+        }
+    }
+
+    /** Constructor. */
+    public FilmStripView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    /** Constructor. */
+    public FilmStripView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /** Constructor. */
+    public FilmStripView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    private void init(Context context) {
+        // This is for positioning camera controller at the same place in
+        // different orientations.
+        setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+
+        setWillNotDraw(false);
+        mContext = context;
+        mScale = 1.0f;
+        mController = new MyController(context);
+        mViewAnimInterpolator = new DecelerateInterpolator();
+        mGestureRecognizer =
+                new FilmStripGestureRecognizer(context, new MyGestureReceiver());
+        mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
+    }
+
+    /**
+     * Returns the controller.
+     *
+     * @return The {@code Controller}.
+     */
+    public Controller getController() {
+        return mController;
+    }
+
+    public void setListener(Listener l) {
+        mListener = l;
+    }
+
+    public void setViewGap(int viewGap) {
+        mViewGap = viewGap;
+    }
+
+    /**
+     * Sets the helper that's to be used to open photo sphere panoramas.
+     */
+    public void setPanoramaViewHelper(PanoramaViewHelper helper) {
+        mPanoramaViewHelper = helper;
+    }
+
+    public float getScale() {
+        return mScale;
+    }
+
+    public boolean isAnchoredTo(int id) {
+        if (mViewInfo[mCurrentInfo].getID() == id
+                && mViewInfo[mCurrentInfo].getCenterX() == mCenterX) {
+            return true;
+        }
+        return false;
+    }
+
+    public int getCurrentType() {
+        if (mDataAdapter == null) {
+            return ImageData.TYPE_NONE;
+        }
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        if (curr == null) {
+            return ImageData.TYPE_NONE;
+        }
+        return mDataAdapter.getImageData(curr.getID()).getType();
+    }
+
+    @Override
+    public void onDraw(Canvas c) {
+        if (mController.hasNewGeometry()) {
+            layoutChildren();
+        }
+    }
+
+    /** Returns [width, height] preserving image aspect ratio. */
+    private int[] calculateChildDimension(
+            int imageWidth, int imageHeight,
+            int boundWidth, int boundHeight) {
+
+        if (imageWidth == ImageData.SIZE_FULL
+                || imageHeight == ImageData.SIZE_FULL) {
+            imageWidth = boundWidth;
+            imageHeight = boundHeight;
+        }
+
+        int[] ret = new int[2];
+        ret[0] = boundWidth;
+        ret[1] = boundHeight;
+
+        if (imageWidth * ret[1] > ret[0] * imageHeight) {
+            ret[1] = imageHeight * ret[0] / imageWidth;
+        } else {
+            ret[0] = imageWidth * ret[1] / imageHeight;
+        }
+
+        return ret;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
+        if (boundWidth == 0 || boundHeight == 0) {
+            // Either width or height is unknown, can't measure children yet.
+            return;
+        }
+
+        if (mDataAdapter != null) {
+            mDataAdapter.suggestViewSizeBound(boundWidth / 2, boundHeight / 2);
+        }
+
+        for (ViewInfo info : mViewInfo) {
+            if (info == null) continue;
+
+            int id = info.getID();
+            int[] dim = calculateChildDimension(
+                    mDataAdapter.getImageData(id).getWidth(),
+                    mDataAdapter.getImageData(id).getHeight(),
+                    boundWidth, boundHeight);
+
+            info.getView().measure(
+                    MeasureSpec.makeMeasureSpec(
+                            dim[0], MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(
+                            dim[1], MeasureSpec.EXACTLY));
+        }
+    }
+
+    @Override
+    protected boolean fitSystemWindows(Rect insets) {
+        if (mViewPhotoSphereButton != null) {
+            // Set the position of the "View Photo Sphere" button.
+            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mViewPhotoSphereButton
+                    .getLayoutParams();
+            params.bottomMargin = insets.bottom;
+            mViewPhotoSphereButton.setLayoutParams(params);
+        }
+
+        return super.fitSystemWindows(insets);
+    }
+
+    private int findTheNearestView(int pointX) {
+
+        int nearest = 0;
+        // Find the first non-null ViewInfo.
+        while (nearest < BUFFER_SIZE
+                && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1)) {
+            nearest++;
+        }
+        // No existing available ViewInfo
+        if (nearest == BUFFER_SIZE) {
+            return -1;
+        }
+        int min = Math.abs(pointX - mViewInfo[nearest].getCenterX());
+
+        for (int infoID = nearest + 1; infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) {
+            // Not measured yet.
+            if (mViewInfo[infoID].getLeftPosition() == -1)
+                continue;
+
+            int c = mViewInfo[infoID].getCenterX();
+            int dist = Math.abs(pointX - c);
+            if (dist < min) {
+                min = dist;
+                nearest = infoID;
+            }
+        }
+        return nearest;
+    }
+
+    private ViewInfo buildInfoFromData(int dataID) {
+        ImageData data = mDataAdapter.getImageData(dataID);
+        if (data == null) {
+            return null;
+        }
+        data.prepare();
+        View v = mDataAdapter.getView(mContext, dataID);
+        if (v == null) {
+            return null;
+        }
+        ViewInfo info = new ViewInfo(dataID, v);
+        v = info.getView();
+        if (v != mCameraView) {
+            addView(info.getView());
+        } else {
+            v.setVisibility(View.VISIBLE);
+        }
+        return info;
+    }
+
+    private void removeInfo(int infoID) {
+        if (infoID >= mViewInfo.length || mViewInfo[infoID] == null) {
+            return;
+        }
+
+        ImageData data = mDataAdapter.getImageData(mViewInfo[infoID].getID());
+        checkForRemoval(data, mViewInfo[infoID].getView());
+        mViewInfo[infoID] = null;
+    }
+
+    /**
+     * We try to keep the one closest to the center of the screen at position
+     * mCurrentInfo.
+     */
+    private void stepIfNeeded() {
+        if (!inFilmStrip() && !inFullScreen()) {
+            // The good timing to step to the next view is when everything is
+            // not in
+            // transition.
+            return;
+        }
+        int nearest = findTheNearestView(mCenterX);
+        // no change made.
+        if (nearest == -1 || nearest == mCurrentInfo)
+            return;
+
+        int adjust = nearest - mCurrentInfo;
+        if (adjust > 0) {
+            for (int k = 0; k < adjust; k++) {
+                removeInfo(k);
+            }
+            for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
+                mViewInfo[k] = mViewInfo[k + adjust];
+            }
+            for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
+                mViewInfo[k] = null;
+                if (mViewInfo[k - 1] != null) {
+                    mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1);
+                }
+            }
+        } else {
+            for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
+                removeInfo(k);
+            }
+            for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
+                mViewInfo[k] = mViewInfo[k + adjust];
+            }
+            for (int k = -1 - adjust; k >= 0; k--) {
+                mViewInfo[k] = null;
+                if (mViewInfo[k + 1] != null) {
+                    mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1);
+                }
+            }
+        }
+    }
+
+    /** Don't go beyond the bound. */
+    private void clampCenterX() {
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        if (curr == null) {
+            return;
+        }
+
+        if (curr.getID() == 0 && mCenterX < curr.getCenterX()) {
+            mCenterX = curr.getCenterX();
+            if (mController.isScrolling()) {
+                mController.stopScrolling();
+            }
+            if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW
+                    && !mController.isScalling()
+                    && mScale != FULLSCREEN_SCALE) {
+                mController.gotoFullScreen();
+            }
+        }
+        if (curr.getID() == mDataAdapter.getTotalNumber() - 1
+                && mCenterX > curr.getCenterX()) {
+            mCenterX = curr.getCenterX();
+            if (!mController.isScrolling()) {
+                mController.stopScrolling();
+            }
+        }
+    }
+
+    private void adjustChildZOrder() {
+        for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
+            if (mViewInfo[i] == null)
+                continue;
+            bringChildToFront(mViewInfo[i].getView());
+        }
+    }
+
+    /**
+     * If the current photo is a photo sphere, this will launch the Photo Sphere
+     * panorama viewer.
+     */
+    private void showPhotoSphere() {
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        if (curr != null) {
+            mDataAdapter.getImageData(curr.getID()).viewPhotoSphere(mPanoramaViewHelper);
+        }
+    }
+
+    /**
+     * @return The ID of the current item, or -1.
+     */
+    private int getCurrentId() {
+        ViewInfo current = mViewInfo[mCurrentInfo];
+        if (current == null) {
+            return -1;
+        }
+        return current.getID();
+    }
+
+    /**
+     * Updates the visibility of the View Photo Sphere button.
+     */
+    private void updatePhotoSphereViewButton() {
+        if (mViewPhotoSphereButton == null) {
+            mViewPhotoSphereButton = (ImageButton) ((View) getParent())
+                    .findViewById(R.id.filmstrip_bottom_control_panorama);
+            mViewPhotoSphereButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View view) {
+                    showPhotoSphere();
+                }
+            });
+        }
+        final int requestId = getCurrentId();
+
+        // Check if the item has changed since the last time we updated the
+        // visibility status. Only then check of the current image is a photo
+        // sphere.
+        if (requestId == mLastItemId || requestId < 0) {
+            return;
+        }
+
+        ImageData data = mDataAdapter.getImageData(requestId);
+        data.isPhotoSphere(mContext, new PanoramaSupportCallback() {
+            @Override
+            public void panoramaInfoAvailable(final boolean isPanorama,
+                    boolean isPanorama360) {
+                // Make sure the returned data is for the current image.
+                if (requestId == getCurrentId()) {
+                    mViewPhotoSphereButton.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mViewPhotoSphereButton.setVisibility(isPanorama ? View.VISIBLE
+                                    : View.GONE);
+                        }
+                    });
+                }
+            }
+        });
+    }
+
+    private void layoutChildren() {
+        if (mAnchorPending) {
+            mCenterX = mViewInfo[mCurrentInfo].getCenterX();
+            mAnchorPending = false;
+        }
+
+        if (mController.hasNewGeometry()) {
+            mCenterX = mController.getNewPosition();
+            mScale = mController.getNewScale();
+        }
+
+        clampCenterX();
+
+        mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterX, mScale);
+
+        int currentViewLeft = mViewInfo[mCurrentInfo].getLeftPosition();
+        int currentViewCenter = mViewInfo[mCurrentInfo].getCenterX();
+        int fullScreenWidth = mDrawArea.width() + mViewGap;
+        float scaleFraction = mViewAnimInterpolator.getInterpolation(
+                (mScale - FILM_STRIP_SCALE) / (FULLSCREEN_SCALE - FILM_STRIP_SCALE));
+
+        // images on the left
+        for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) {
+            ViewInfo curr = mViewInfo[infoID];
+            if (curr == null) {
+                continue;
+            }
+
+            ViewInfo next = mViewInfo[infoID + 1];
+            int myLeft =
+                    next.getLeftPosition() - curr.getView().getMeasuredWidth() - mViewGap;
+            curr.setLeftPosition(myLeft);
+            curr.layoutIn(mDrawArea, mCenterX, mScale);
+            curr.getView().setAlpha(1f);
+            int infoDiff = mCurrentInfo - infoID;
+            curr.setTranslationX(
+                    (currentViewCenter
+                            - fullScreenWidth * infoDiff - curr.getCenterX()) * scaleFraction,
+                    mScale);
+        }
+
+        // images on the right
+        for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) {
+            ViewInfo curr = mViewInfo[infoID];
+            if (curr == null) {
+                continue;
+            }
+
+            ViewInfo prev = mViewInfo[infoID - 1];
+            int myLeft =
+                    prev.getLeftPosition() + prev.getView().getMeasuredWidth() + mViewGap;
+            curr.setLeftPosition(myLeft);
+            curr.layoutIn(mDrawArea, mCenterX, mScale);
+            if (infoID == mCurrentInfo + 1) {
+                curr.getView().setAlpha(1f - scaleFraction);
+            } else {
+                if (scaleFraction == 0f) {
+                    curr.getView().setAlpha(1f);
+                } else {
+                    curr.getView().setAlpha(0f);
+                }
+            }
+            curr.setTranslationX((currentViewLeft - myLeft) * scaleFraction, mScale);
+        }
+
+        stepIfNeeded();
+        adjustChildZOrder();
+        invalidate();
+        updatePhotoSphereViewButton();
+        mLastItemId = getCurrentId();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (mViewInfo[mCurrentInfo] == null) {
+            return;
+        }
+
+        mDrawArea.left = l;
+        mDrawArea.top = t;
+        mDrawArea.right = r;
+        mDrawArea.bottom = b;
+
+        layoutChildren();
+    }
+
+    // Keeps the view in the view hierarchy if it's camera preview.
+    // Remove from the hierarchy otherwise.
+    private void checkForRemoval(ImageData data, View v) {
+        if (data.getType() != ImageData.TYPE_CAMERA_PREVIEW) {
+            removeView(v);
+            data.recycle();
+        } else {
+            v.setVisibility(View.INVISIBLE);
+            if (mCameraView != null && mCameraView != v) {
+                removeView(mCameraView);
+            }
+            mCameraView = v;
+        }
+    }
+
+    private void slideViewBack(View v) {
+        v.animate().translationX(0)
+                .alpha(1f)
+                .setDuration(DURATION_GEOMETRY_ADJUST)
+                .setInterpolator(mViewAnimInterpolator)
+                .start();
+    }
+
+    private void updateRemoval(int dataID, final ImageData data) {
+        int removedInfo = findInfoByDataID(dataID);
+
+        // adjust the data id to be consistent
+        for (int i = 0; i < BUFFER_SIZE; i++) {
+            if (mViewInfo[i] == null || mViewInfo[i].getID() <= dataID) {
+                continue;
+            }
+            mViewInfo[i].setID(mViewInfo[i].getID() - 1);
+        }
+        if (removedInfo == -1) {
+            return;
+        }
+
+        final View removedView = mViewInfo[removedInfo].getView();
+        final int offsetX = removedView.getMeasuredWidth() + mViewGap;
+
+        for (int i = removedInfo + 1; i < BUFFER_SIZE; i++) {
+            if (mViewInfo[i] != null) {
+                mViewInfo[i].setLeftPosition(mViewInfo[i].getLeftPosition() - offsetX);
+            }
+        }
+
+        if (removedInfo >= mCurrentInfo
+                && mViewInfo[removedInfo].getID() < mDataAdapter.getTotalNumber()) {
+            // Fill the removed info by left shift when the current one or
+            // anyone on the right is removed, and there's more data on the
+            // right available.
+            for (int i = removedInfo; i < BUFFER_SIZE - 1; i++) {
+                mViewInfo[i] = mViewInfo[i + 1];
+            }
+
+            // pull data out from the DataAdapter for the last one.
+            int curr = BUFFER_SIZE - 1;
+            int prev = curr - 1;
+            if (mViewInfo[prev] != null) {
+                mViewInfo[curr] = buildInfoFromData(mViewInfo[prev].getID() + 1);
+            }
+
+            // Translate the views to their original places.
+            for (int i = removedInfo; i < BUFFER_SIZE; i++) {
+                if (mViewInfo[i] != null) {
+                    mViewInfo[i].setTranslationX(offsetX, mScale);
+                }
+            }
+
+            // The end of the filmstrip might have been changed.
+            // The mCenterX might be out of the bound.
+            ViewInfo currInfo = mViewInfo[mCurrentInfo];
+            if (currInfo.getID() == mDataAdapter.getTotalNumber() - 1
+                    && mCenterX > currInfo.getCenterX()) {
+                int adjustDiff = currInfo.getCenterX() - mCenterX;
+                mCenterX = currInfo.getCenterX();
+                for (int i = 0; i < BUFFER_SIZE; i++) {
+                    if (mViewInfo[i] != null) {
+                        mViewInfo[i].translateXBy(adjustDiff, mScale);
+                    }
+                }
+            }
+        } else {
+            // fill the removed place by right shift
+            mCenterX -= offsetX;
+
+            for (int i = removedInfo; i > 0; i--) {
+                mViewInfo[i] = mViewInfo[i - 1];
+            }
+
+            // pull data out from the DataAdapter for the first one.
+            int curr = 0;
+            int next = curr + 1;
+            if (mViewInfo[next] != null) {
+                mViewInfo[curr] = buildInfoFromData(mViewInfo[next].getID() - 1);
+            }
+
+            // Translate the views to their original places.
+            for (int i = removedInfo; i >= 0; i--) {
+                if (mViewInfo[i] != null) {
+                    mViewInfo[i].setTranslationX(-offsetX, mScale);
+                }
+            }
+        }
+
+        // Now, slide every one back.
+        for (int i = 0; i < BUFFER_SIZE; i++) {
+            if (mViewInfo[i] != null
+                    && mViewInfo[i].getTranslationX(mScale) != 0f) {
+                slideViewBack(mViewInfo[i].getView());
+            }
+        }
+
+        int transY = getHeight() / 8;
+        if (removedView.getTranslationY() < 0) {
+            transY = -transY;
+        }
+        removedView.animate()
+                .alpha(0f)
+                .translationYBy(transY)
+                .setInterpolator(mViewAnimInterpolator)
+                .setDuration(DURATION_GEOMETRY_ADJUST)
+                .withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        checkForRemoval(data, removedView);
+                    }
+                })
+                .start();
+        layoutChildren();
+    }
+
+    // returns -1 on failure.
+    private int findInfoByDataID(int dataID) {
+        for (int i = 0; i < BUFFER_SIZE; i++) {
+            if (mViewInfo[i] != null
+                    && mViewInfo[i].getID() == dataID) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private void updateInsertion(int dataID) {
+        int insertedInfo = findInfoByDataID(dataID);
+        if (insertedInfo == -1) {
+            // Not in the current info buffers. Check if it's inserted
+            // at the end.
+            if (dataID == mDataAdapter.getTotalNumber() - 1) {
+                int prev = findInfoByDataID(dataID - 1);
+                if (prev >= 0 && prev < BUFFER_SIZE - 1) {
+                    // The previous data is in the buffer and we still
+                    // have room for the inserted data.
+                    insertedInfo = prev + 1;
+                }
+            }
+        }
+
+        // adjust the data id to be consistent
+        for (int i = 0; i < BUFFER_SIZE; i++) {
+            if (mViewInfo[i] == null || mViewInfo[i].getID() < dataID) {
+                continue;
+            }
+            mViewInfo[i].setID(mViewInfo[i].getID() + 1);
+        }
+        if (insertedInfo == -1) {
+            return;
+        }
+
+        final ImageData data = mDataAdapter.getImageData(dataID);
+        int[] dim = calculateChildDimension(
+                data.getWidth(), data.getHeight(),
+                getMeasuredWidth(), getMeasuredHeight());
+        final int offsetX = dim[0] + mViewGap;
+        ViewInfo viewInfo = buildInfoFromData(dataID);
+
+        if (insertedInfo >= mCurrentInfo) {
+            if (insertedInfo == mCurrentInfo) {
+                viewInfo.setLeftPosition(mViewInfo[mCurrentInfo].getLeftPosition());
+            }
+            // Shift right to make rooms for newly inserted item.
+            removeInfo(BUFFER_SIZE - 1);
+            for (int i = BUFFER_SIZE - 1; i > insertedInfo; i--) {
+                mViewInfo[i] = mViewInfo[i - 1];
+                if (mViewInfo[i] != null) {
+                    mViewInfo[i].setTranslationX(-offsetX, mScale);
+                    slideViewBack(mViewInfo[i].getView());
+                }
+            }
+        } else {
+            // Shift left. Put the inserted data on the left instead of the
+            // found position.
+            --insertedInfo;
+            if (insertedInfo < 0) {
+                return;
+            }
+            removeInfo(0);
+            for (int i = 1; i <= insertedInfo; i++) {
+                if (mViewInfo[i] != null) {
+                    mViewInfo[i].setTranslationX(offsetX, mScale);
+                    slideViewBack(mViewInfo[i].getView());
+                    mViewInfo[i - 1] = mViewInfo[i];
+                }
+            }
+        }
+
+        mViewInfo[insertedInfo] = viewInfo;
+        View insertedView = mViewInfo[insertedInfo].getView();
+        insertedView.setAlpha(0f);
+        insertedView.setTranslationY(getHeight() / 8);
+        insertedView.animate()
+                .alpha(1f)
+                .translationY(0f)
+                .setInterpolator(mViewAnimInterpolator)
+                .setDuration(DURATION_GEOMETRY_ADJUST)
+                .start();
+        invalidate();
+    }
+
+    public void setDataAdapter(DataAdapter adapter) {
+        mDataAdapter = adapter;
+        mDataAdapter.suggestViewSizeBound(getMeasuredWidth(), getMeasuredHeight());
+        mDataAdapter.setListener(new DataAdapter.Listener() {
+            @Override
+            public void onDataLoaded() {
+                reload();
+            }
+
+            @Override
+            public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
+                update(reporter);
+            }
+
+            @Override
+            public void onDataInserted(int dataID, ImageData data) {
+                if (mViewInfo[mCurrentInfo] == null) {
+                    // empty now, simply do a reload.
+                    reload();
+                    return;
+                }
+                updateInsertion(dataID);
+            }
+
+            @Override
+            public void onDataRemoved(int dataID, ImageData data) {
+                updateRemoval(dataID, data);
+            }
+        });
+    }
+
+    public boolean inFilmStrip() {
+        return (mScale == FILM_STRIP_SCALE);
+    }
+
+    public boolean inFullScreen() {
+        return (mScale == FULLSCREEN_SCALE);
+    }
+
+    public boolean inCameraFullscreen() {
+        return isAnchoredTo(0) && inFullScreen()
+                && (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (inFilmStrip()) {
+            return true;
+        }
+
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mCheckToIntercept = true;
+            mDown = MotionEvent.obtain(ev);
+            ViewInfo viewInfo = mViewInfo[mCurrentInfo];
+            // Do not intercept touch if swipe is not enabled
+            if (viewInfo != null && !mDataAdapter.canSwipeInFullScreen(viewInfo.getID())) {
+                mCheckToIntercept = false;
+            }
+            return false;
+        } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+            // Do not intercept touch once child is in zoom mode
+            mCheckToIntercept = false;
+            return false;
+        } else {
+            if (!mCheckToIntercept) {
+                return false;
+            }
+            if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) {
+                return false;
+            }
+            int deltaX = (int) (ev.getX() - mDown.getX());
+            int deltaY = (int) (ev.getY() - mDown.getY());
+            if (ev.getActionMasked() == MotionEvent.ACTION_MOVE
+                    && deltaX < mSlop * (-1)) {
+                // intercept left swipe
+                if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        mGestureRecognizer.onTouchEvent(ev);
+        return true;
+    }
+
+    private void updateViewInfo(int infoID) {
+        ViewInfo info = mViewInfo[infoID];
+        removeView(info.getView());
+        mViewInfo[infoID] = buildInfoFromData(info.getID());
+    }
+
+    /** Some of the data is changed. */
+    private void update(DataAdapter.UpdateReporter reporter) {
+        // No data yet.
+        if (mViewInfo[mCurrentInfo] == null) {
+            reload();
+            return;
+        }
+
+        // Check the current one.
+        ViewInfo curr = mViewInfo[mCurrentInfo];
+        int dataID = curr.getID();
+        if (reporter.isDataRemoved(dataID)) {
+            mCenterX = -1;
+            reload();
+            return;
+        }
+        if (reporter.isDataUpdated(dataID)) {
+            updateViewInfo(mCurrentInfo);
+        }
+
+        // Check left
+        for (int i = mCurrentInfo - 1; i >= 0; i--) {
+            curr = mViewInfo[i];
+            if (curr != null) {
+                dataID = curr.getID();
+                if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
+                    updateViewInfo(i);
+                }
+            } else {
+                ViewInfo next = mViewInfo[i + 1];
+                if (next != null) {
+                    mViewInfo[i] = buildInfoFromData(next.getID() - 1);
+                }
+            }
+        }
+
+        // Check right
+        for (int i = mCurrentInfo + 1; i < BUFFER_SIZE; i++) {
+            curr = mViewInfo[i];
+            if (curr != null) {
+                dataID = curr.getID();
+                if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
+                    updateViewInfo(i);
+                }
+            } else {
+                ViewInfo prev = mViewInfo[i - 1];
+                if (prev != null) {
+                    mViewInfo[i] = buildInfoFromData(prev.getID() + 1);
+                }
+            }
+        }
+    }
+
+    /**
+     * The whole data might be totally different. Flush all and load from the
+     * start.
+     */
+    private void reload() {
+        removeAllViews();
+        int dataNumber = mDataAdapter.getTotalNumber();
+        if (dataNumber == 0) {
+            return;
+        }
+
+        mViewInfo[mCurrentInfo] = buildInfoFromData(0);
+        mViewInfo[mCurrentInfo].setLeftPosition(0);
+        if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) {
+            // we are in camera mode by default.
+            mController.lockAtCurrentView();
+        }
+        for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) {
+            int infoID = mCurrentInfo + i;
+            if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) {
+                mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1);
+            }
+            infoID = mCurrentInfo - i;
+            if (infoID >= 0 && mViewInfo[infoID + 1] != null) {
+                mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1);
+            }
+        }
+        layoutChildren();
+    }
+
+    private void promoteData(int infoID, int dataID) {
+        if (mListener != null) {
+            mListener.onDataPromoted(dataID);
+        }
+    }
+
+    private void demoteData(int infoID, int dataID) {
+        if (mListener != null) {
+            mListener.onDataDemoted(dataID);
+        }
+    }
+
+    /**
+     * MyController controls all the geometry animations. It passively tells the
+     * geometry information on demand.
+     */
+    private class MyController implements
+            Controller,
+            ValueAnimator.AnimatorUpdateListener,
+            Animator.AnimatorListener {
+
+        private ValueAnimator mScaleAnimator;
+        private boolean mHasNewScale;
+        private float mNewScale;
+
+        private Scroller mScroller;
+        private boolean mHasNewPosition;
+        private DecelerateInterpolator mDecelerateInterpolator;
+
+        private boolean mCanStopScroll;
+
+        private boolean mIsPositionLocked;
+        private int mLockedViewInfo;
+
+        MyController(Context context) {
+            mScroller = new Scroller(context);
+            mHasNewPosition = false;
+            mScaleAnimator = new ValueAnimator();
+            mScaleAnimator.addUpdateListener(MyController.this);
+            mScaleAnimator.addListener(MyController.this);
+            mDecelerateInterpolator = new DecelerateInterpolator();
+            mCanStopScroll = true;
+            mHasNewScale = false;
+        }
+
+        @Override
+        public boolean isScrolling() {
+            return !mScroller.isFinished();
+        }
+
+        @Override
+        public boolean isScalling() {
+            return mScaleAnimator.isRunning();
+        }
+
+        boolean hasNewGeometry() {
+            mHasNewPosition = mScroller.computeScrollOffset();
+            if (!mHasNewPosition) {
+                mCanStopScroll = true;
+            }
+            // If the position is locked, then we always return true to force
+            // the position value to use the locked value.
+            return (mHasNewPosition || mHasNewScale || mIsPositionLocked);
+        }
+
+        /**
+         * Always call {@link #hasNewGeometry()} before getting the new scale
+         * value.
+         */
+        float getNewScale() {
+            if (!mHasNewScale) {
+                return mScale;
+            }
+            mHasNewScale = false;
+            return mNewScale;
+        }
+
+        /**
+         * Always call {@link #hasNewGeometry()} before getting the new position
+         * value.
+         */
+        int getNewPosition() {
+            if (mIsPositionLocked) {
+                if (mViewInfo[mLockedViewInfo] == null)
+                    return mCenterX;
+                return mViewInfo[mLockedViewInfo].getCenterX();
+            }
+            if (!mHasNewPosition)
+                return mCenterX;
+            return mScroller.getCurrX();
+        }
+
+        @Override
+        public void lockAtCurrentView() {
+            mIsPositionLocked = true;
+            mLockedViewInfo = mCurrentInfo;
+        }
+
+        @Override
+        public void unlockPosition() {
+            if (mIsPositionLocked) {
+                // only when the position is previously locked we set the
+                // current position to make it consistent.
+                if (mViewInfo[mLockedViewInfo] != null) {
+                    mCenterX = mViewInfo[mLockedViewInfo].getCenterX();
+                }
+                mIsPositionLocked = false;
+            }
+        }
+
+        private int estimateMinX(int dataID, int leftPos, int viewWidth) {
+            return leftPos - (dataID + 100) * (viewWidth + mViewGap);
+        }
+
+        private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
+            return leftPos
+                    + (mDataAdapter.getTotalNumber() - dataID + 100)
+                    * (viewWidth + mViewGap);
+        }
+
+        @Override
+        public void scroll(float deltaX) {
+            if (mController.isScrolling()) {
+                return;
+            }
+            mCenterX += deltaX;
+        }
+
+        @Override
+        public void fling(float velocityX) {
+            if (!stopScrolling() || mIsPositionLocked) {
+                return;
+            }
+            ViewInfo info = mViewInfo[mCurrentInfo];
+            if (info == null) {
+                return;
+            }
+
+            float scaledVelocityX = velocityX / mScale;
+            if (inCameraFullscreen() && scaledVelocityX < 0) {
+                // Swipe left in camera preview.
+                gotoFilmStrip();
+            }
+
+            int w = getWidth();
+            // Estimation of possible length on the left. To ensure the
+            // velocity doesn't become too slow eventually, we add a huge number
+            // to the estimated maximum.
+            int minX = estimateMinX(info.getID(), info.getLeftPosition(), w);
+            // Estimation of possible length on the right. Likewise, exaggerate
+            // the possible maximum too.
+            int maxX = estimateMaxX(info.getID(), info.getLeftPosition(), w);
+            mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
+
+            layoutChildren();
+        }
+
+        @Override
+        public boolean stopScrolling() {
+            if (!mCanStopScroll)
+                return false;
+            mScroller.forceFinished(true);
+            mHasNewPosition = false;
+            return true;
+        }
+
+        private void stopScale() {
+            mScaleAnimator.cancel();
+            mHasNewScale = false;
+        }
+
+        @Override
+        public void scrollTo(int position, int duration, boolean interruptible) {
+            if (!stopScrolling() || mIsPositionLocked)
+                return;
+            mCanStopScroll = interruptible;
+            stopScrolling();
+            mScroller.startScroll(mCenterX, 0, position - mCenterX,
+                    0, duration);
+            invalidate();
+        }
+
+        private void scaleTo(float scale, int duration) {
+            stopScale();
+            mScaleAnimator.setDuration(duration);
+            mScaleAnimator.setFloatValues(mScale, scale);
+            mScaleAnimator.setInterpolator(mDecelerateInterpolator);
+            mScaleAnimator.start();
+            mHasNewScale = true;
+            layoutChildren();
+        }
+
+        @Override
+        public void gotoFilmStrip() {
+            unlockPosition();
+            scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST);
+            if (mListener != null) {
+                mListener.onSwitchMode(false);
+            }
+        }
+
+        @Override
+        public void gotoFullScreen() {
+            if (mViewInfo[mCurrentInfo] != null) {
+                mController.scrollTo(mViewInfo[mCurrentInfo].getCenterX(),
+                        DURATION_GEOMETRY_ADJUST, false);
+            }
+            enterFullScreen();
+        }
+
+        private void enterFullScreen() {
+            if (mListener != null) {
+                // TODO: After full size images snapping to fill the screen at
+                // the end of a scroll/fling is implemented, we should only make
+                // this call when the view on the center of the screen is
+                // camera preview
+                mListener.onSwitchMode(true);
+            }
+            if (inFullScreen()) {
+                return;
+            }
+            scaleTo(1f, DURATION_GEOMETRY_ADJUST);
+        }
+
+        @Override
+        public void gotoCameraFullScreen() {
+            if (mDataAdapter.getImageData(0).getType()
+                    != ImageData.TYPE_CAMERA_PREVIEW) {
+                return;
+            }
+            gotoFullScreen();
+            scrollTo(
+                    estimateMinX(mViewInfo[mCurrentInfo].getID(),
+                            mViewInfo[mCurrentInfo].getLeftPosition(),
+                            getWidth()),
+                    DURATION_GEOMETRY_ADJUST, false);
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            mHasNewScale = true;
+            mNewScale = (Float) animation.getAnimatedValue();
+            layoutChildren();
+        }
+
+        @Override
+        public void onAnimationStart(Animator anim) {
+        }
+
+        @Override
+        public void onAnimationEnd(Animator anim) {
+            ViewInfo info = mViewInfo[mCurrentInfo];
+            if (info != null && mCenterX == info.getCenterX()) {
+                if (inFullScreen()) {
+                    lockAtCurrentView();
+                } else if (inFilmStrip()) {
+                    unlockPosition();
+                }
+            }
+        }
+
+        @Override
+        public void onAnimationCancel(Animator anim) {
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator anim) {
+        }
+    }
+
+    private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener {
+        // Indicating the current trend of scaling is up (>1) or down (<1).
+        private float mScaleTrend;
+
+        @Override
+        public boolean onSingleTapUp(float x, float y) {
+            if (inFilmStrip()) {
+                for (int i = 0; i < BUFFER_SIZE; i++) {
+                    if (mViewInfo[i] == null) {
+                        continue;
+                    }
+
+                    if (mViewInfo[i].areaContains(x, y)) {
+                        mController.scrollTo(mViewInfo[i].getCenterX(),
+                                DURATION_GEOMETRY_ADJUST, false);
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onDoubleTap(float x, float y) {
+            if (inFilmStrip()) {
+                ViewInfo centerInfo = mViewInfo[mCurrentInfo];
+                if (centerInfo != null && centerInfo.areaContains(x, y)) {
+                    mController.gotoFullScreen();
+                    return true;
+                }
+            } else if (inFullScreen()) {
+                mController.gotoFilmStrip();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onDown(float x, float y) {
+            if (mController.isScrolling()) {
+                mController.stopScrolling();
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onUp(float x, float y) {
+            float halfH = getHeight() / 2;
+            for (int i = 0; i < BUFFER_SIZE; i++) {
+                if (mViewInfo[i] == null) {
+                    continue;
+                }
+                float transY = mViewInfo[i].getTranslationY(mScale);
+                if (transY == 0) {
+                    continue;
+                }
+                int id = mViewInfo[i].getID();
+
+                if (mDataAdapter.getImageData(id)
+                        .isUIActionSupported(ImageData.ACTION_DEMOTE)
+                        && transY > halfH) {
+                    demoteData(i, id);
+                } else if (mDataAdapter.getImageData(id)
+                        .isUIActionSupported(ImageData.ACTION_PROMOTE)
+                        && transY < -halfH) {
+                    promoteData(i, id);
+                } else {
+                    // put the view back.
+                    mViewInfo[i].getView().animate()
+                            .translationY(0f)
+                            .alpha(1f)
+                            .setDuration(DURATION_GEOMETRY_ADJUST)
+                            .start();
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onScroll(float x, float y, float dx, float dy) {
+            int deltaX = (int) (dx / mScale);
+            if (inFilmStrip()) {
+                if (Math.abs(dx) > Math.abs(dy)) {
+                    if (deltaX > 0 && inCameraFullscreen()) {
+                        mController.gotoFilmStrip();
+                    }
+                    mController.scroll(deltaX);
+                } else {
+                    // Vertical part. Promote or demote.
+                    // int scaledDeltaY = (int) (dy * mScale);
+                    int hit = 0;
+                    Rect hitRect = new Rect();
+                    for (; hit < BUFFER_SIZE; hit++) {
+                        if (mViewInfo[hit] == null) {
+                            continue;
+                        }
+                        mViewInfo[hit].getView().getHitRect(hitRect);
+                        if (hitRect.contains((int) x, (int) y)) {
+                            break;
+                        }
+                    }
+                    if (hit == BUFFER_SIZE) {
+                        return false;
+                    }
+
+                    ImageData data = mDataAdapter.getImageData(mViewInfo[hit].getID());
+                    float transY = mViewInfo[hit].getTranslationY(mScale) - dy / mScale;
+                    if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) && transY > 0f) {
+                        transY = 0f;
+                    }
+                    if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) && transY < 0f) {
+                        transY = 0f;
+                    }
+                    mViewInfo[hit].setTranslationY(transY, mScale);
+                }
+            } else if (inFullScreen()) {
+                if (deltaX > 0 && inCameraFullscreen()) {
+                    mController.gotoFilmStrip();
+                }
+                mController.scroll(deltaX);
+            }
+            layoutChildren();
+
+            return true;
+        }
+
+        @Override
+        public boolean onFling(float velocityX, float velocityY) {
+            if (Math.abs(velocityX) > Math.abs(velocityY)) {
+                mController.fling(velocityX);
+            } else {
+                // ignore vertical fling.
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onScaleBegin(float focusX, float focusY) {
+            if (inCameraFullscreen()) {
+                return false;
+            }
+            mScaleTrend = 1f;
+            return true;
+        }
+
+        @Override
+        public boolean onScale(float focusX, float focusY, float scale) {
+            if (inCameraFullscreen()) {
+                return false;
+            }
+
+            mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f;
+            mScale *= scale;
+            if (mScale <= FILM_STRIP_SCALE) {
+                mScale = FILM_STRIP_SCALE;
+            }
+            if (mScale >= FULLSCREEN_SCALE) {
+                mScale = FULLSCREEN_SCALE;
+            }
+            layoutChildren();
+            return true;
+        }
+
+        @Override
+        public void onScaleEnd() {
+            if (mScaleTrend >= 1f) {
+                mController.gotoFullScreen();
+            } else {
+                mController.gotoFilmStrip();
+            }
+            mScaleTrend = 1f;
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/FocusIndicator.java b/src/com/android/camera/ui/FocusIndicator.java
new file mode 100644
index 0000000..e060570
--- /dev/null
+++ b/src/com/android/camera/ui/FocusIndicator.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+public interface FocusIndicator {
+    public void showStart();
+    public void showSuccess(boolean timeout);
+    public void showFail(boolean timeout);
+    public void clear();
+}
diff --git a/src/com/android/camera/ui/InLineSettingCheckBox.java b/src/com/android/camera/ui/InLineSettingCheckBox.java
new file mode 100644
index 0000000..c1aa5a9
--- /dev/null
+++ b/src/com/android/camera/ui/InLineSettingCheckBox.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/* A check box setting control which turns on/off the setting. */
+public class InLineSettingCheckBox extends InLineSettingItem {
+    private CheckBox mCheckBox;
+
+    OnCheckedChangeListener mCheckedChangeListener = new OnCheckedChangeListener() {
+        @Override
+        public void onCheckedChanged(CompoundButton buttonView, boolean desiredState) {
+            changeIndex(desiredState ? 1 : 0);
+        }
+    };
+
+    public InLineSettingCheckBox(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mCheckBox = (CheckBox) findViewById(R.id.setting_check_box);
+        mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener);
+    }
+
+    @Override
+    public void initialize(ListPreference preference) {
+        super.initialize(preference);
+        // Add content descriptions for the increment and decrement buttons.
+        mCheckBox.setContentDescription(getContext().getResources().getString(
+                R.string.accessibility_check_box, mPreference.getTitle()));
+    }
+
+    @Override
+    protected void updateView() {
+        mCheckBox.setOnCheckedChangeListener(null);
+        if (mOverrideValue == null) {
+            mCheckBox.setChecked(mIndex == 1);
+        } else {
+            int index = mPreference.findIndexOfValue(mOverrideValue);
+            mCheckBox.setChecked(index == 1);
+        }
+        mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener);
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        event.getText().add(mPreference.getTitle());
+        return true;
+    }
+
+    @Override
+    public void setEnabled(boolean enable) {
+        if (mTitle != null) mTitle.setEnabled(enable);
+        if (mCheckBox != null) mCheckBox.setEnabled(enable);
+    }
+}
diff --git a/src/com/android/camera/ui/InLineSettingItem.java b/src/com/android/camera/ui/InLineSettingItem.java
new file mode 100644
index 0000000..839a77f
--- /dev/null
+++ b/src/com/android/camera/ui/InLineSettingItem.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/**
+ * A one-line camera setting could be one of three types: knob, switch or restore
+ * preference button. The setting includes a title for showing the preference
+ * title which is initialized in the SimpleAdapter. A knob also includes
+ * (ex: Picture size), a previous button, the current value (ex: 5MP),
+ * and a next button. A switch, i.e. the preference RecordLocationPreference,
+ * has only two values on and off which will be controlled in a switch button.
+ * Other setting popup window includes several InLineSettingItem items with
+ * different types if possible.
+ */
+public abstract class InLineSettingItem extends LinearLayout {
+    private Listener mListener;
+    protected ListPreference mPreference;
+    protected int mIndex;
+    // Scene mode can override the original preference value.
+    protected String mOverrideValue;
+    protected TextView mTitle;
+
+    static public interface Listener {
+        public void onSettingChanged(ListPreference pref);
+    }
+
+    public InLineSettingItem(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    protected void setTitle(ListPreference preference) {
+        mTitle = ((TextView) findViewById(R.id.title));
+        mTitle.setText(preference.getTitle());
+    }
+
+    public void initialize(ListPreference preference) {
+        setTitle(preference);
+        if (preference == null) return;
+        mPreference = preference;
+        reloadPreference();
+    }
+
+    protected abstract void updateView();
+
+    protected boolean changeIndex(int index) {
+        if (index >= mPreference.getEntryValues().length || index < 0) return false;
+        mIndex = index;
+        mPreference.setValueIndex(mIndex);
+        if (mListener != null) {
+            mListener.onSettingChanged(mPreference);
+        }
+        updateView();
+        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        return true;
+    }
+
+    // The value of the preference may have changed. Update the UI.
+    public void reloadPreference() {
+        mIndex = mPreference.findIndexOfValue(mPreference.getValue());
+        updateView();
+    }
+
+    public void setSettingChangedListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void overrideSettings(String value) {
+        mOverrideValue = value;
+        updateView();
+    }
+}
diff --git a/src/com/android/camera/ui/InLineSettingMenu.java b/src/com/android/camera/ui/InLineSettingMenu.java
new file mode 100644
index 0000000..8e45c3e
--- /dev/null
+++ b/src/com/android/camera/ui/InLineSettingMenu.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.TextView;
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/* Setting menu item that will bring up a menu when you click on it. */
+public class InLineSettingMenu extends InLineSettingItem {
+    private static final String TAG = "InLineSettingMenu";
+    // The view that shows the current selected setting. Ex: 5MP
+    private TextView mEntry;
+
+    public InLineSettingMenu(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mEntry = (TextView) findViewById(R.id.current_setting);
+    }
+
+    @Override
+    public void initialize(ListPreference preference) {
+        super.initialize(preference);
+        //TODO: add contentDescription
+    }
+
+    @Override
+    protected void updateView() {
+        if (mOverrideValue == null) {
+            mEntry.setText(mPreference.getEntry());
+        } else {
+            int index = mPreference.findIndexOfValue(mOverrideValue);
+            if (index != -1) {
+                mEntry.setText(mPreference.getEntries()[index]);
+            } else {
+                // Avoid the crash if camera driver has bugs.
+                Log.e(TAG, "Fail to find override value=" + mOverrideValue);
+                mPreference.print();
+            }
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        event.getText().add(mPreference.getTitle() + mPreference.getEntry());
+        return true;
+    }
+
+    @Override
+    public void setEnabled(boolean enable) {
+        super.setEnabled(enable);
+        if (mTitle != null) mTitle.setEnabled(enable);
+        if (mEntry != null) mEntry.setEnabled(enable);
+    }
+}
diff --git a/src/com/android/camera/ui/LayoutChangeHelper.java b/src/com/android/camera/ui/LayoutChangeHelper.java
new file mode 100644
index 0000000..ef4eb6a
--- /dev/null
+++ b/src/com/android/camera/ui/LayoutChangeHelper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.view.View;
+
+public class LayoutChangeHelper implements LayoutChangeNotifier {
+    private LayoutChangeNotifier.Listener mListener;
+    private boolean mFirstTimeLayout;
+    private View mView;
+
+    public LayoutChangeHelper(View v) {
+        mView = v;
+        mFirstTimeLayout = true;
+    }
+
+    @Override
+    public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener) {
+        mListener = listener;
+    }
+
+    public void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (mListener == null) return;
+        if (mFirstTimeLayout || changed) {
+            mFirstTimeLayout = false;
+            mListener.onLayoutChange(mView, l, t, r, b);
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/LayoutChangeNotifier.java b/src/com/android/camera/ui/LayoutChangeNotifier.java
new file mode 100644
index 0000000..6261d34
--- /dev/null
+++ b/src/com/android/camera/ui/LayoutChangeNotifier.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.view.View;
+
+public interface LayoutChangeNotifier {
+    public interface Listener {
+        // Invoked only when the layout has changed or it is the first layout.
+        public void onLayoutChange(View v, int l, int t, int r, int b);
+    }
+
+    public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener);
+}
diff --git a/src/com/android/camera/ui/LayoutNotifyView.java b/src/com/android/camera/ui/LayoutNotifyView.java
new file mode 100644
index 0000000..6e118fc
--- /dev/null
+++ b/src/com/android/camera/ui/LayoutNotifyView.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+/*
+ * Customized view to support onLayoutChange() at or before API 10.
+ */
+public class LayoutNotifyView extends View implements LayoutChangeNotifier {
+    private LayoutChangeHelper mLayoutChangeHelper = new LayoutChangeHelper(this);
+
+    public LayoutNotifyView(Context context) {
+        super(context);
+    }
+
+    public LayoutNotifyView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void setOnLayoutChangeListener(
+            LayoutChangeNotifier.Listener listener) {
+        mLayoutChangeHelper.setOnLayoutChangeListener(listener);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mLayoutChangeHelper.onLayout(changed, l, t, r, b);
+    }
+}
diff --git a/src/com/android/camera/ui/ListPrefSettingPopup.java b/src/com/android/camera/ui/ListPrefSettingPopup.java
new file mode 100644
index 0000000..cfef73f
--- /dev/null
+++ b/src/com/android/camera/ui/ListPrefSettingPopup.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.SimpleAdapter;
+
+import com.android.camera.IconListPreference;
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+// A popup window that shows one camera setting. The title is the name of the
+// setting (ex: white-balance). The entries are the supported values (ex:
+// daylight, incandescent, etc). If initialized with an IconListPreference,
+// the entries will contain both text and icons. Otherwise, entries will be
+// shown in text.
+public class ListPrefSettingPopup extends AbstractSettingPopup implements
+        AdapterView.OnItemClickListener {
+    private static final String TAG = "ListPrefSettingPopup";
+    private ListPreference mPreference;
+    private Listener mListener;
+
+    static public interface Listener {
+        public void onListPrefChanged(ListPreference pref);
+    }
+
+    public ListPrefSettingPopup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    private class ListPrefSettingAdapter extends SimpleAdapter {
+        ListPrefSettingAdapter(Context context, List<? extends Map<String, ?>> data,
+                int resource, String[] from, int[] to) {
+            super(context, data, resource, from, to);
+        }
+
+        @Override
+        public void setViewImage(ImageView v, String value) {
+            if ("".equals(value)) {
+                // Some settings have no icons. Ex: exposure compensation.
+                v.setVisibility(View.GONE);
+            } else {
+                super.setViewImage(v, value);
+            }
+        }
+    }
+
+    public void initialize(ListPreference preference) {
+        mPreference = preference;
+        Context context = getContext();
+        CharSequence[] entries = mPreference.getEntries();
+        int[] iconIds = null;
+        if (preference instanceof IconListPreference) {
+            iconIds = ((IconListPreference) mPreference).getImageIds();
+            if (iconIds == null) {
+                iconIds = ((IconListPreference) mPreference).getLargeIconIds();
+            }
+        }
+        // Set title.
+        mTitle.setText(mPreference.getTitle());
+
+        // Prepare the ListView.
+        ArrayList<HashMap<String, Object>> listItem =
+                new ArrayList<HashMap<String, Object>>();
+        for(int i = 0; i < entries.length; ++i) {
+            HashMap<String, Object> map = new HashMap<String, Object>();
+            map.put("text", entries[i].toString());
+            if (iconIds != null) map.put("image", iconIds[i]);
+            listItem.add(map);
+        }
+        SimpleAdapter listItemAdapter = new ListPrefSettingAdapter(context, listItem,
+                R.layout.setting_item,
+                new String[] {"text", "image"},
+                new int[] {R.id.text, R.id.image});
+        ((ListView) mSettingList).setAdapter(listItemAdapter);
+        ((ListView) mSettingList).setOnItemClickListener(this);
+        reloadPreference();
+    }
+
+    // The value of the preference may have changed. Update the UI.
+    @Override
+    public void reloadPreference() {
+        int index = mPreference.findIndexOfValue(mPreference.getValue());
+        if (index != -1) {
+            ((ListView) mSettingList).setItemChecked(index, true);
+        } else {
+            Log.e(TAG, "Invalid preference value.");
+            mPreference.print();
+        }
+    }
+
+    public void setSettingChangedListener(Listener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view,
+            int index, long id) {
+        mPreference.setValueIndex(index);
+        if (mListener != null) mListener.onListPrefChanged(mPreference);
+    }
+}
diff --git a/src/com/android/camera/ui/MoreSettingPopup.java b/src/com/android/camera/ui/MoreSettingPopup.java
new file mode 100644
index 0000000..5900058
--- /dev/null
+++ b/src/com/android/camera/ui/MoreSettingPopup.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.camera.ListPreference;
+import com.android.camera.PreferenceGroup;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+/* A popup window that contains several camera settings. */
+public class MoreSettingPopup extends AbstractSettingPopup
+        implements InLineSettingItem.Listener,
+        AdapterView.OnItemClickListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MoreSettingPopup";
+
+    private Listener mListener;
+    private ArrayList<ListPreference> mListItem = new ArrayList<ListPreference>();
+
+    // Keep track of which setting items are disabled
+    // e.g. White balance will be disabled when scene mode is set to non-auto
+    private boolean[] mEnabled;
+
+    static public interface Listener {
+        public void onSettingChanged(ListPreference pref);
+        public void onPreferenceClicked(ListPreference pref);
+    }
+
+    private class MoreSettingAdapter extends ArrayAdapter<ListPreference> {
+        LayoutInflater mInflater;
+        String mOnString;
+        String mOffString;
+        MoreSettingAdapter() {
+            super(MoreSettingPopup.this.getContext(), 0, mListItem);
+            Context context = getContext();
+            mInflater = LayoutInflater.from(context);
+            mOnString = context.getString(R.string.setting_on);
+            mOffString = context.getString(R.string.setting_off);
+        }
+
+        private int getSettingLayoutId(ListPreference pref) {
+
+            if (isOnOffPreference(pref)) {
+                return R.layout.in_line_setting_check_box;
+            }
+            return R.layout.in_line_setting_menu;
+        }
+
+        private boolean isOnOffPreference(ListPreference pref) {
+            CharSequence[] entries = pref.getEntries();
+            if (entries.length != 2) return false;
+            String str1 = entries[0].toString();
+            String str2 = entries[1].toString();
+            return ((str1.equals(mOnString) && str2.equals(mOffString)) ||
+                    (str1.equals(mOffString) && str2.equals(mOnString)));
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView != null) return convertView;
+
+            ListPreference pref = mListItem.get(position);
+
+            int viewLayoutId = getSettingLayoutId(pref);
+            InLineSettingItem view = (InLineSettingItem)
+                    mInflater.inflate(viewLayoutId, parent, false);
+
+            view.initialize(pref); // no init for restore one
+            view.setSettingChangedListener(MoreSettingPopup.this);
+            if (position >= 0 && position < mEnabled.length) {
+                view.setEnabled(mEnabled[position]);
+            } else {
+                Log.w(TAG, "Invalid input: enabled list length, " + mEnabled.length
+                        + " position " + position);
+            }
+            return view;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            if (position >= 0 && position < mEnabled.length) {
+                return mEnabled[position];
+            }
+            return true;
+        }
+    }
+
+    public void setSettingChangedListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public MoreSettingPopup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void initialize(PreferenceGroup group, String[] keys) {
+        // Prepare the setting items.
+        for (int i = 0; i < keys.length; ++i) {
+            ListPreference pref = group.findPreference(keys[i]);
+            if (pref != null) mListItem.add(pref);
+        }
+
+        ArrayAdapter<ListPreference> mListItemAdapter = new MoreSettingAdapter();
+        ((ListView) mSettingList).setAdapter(mListItemAdapter);
+        ((ListView) mSettingList).setOnItemClickListener(this);
+        ((ListView) mSettingList).setSelector(android.R.color.transparent);
+        // Initialize mEnabled
+        mEnabled = new boolean[mListItem.size()];
+        for (int i = 0; i < mEnabled.length; i++) {
+            mEnabled[i] = true;
+        }
+    }
+
+    // When preferences are disabled, we will display them grayed out. Users
+    // will not be able to change the disabled preferences, but they can still see
+    // the current value of the preferences
+    public void setPreferenceEnabled(String key, boolean enable) {
+        int count = mEnabled == null ? 0 : mEnabled.length;
+        for (int j = 0; j < count; j++) {
+            ListPreference pref = mListItem.get(j);
+            if (pref != null && key.equals(pref.getKey())) {
+                mEnabled[j] = enable;
+                break;
+            }
+        }
+    }
+
+    public void onSettingChanged(ListPreference pref) {
+        if (mListener != null) {
+            mListener.onSettingChanged(pref);
+        }
+    }
+
+    // Scene mode can override other camera settings (ex: flash mode).
+    public void overrideSettings(final String ... keyvalues) {
+        int count = mEnabled == null ? 0 : mEnabled.length;
+        for (int i = 0; i < keyvalues.length; i += 2) {
+            String key = keyvalues[i];
+            String value = keyvalues[i + 1];
+            for (int j = 0; j < count; j++) {
+                ListPreference pref = mListItem.get(j);
+                if (pref != null && key.equals(pref.getKey())) {
+                    // Change preference
+                    if (value != null) pref.setValue(value);
+                    // If the preference is overridden, disable the preference
+                    boolean enable = value == null;
+                    mEnabled[j] = enable;
+                    if (mSettingList.getChildCount() > j) {
+                        mSettingList.getChildAt(j).setEnabled(enable);
+                    }
+                }
+            }
+        }
+        reloadPreference();
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position,
+            long id) {
+        if (mListener != null) {
+            ListPreference pref = mListItem.get(position);
+            mListener.onPreferenceClicked(pref);
+        }
+    }
+
+    @Override
+    public void reloadPreference() {
+        int count = mSettingList.getChildCount();
+        for (int i = 0; i < count; i++) {
+            ListPreference pref = mListItem.get(i);
+            if (pref != null) {
+                InLineSettingItem settingItem =
+                        (InLineSettingItem) mSettingList.getChildAt(i);
+                settingItem.reloadPreference();
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/OnIndicatorEventListener.java b/src/com/android/camera/ui/OnIndicatorEventListener.java
new file mode 100644
index 0000000..566f5c7
--- /dev/null
+++ b/src/com/android/camera/ui/OnIndicatorEventListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+public interface OnIndicatorEventListener {
+    public static int EVENT_ENTER_SECOND_LEVEL_INDICATOR_BAR = 0;
+    public static int EVENT_LEAVE_SECOND_LEVEL_INDICATOR_BAR = 1;
+    public static int EVENT_ENTER_ZOOM_CONTROL = 2;
+    public static int EVENT_LEAVE_ZOOM_CONTROL = 3;
+    void onIndicatorEvent(int event);
+}
diff --git a/src/com/android/camera/ui/OverlayRenderer.java b/src/com/android/camera/ui/OverlayRenderer.java
new file mode 100644
index 0000000..417e219
--- /dev/null
+++ b/src/com/android/camera/ui/OverlayRenderer.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+public abstract class OverlayRenderer implements RenderOverlay.Renderer {
+
+    private static final String TAG = "CAM OverlayRenderer";
+    protected RenderOverlay mOverlay;
+
+    protected int mLeft, mTop, mRight, mBottom;
+
+    protected boolean mVisible;
+
+    public void setVisible(boolean vis) {
+        mVisible = vis;
+        update();
+    }
+
+    public boolean isVisible() {
+        return mVisible;
+    }
+
+    // default does not handle touch
+    @Override
+    public boolean handlesTouch() {
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent evt) {
+        return false;
+    }
+
+    public abstract void onDraw(Canvas canvas);
+
+    public void draw(Canvas canvas) {
+        if (mVisible) {
+            onDraw(canvas);
+        }
+    }
+
+    @Override
+    public void setOverlay(RenderOverlay overlay) {
+        mOverlay = overlay;
+    }
+
+    @Override
+    public void layout(int left, int top, int right, int bottom) {
+        mLeft = left;
+        mRight = right;
+        mTop = top;
+        mBottom = bottom;
+    }
+
+    protected Context getContext() {
+        if (mOverlay != null) {
+            return mOverlay.getContext();
+        } else {
+            return null;
+        }
+    }
+
+    public int getWidth() {
+        return mRight - mLeft;
+    }
+
+    public int getHeight() {
+        return mBottom - mTop;
+    }
+
+    protected void update() {
+        if (mOverlay != null) {
+            mOverlay.update();
+        }
+    }
+
+}
diff --git a/src/com/android/camera/ui/PieItem.java b/src/com/android/camera/ui/PieItem.java
new file mode 100644
index 0000000..47fe067
--- /dev/null
+++ b/src/com/android/camera/ui/PieItem.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Pie menu item
+ */
+public class PieItem {
+
+    public static interface OnClickListener {
+        void onClick(PieItem item);
+    }
+
+    private Drawable mDrawable;
+    private int level;
+
+    private boolean mSelected;
+    private boolean mEnabled;
+    private List<PieItem> mItems;
+    private Path mPath;
+    private OnClickListener mOnClickListener;
+    private float mAlpha;
+    private CharSequence mLabel;
+
+    // Gray out the view when disabled
+    private static final float ENABLED_ALPHA = 1;
+    private static final float DISABLED_ALPHA = (float) 0.3;
+    private boolean mChangeAlphaWhenDisabled = true;
+
+    public PieItem(Drawable drawable, int level) {
+        mDrawable = drawable;
+        this.level = level;
+        if (drawable != null) {
+            setAlpha(1f);
+        }
+        mEnabled = true;
+    }
+
+    public void setLabel(CharSequence txt) {
+        mLabel = txt;
+    }
+
+    public CharSequence getLabel() {
+        return mLabel;
+    }
+
+    public boolean hasItems() {
+        return mItems != null;
+    }
+
+    public List<PieItem> getItems() {
+        return mItems;
+    }
+
+    public void addItem(PieItem item) {
+        if (mItems == null) {
+            mItems = new ArrayList<PieItem>();
+        }
+        mItems.add(item);
+    }
+
+    public void clearItems() {
+        mItems = null;
+    }
+
+    public void setLevel(int level) {
+        this.level = level;
+    }
+
+    public void setPath(Path p) {
+        mPath = p;
+    }
+
+    public Path getPath() {
+        return mPath;
+    }
+
+    public void setChangeAlphaWhenDisabled (boolean enable) {
+        mChangeAlphaWhenDisabled = enable;
+    }
+
+    public void setAlpha(float alpha) {
+        mAlpha = alpha;
+        mDrawable.setAlpha((int) (255 * alpha));
+    }
+
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+        if (mChangeAlphaWhenDisabled) {
+            if (mEnabled) {
+                setAlpha(ENABLED_ALPHA);
+            } else {
+                setAlpha(DISABLED_ALPHA);
+            }
+        }
+    }
+
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    public void setSelected(boolean s) {
+        mSelected = s;
+    }
+
+    public boolean isSelected() {
+        return mSelected;
+    }
+
+    public int getLevel() {
+        return level;
+    }
+
+
+    public void setOnClickListener(OnClickListener listener) {
+        mOnClickListener = listener;
+    }
+
+    public void performClick() {
+        if (mOnClickListener != null) {
+            mOnClickListener.onClick(this);
+        }
+    }
+
+    public int getIntrinsicWidth() {
+        return mDrawable.getIntrinsicWidth();
+    }
+
+    public int getIntrinsicHeight() {
+        return mDrawable.getIntrinsicHeight();
+    }
+
+    public void setBounds(int left, int top, int right, int bottom) {
+        mDrawable.setBounds(left, top, right, bottom);
+    }
+
+    public void draw(Canvas canvas) {
+        mDrawable.draw(canvas);
+    }
+
+    public void setImageResource(Context context, int resId) {
+        Drawable d = context.getResources().getDrawable(resId).mutate();
+        d.setBounds(mDrawable.getBounds());
+        mDrawable = d;
+        setAlpha(mAlpha);
+    }
+
+}
diff --git a/src/com/android/camera/ui/PieMenuButton.java b/src/com/android/camera/ui/PieMenuButton.java
new file mode 100644
index 0000000..0e23226
--- /dev/null
+++ b/src/com/android/camera/ui/PieMenuButton.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class PieMenuButton extends View {
+    private boolean mPressed;
+    private boolean mReadyToClick = false;
+    public PieMenuButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        mPressed = isPressed();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean handled = super.onTouchEvent(event);
+        if (MotionEvent.ACTION_UP == event.getAction() && mPressed) {
+            // Perform a customized click as soon as the ACTION_UP event
+            // is received. The reason for doing this is that Framework
+            // delays the performClick() call after ACTION_UP. But we do not
+            // want the delay because it affects an important state change
+            // for PieRenderer.
+            mReadyToClick = true;
+            performClick();
+        }
+        return handled;
+    }
+
+    @Override
+    public boolean performClick() {
+        if (mReadyToClick) {
+            // We only respond to our customized click which happens right
+            // after ACTION_UP event is received, with no delay.
+            mReadyToClick = false;
+            return super.performClick();
+        }
+        return false;
+    }
+};
\ No newline at end of file
diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java
new file mode 100644
index 0000000..c78107c
--- /dev/null
+++ b/src/com/android/camera/ui/PieRenderer.java
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Message;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import com.android.camera.drawable.TextDrawable;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PieRenderer extends OverlayRenderer
+        implements FocusIndicator {
+
+    private static final String TAG = "CAM Pie";
+
+    // Sometimes continuous autofocus starts and stops several times quickly.
+    // These states are used to make sure the animation is run for at least some
+    // time.
+    private volatile int mState;
+    private ScaleAnimation mAnimation = new ScaleAnimation();
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_FOCUSING = 1;
+    private static final int STATE_FINISHING = 2;
+    private static final int STATE_PIE = 8;
+
+    private static final float MATH_PI_2 = (float)(Math.PI / 2);
+
+    private Runnable mDisappear = new Disappear();
+    private Animation.AnimationListener mEndAction = new EndAction();
+    private static final int SCALING_UP_TIME = 600;
+    private static final int SCALING_DOWN_TIME = 100;
+    private static final int DISAPPEAR_TIMEOUT = 200;
+    private static final int DIAL_HORIZONTAL = 157;
+    // fade out timings
+    private static final int PIE_FADE_OUT_DURATION = 600;
+
+    private static final long PIE_FADE_IN_DURATION = 200;
+    private static final long PIE_XFADE_DURATION = 200;
+    private static final long PIE_SELECT_FADE_DURATION = 300;
+    private static final long PIE_OPEN_SUB_DELAY = 400;
+    private static final long PIE_SLICE_DURATION = 80;
+
+    private static final int MSG_OPEN = 0;
+    private static final int MSG_CLOSE = 1;
+    private static final int MSG_OPENSUBMENU = 2;
+
+    protected static float CENTER = (float) Math.PI / 2;
+    protected static float RAD24 = (float)(24 * Math.PI / 180);
+    protected static final float SWEEP_SLICE = 0.14f;
+    protected static final float SWEEP_ARC = 0.23f;
+
+    // geometry
+    private int mRadius;
+    private int mRadiusInc;
+
+    // the detection if touch is inside a slice is offset
+    // inbounds by this amount to allow the selection to show before the
+    // finger covers it
+    private int mTouchOffset;
+
+    private List<PieItem> mOpen;
+
+    private Paint mSelectedPaint;
+    private Paint mSubPaint;
+    private Paint mMenuArcPaint;
+
+    // touch handling
+    private PieItem mCurrentItem;
+
+    private Paint mFocusPaint;
+    private int mSuccessColor;
+    private int mFailColor;
+    private int mCircleSize;
+    private int mFocusX;
+    private int mFocusY;
+    private int mCenterX;
+    private int mCenterY;
+    private int mArcCenterY;
+    private int mSliceCenterY;
+    private int mPieCenterX;
+    private int mPieCenterY;
+    private int mSliceRadius;
+    private int mArcRadius;
+    private int mArcOffset;
+
+    private int mDialAngle;
+    private RectF mCircle;
+    private RectF mDial;
+    private Point mPoint1;
+    private Point mPoint2;
+    private int mStartAnimationAngle;
+    private boolean mFocused;
+    private int mInnerOffset;
+    private int mOuterStroke;
+    private int mInnerStroke;
+    private boolean mTapMode;
+    private boolean mBlockFocus;
+    private int mTouchSlopSquared;
+    private Point mDown;
+    private boolean mOpening;
+    private ValueAnimator mXFade;
+    private ValueAnimator mFadeIn;
+    private ValueAnimator mFadeOut;
+    private ValueAnimator mSlice;
+    private volatile boolean mFocusCancelled;
+    private PointF mPolar = new PointF();
+    private TextDrawable mLabel;
+    private int mDeadZone;
+    private int mAngleZone;
+    private float mCenterAngle;
+
+
+
+    private Handler mHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            switch(msg.what) {
+            case MSG_OPEN:
+                if (mListener != null) {
+                    mListener.onPieOpened(mPieCenterX, mPieCenterY);
+                }
+                break;
+            case MSG_CLOSE:
+                if (mListener != null) {
+                    mListener.onPieClosed();
+                }
+                break;
+            case MSG_OPENSUBMENU:
+                onEnterOpen();
+                break;
+            }
+
+        }
+    };
+
+    private PieListener mListener;
+
+    static public interface PieListener {
+        public void onPieOpened(int centerX, int centerY);
+        public void onPieClosed();
+    }
+
+    public void setPieListener(PieListener pl) {
+        mListener = pl;
+    }
+
+    public PieRenderer(Context context) {
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        setVisible(false);
+        mOpen = new ArrayList<PieItem>();
+        mOpen.add(new PieItem(null, 0));
+        Resources res = ctx.getResources();
+        mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
+        mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
+        mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
+        mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
+        mSelectedPaint = new Paint();
+        mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
+        mSelectedPaint.setAntiAlias(true);
+        mSubPaint = new Paint();
+        mSubPaint.setAntiAlias(true);
+        mSubPaint.setColor(Color.argb(200, 250, 230, 128));
+        mFocusPaint = new Paint();
+        mFocusPaint.setAntiAlias(true);
+        mFocusPaint.setColor(Color.WHITE);
+        mFocusPaint.setStyle(Paint.Style.STROKE);
+        mSuccessColor = Color.GREEN;
+        mFailColor = Color.RED;
+        mCircle = new RectF();
+        mDial = new RectF();
+        mPoint1 = new Point();
+        mPoint2 = new Point();
+        mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
+        mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
+        mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
+        mState = STATE_IDLE;
+        mBlockFocus = false;
+        mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
+        mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
+        mDown = new Point();
+        mMenuArcPaint = new Paint();
+        mMenuArcPaint.setAntiAlias(true);
+        mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255));
+        mMenuArcPaint.setStrokeWidth(10);
+        mMenuArcPaint.setStyle(Paint.Style.STROKE);
+        mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius);
+        mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius);
+        mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset);
+        mLabel = new TextDrawable(res);
+        mLabel.setDropShadow(true);
+        mDeadZone = res.getDimensionPixelSize(R.dimen.pie_deadzone_width);
+        mAngleZone = res.getDimensionPixelSize(R.dimen.pie_anglezone_width);
+    }
+
+    private PieItem getRoot() {
+        return mOpen.get(0);
+    }
+
+    public boolean showsItems() {
+        return mTapMode;
+    }
+
+    public void addItem(PieItem item) {
+        // add the item to the pie itself
+        getRoot().addItem(item);
+    }
+
+    public void clearItems() {
+        getRoot().clearItems();
+    }
+
+    public void showInCenter() {
+        if ((mState == STATE_PIE) && isVisible()) {
+            mTapMode = false;
+            show(false);
+        } else {
+            if (mState != STATE_IDLE) {
+                cancelFocus();
+            }
+            mState = STATE_PIE;
+            resetPieCenter();
+            setCenter(mPieCenterX, mPieCenterY);
+            mTapMode = true;
+            show(true);
+        }
+    }
+
+    public void hide() {
+        show(false);
+    }
+
+    /**
+     * guaranteed has center set
+     * @param show
+     */
+    private void show(boolean show) {
+        if (show) {
+            if (mXFade != null) {
+                mXFade.cancel();
+            }
+            mState = STATE_PIE;
+            // ensure clean state
+            mCurrentItem = null;
+            PieItem root = getRoot();
+            for (PieItem openItem : mOpen) {
+                if (openItem.hasItems()) {
+                    for (PieItem item : openItem.getItems()) {
+                        item.setSelected(false);
+                    }
+                }
+            }
+            mLabel.setText("");
+            mOpen.clear();
+            mOpen.add(root);
+            layoutPie();
+            fadeIn();
+        } else {
+            mState = STATE_IDLE;
+            mTapMode = false;
+            if (mXFade != null) {
+                mXFade.cancel();
+            }
+            if (mLabel != null) {
+                mLabel.setText("");
+            }
+        }
+        setVisible(show);
+        mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
+    }
+
+    public boolean isOpen() {
+        return mState == STATE_PIE && isVisible();
+    }
+
+    private void fadeIn() {
+        mFadeIn = new ValueAnimator();
+        mFadeIn.setFloatValues(0f, 1f);
+        mFadeIn.setDuration(PIE_FADE_IN_DURATION);
+        // linear interpolation
+        mFadeIn.setInterpolator(null);
+        mFadeIn.addListener(new AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mFadeIn = null;
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationCancel(Animator arg0) {
+            }
+        });
+        mFadeIn.start();
+    }
+
+    public void setCenter(int x, int y) {
+        mPieCenterX = x;
+        mPieCenterY = y;
+        mSliceCenterY = y + mSliceRadius - mArcOffset;
+        mArcCenterY = y - mArcOffset + mArcRadius;
+    }
+
+    @Override
+    public void layout(int l, int t, int r, int b) {
+        super.layout(l, t, r, b);
+        mCenterX = (r - l) / 2;
+        mCenterY = (b - t) / 2;
+
+        mFocusX = mCenterX;
+        mFocusY = mCenterY;
+        resetPieCenter();
+        setCircle(mFocusX, mFocusY);
+        if (isVisible() && mState == STATE_PIE) {
+            setCenter(mPieCenterX, mPieCenterY);
+            layoutPie();
+        }
+    }
+
+    private void resetPieCenter() {
+        mPieCenterX = mCenterX;
+        mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone);
+    }
+
+    private void layoutPie() {
+        mCenterAngle = getCenterAngle();
+        layoutItems(0, getRoot().getItems());
+        layoutLabel(getLevel());
+    }
+
+    private void layoutLabel(int level) {
+        int x = mPieCenterX - (int) (FloatMath.sin(mCenterAngle - CENTER)
+                * (mArcRadius + (level + 2) * mRadiusInc));
+        int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc;
+        int w = mLabel.getIntrinsicWidth();
+        int h = mLabel.getIntrinsicHeight();
+        mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2);
+    }
+
+    private void layoutItems(int level, List<PieItem> items) {
+        int extend = 1;
+        Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend,
+                mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4,
+                mPieCenterX, mArcCenterY - level * mRadiusInc);
+        final int count = items.size();
+        int pos = 0;
+        for (PieItem item : items) {
+            // shared between items
+            item.setPath(path);
+            float angle = getArcCenter(item, pos, count);
+            int w = item.getIntrinsicWidth();
+            int h = item.getIntrinsicHeight();
+            // move views to outer border
+            int r = mArcRadius + mRadiusInc * 2 / 3;
+            int x = (int) (r * Math.cos(angle));
+            int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2;
+            x = mPieCenterX + x - w / 2;
+            item.setBounds(x, y, x + w, y + h);
+            item.setLevel(level);
+            if (item.hasItems()) {
+                layoutItems(level + 1, item.getItems());
+            }
+            pos++;
+        }
+    }
+
+    private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) {
+        RectF bb =
+                new RectF(cx - outer, cy - outer, cx + outer,
+                        cy + outer);
+        RectF bbi =
+                new RectF(cx - inner, cy - inner, cx + inner,
+                        cy + inner);
+        Path path = new Path();
+        path.arcTo(bb, start, end - start, true);
+        path.arcTo(bbi, end, start - end);
+        path.close();
+        return path;
+    }
+
+    private float getArcCenter(PieItem item, int pos, int count) {
+        return getCenter(pos, count, SWEEP_ARC);
+    }
+
+    private float getSliceCenter(PieItem item, int pos, int count) {
+        float center = (getCenterAngle() - CENTER) * 0.5f + CENTER;
+        return center + (count - 1) * SWEEP_SLICE / 2f
+                - pos * SWEEP_SLICE;
+    }
+
+    private float getCenter(int pos, int count, float sweep) {
+        return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep;
+    }
+
+    private float getCenterAngle() {
+        float center = CENTER;
+        if (mPieCenterX < mDeadZone + mAngleZone) {
+            center = CENTER - (mAngleZone - mPieCenterX + mDeadZone) * RAD24
+                    / (float) mAngleZone;
+        } else if (mPieCenterX > getWidth() - mDeadZone - mAngleZone) {
+            center = CENTER + (mPieCenterX - (getWidth() - mDeadZone - mAngleZone)) * RAD24
+                    / (float) mAngleZone;
+        }
+        return center;
+    }
+
+    /**
+     * converts a
+     * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
+     * @return skia angle
+     */
+    private float getDegrees(double angle) {
+        return (float) (360 - 180 * angle / Math.PI);
+    }
+
+    private void startFadeOut(final PieItem item) {
+        if (mFadeIn != null) {
+            mFadeIn.cancel();
+        }
+        if (mXFade != null) {
+            mXFade.cancel();
+        }
+        mFadeOut = new ValueAnimator();
+        mFadeOut.setFloatValues(1f, 0f);
+        mFadeOut.setDuration(PIE_FADE_OUT_DURATION);
+        mFadeOut.addListener(new AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                item.performClick();
+                mFadeOut = null;
+                deselect();
+                show(false);
+                mOverlay.setAlpha(1);
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animator) {
+            }
+
+        });
+        mFadeOut.start();
+    }
+
+    // root does not count
+    private boolean hasOpenItem() {
+        return mOpen.size() > 1;
+    }
+
+    // pop an item of the open item stack
+    private PieItem closeOpenItem() {
+        PieItem item = getOpenItem();
+        mOpen.remove(mOpen.size() -1);
+        return item;
+    }
+
+    private PieItem getOpenItem() {
+        return mOpen.get(mOpen.size() - 1);
+    }
+
+    // return the children either the root or parent of the current open item
+    private PieItem getParent() {
+        return mOpen.get(Math.max(0, mOpen.size() - 2));
+    }
+
+    private int getLevel() {
+        return mOpen.size() - 1;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        float alpha = 1;
+        if (mXFade != null) {
+            alpha = (Float) mXFade.getAnimatedValue();
+        } else if (mFadeIn != null) {
+            alpha = (Float) mFadeIn.getAnimatedValue();
+        } else if (mFadeOut != null) {
+            alpha = (Float) mFadeOut.getAnimatedValue();
+        }
+        int state = canvas.save();
+        if (mFadeIn != null) {
+            float sf = 0.9f + alpha * 0.1f;
+            canvas.scale(sf, sf, mPieCenterX, mPieCenterY);
+        }
+        if (mState != STATE_PIE) {
+            drawFocus(canvas);
+        }
+        if (mState == STATE_FINISHING) {
+            canvas.restoreToCount(state);
+            return;
+        }
+        if (mState != STATE_PIE) return;
+        if (!hasOpenItem() || (mXFade != null)) {
+            // draw base menu
+            drawArc(canvas, getLevel(), getParent());
+            List<PieItem> items = getParent().getItems();
+            final int count = items.size();
+            int pos = 0;
+            for (PieItem item : getParent().getItems()) {
+                drawItem(Math.max(0, mOpen.size() - 2), pos, count, canvas, item, alpha);
+                pos++;
+            }
+            mLabel.draw(canvas);
+        }
+        if (hasOpenItem()) {
+            int level = getLevel();
+            drawArc(canvas, level, getOpenItem());
+            List<PieItem> items = getOpenItem().getItems();
+            final int count = items.size();
+            int pos = 0;
+            for (PieItem inner : items) {
+                if (mFadeOut != null) {
+                    drawItem(level, pos, count, canvas, inner, alpha);
+                } else {
+                    drawItem(level, pos, count, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+                }
+                pos++;
+            }
+            mLabel.draw(canvas);
+        }
+        canvas.restoreToCount(state);
+    }
+
+    private void drawArc(Canvas canvas, int level, PieItem item) {
+        // arc
+        if (mState == STATE_PIE) {
+            final int count = item.getItems().size();
+            float start = mCenterAngle + (count * SWEEP_ARC / 2f);
+            float end =  mCenterAngle - (count * SWEEP_ARC / 2f);
+            int cy = mArcCenterY - level * mRadiusInc;
+            canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius,
+                    mPieCenterX + mArcRadius, cy + mArcRadius),
+                    getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint);
+        }
+    }
+
+    private void drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha) {
+        if (mState == STATE_PIE) {
+            if (item.getPath() != null) {
+                int y = mArcCenterY - level * mRadiusInc;
+                if (item.isSelected()) {
+                    Paint p = mSelectedPaint;
+                    int state = canvas.save();
+                    float angle = 0;
+                    if (mSlice != null) {
+                        angle = (Float) mSlice.getAnimatedValue();
+                    } else {
+                        angle = getArcCenter(item, pos, count) - SWEEP_ARC / 2f;
+                    }
+                    angle = getDegrees(angle);
+                    canvas.rotate(angle, mPieCenterX, y);
+                    if (mFadeOut != null) {
+                        p.setAlpha((int)(255 * alpha));
+                    }
+                    canvas.drawPath(item.getPath(), p);
+                    if (mFadeOut != null) {
+                        p.setAlpha(255);
+                    }
+                    canvas.restoreToCount(state);
+                }
+                if (mFadeOut == null) {
+                    alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
+                    // draw the item view
+                    item.setAlpha(alpha);
+                }
+                item.draw(canvas);
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent evt) {
+        float x = evt.getX();
+        float y = evt.getY();
+        int action = evt.getActionMasked();
+        getPolar(x, y, !mTapMode, mPolar);
+        if (MotionEvent.ACTION_DOWN == action) {
+            if ((x < mDeadZone) || (x > getWidth() - mDeadZone)) {
+                return false;
+            }
+            mDown.x = (int) evt.getX();
+            mDown.y = (int) evt.getY();
+            mOpening = false;
+            if (mTapMode) {
+                PieItem item = findItem(mPolar);
+                if ((item != null) && (mCurrentItem != item)) {
+                    mState = STATE_PIE;
+                    onEnter(item);
+                }
+            } else {
+                setCenter((int) x, (int) y);
+                show(true);
+            }
+            return true;
+        } else if (MotionEvent.ACTION_UP == action) {
+            if (isVisible()) {
+                PieItem item = mCurrentItem;
+                if (mTapMode) {
+                    item = findItem(mPolar);
+                    if (mOpening) {
+                        mOpening = false;
+                        return true;
+                    }
+                }
+                if (item == null) {
+                    mTapMode = false;
+                    show(false);
+                } else if (!mOpening && !item.hasItems()) {
+                        startFadeOut(item);
+                        mTapMode = false;
+                } else {
+                    mTapMode = true;
+                }
+                return true;
+            }
+        } else if (MotionEvent.ACTION_CANCEL == action) {
+            if (isVisible() || mTapMode) {
+                show(false);
+            }
+            deselect();
+            mHandler.removeMessages(MSG_OPENSUBMENU);
+            return false;
+        } else if (MotionEvent.ACTION_MOVE == action) {
+            if (pulledToCenter(mPolar)) {
+                mHandler.removeMessages(MSG_OPENSUBMENU);
+                if (hasOpenItem()) {
+                    if (mCurrentItem != null) {
+                        mCurrentItem.setSelected(false);
+                    }
+                    closeOpenItem();
+                    mCurrentItem = null;
+                } else {
+                    deselect();
+                }
+                mLabel.setText("");
+                return false;
+            }
+            PieItem item = findItem(mPolar);
+            boolean moved = hasMoved(evt);
+            if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
+                mHandler.removeMessages(MSG_OPENSUBMENU);
+                // only select if we didn't just open or have moved past slop
+                if (moved) {
+                    // switch back to swipe mode
+                    mTapMode = false;
+                }
+                onEnterSelect(item);
+                mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY);
+            }
+        }
+        return false;
+    }
+
+    private boolean pulledToCenter(PointF polarCoords) {
+        return polarCoords.y < mArcRadius - mRadiusInc;
+    }
+
+    private boolean inside(PointF polar, PieItem item, int pos, int count) {
+        float start = getSliceCenter(item, pos, count) - SWEEP_SLICE / 2f;
+        boolean res =  (mArcRadius < polar.y)
+                && (start < polar.x)
+                && (start + SWEEP_SLICE > polar.x)
+                && (!mTapMode || (mArcRadius + mRadiusInc > polar.y));
+        return res;
+    }
+
+    private void getPolar(float x, float y, boolean useOffset, PointF res) {
+        // get angle and radius from x/y
+        res.x = (float) Math.PI / 2;
+        x = x - mPieCenterX;
+        float y1 = mSliceCenterY - getLevel() * mRadiusInc - y;
+        float y2 = mArcCenterY - getLevel() * mRadiusInc - y;
+        res.y = (float) Math.sqrt(x * x + y2 * y2);
+        if (x != 0) {
+            res.x = (float) Math.atan2(y1,  x);
+            if (res.x < 0) {
+                res.x = (float) (2 * Math.PI + res.x);
+            }
+        }
+        res.y = res.y + (useOffset ? mTouchOffset : 0);
+    }
+
+    private boolean hasMoved(MotionEvent e) {
+        return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
+                + (e.getY() - mDown.y) * (e.getY() - mDown.y);
+    }
+
+    private void onEnterSelect(PieItem item) {
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        if (item != null && item.isEnabled()) {
+            moveSelection(mCurrentItem, item);
+            item.setSelected(true);
+            mCurrentItem = item;
+            mLabel.setText(mCurrentItem.getLabel());
+            layoutLabel(getLevel());
+        } else {
+            mCurrentItem = null;
+        }
+    }
+
+    private void onEnterOpen() {
+        if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
+            openCurrentItem();
+        }
+    }
+
+    /**
+     * enter a slice for a view
+     * updates model only
+     * @param item
+     */
+    private void onEnter(PieItem item) {
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        if (item != null && item.isEnabled()) {
+            item.setSelected(true);
+            mCurrentItem = item;
+            mLabel.setText(mCurrentItem.getLabel());
+            if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
+                openCurrentItem();
+                layoutLabel(getLevel());
+            }
+        } else {
+            mCurrentItem = null;
+        }
+    }
+
+    private void deselect() {
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        if (hasOpenItem()) {
+            PieItem item = closeOpenItem();
+            onEnter(item);
+        } else {
+            mCurrentItem = null;
+        }
+    }
+
+    private int getItemPos(PieItem target) {
+        List<PieItem> items = getOpenItem().getItems();
+        return items.indexOf(target);
+    }
+
+    private int getCurrentCount() {
+        return getOpenItem().getItems().size();
+    }
+
+    private void moveSelection(PieItem from, PieItem to) {
+        final int count = getCurrentCount();
+        final int fromPos = getItemPos(from);
+        final int toPos = getItemPos(to);
+        if (fromPos != -1 && toPos != -1) {
+            float startAngle = getArcCenter(from, getItemPos(from), count)
+                    - SWEEP_ARC / 2f;
+            float endAngle = getArcCenter(to, getItemPos(to), count)
+                    - SWEEP_ARC / 2f;
+            mSlice = new ValueAnimator();
+            mSlice.setFloatValues(startAngle, endAngle);
+            // linear interpolater
+            mSlice.setInterpolator(null);
+            mSlice.setDuration(PIE_SLICE_DURATION);
+            mSlice.addListener(new AnimatorListener() {
+                @Override
+                public void onAnimationEnd(Animator arg0) {
+                    mSlice = null;
+                }
+
+                @Override
+                public void onAnimationRepeat(Animator arg0) {
+                }
+
+                @Override
+                public void onAnimationStart(Animator arg0) {
+                }
+
+                @Override
+                public void onAnimationCancel(Animator arg0) {
+                }
+            });
+            mSlice.start();
+        }
+    }
+
+    private void openCurrentItem() {
+        if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
+            mOpen.add(mCurrentItem);
+            layoutLabel(getLevel());
+            mOpening = true;
+            if (mFadeIn != null) {
+                mFadeIn.cancel();
+            }
+            mXFade = new ValueAnimator();
+            mXFade.setFloatValues(1f, 0f);
+            mXFade.setDuration(PIE_XFADE_DURATION);
+            // Linear interpolation
+            mXFade.setInterpolator(null);
+            final PieItem ci = mCurrentItem;
+            mXFade.addListener(new AnimatorListener() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mXFade = null;
+                    ci.setSelected(false);
+                    mOpening = false;
+                }
+
+                @Override
+                public void onAnimationRepeat(Animator animation) {
+                }
+
+                @Override
+                public void onAnimationCancel(Animator arg0) {
+                }
+            });
+            mXFade.start();
+        }
+    }
+
+    /**
+     * @param polar x: angle, y: dist
+     * @return the item at angle/dist or null
+     */
+    private PieItem findItem(PointF polar) {
+        // find the matching item:
+        List<PieItem> items = getOpenItem().getItems();
+        final int count = items.size();
+        int pos = 0;
+        for (PieItem item : items) {
+            if (inside(polar, item, pos, count)) {
+                return item;
+            }
+            pos++;
+        }
+        return null;
+    }
+
+
+    @Override
+    public boolean handlesTouch() {
+        return true;
+    }
+
+    // focus specific code
+
+    public void setBlockFocus(boolean blocked) {
+        mBlockFocus = blocked;
+        if (blocked) {
+            clear();
+        }
+    }
+
+    public void setFocus(int x, int y) {
+        mFocusX = x;
+        mFocusY = y;
+        setCircle(mFocusX, mFocusY);
+    }
+
+    public void alignFocus(int x, int y) {
+        mOverlay.removeCallbacks(mDisappear);
+        mAnimation.cancel();
+        mAnimation.reset();
+        mFocusX = x;
+        mFocusY = y;
+        mDialAngle = DIAL_HORIZONTAL;
+        setCircle(x, y);
+        mFocused = false;
+    }
+
+    public int getSize() {
+        return 2 * mCircleSize;
+    }
+
+    private int getRandomRange() {
+        return (int)(-60 + 120 * Math.random());
+    }
+
+    private void setCircle(int cx, int cy) {
+        mCircle.set(cx - mCircleSize, cy - mCircleSize,
+                cx + mCircleSize, cy + mCircleSize);
+        mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
+                cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
+    }
+
+    public void drawFocus(Canvas canvas) {
+        if (mBlockFocus) return;
+        mFocusPaint.setStrokeWidth(mOuterStroke);
+        canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
+        if (mState == STATE_PIE) return;
+        int color = mFocusPaint.getColor();
+        if (mState == STATE_FINISHING) {
+            mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
+        }
+        mFocusPaint.setStrokeWidth(mInnerStroke);
+        drawLine(canvas, mDialAngle, mFocusPaint);
+        drawLine(canvas, mDialAngle + 45, mFocusPaint);
+        drawLine(canvas, mDialAngle + 180, mFocusPaint);
+        drawLine(canvas, mDialAngle + 225, mFocusPaint);
+        canvas.save();
+        // rotate the arc instead of its offset to better use framework's shape caching
+        canvas.rotate(mDialAngle, mFocusX, mFocusY);
+        canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
+        canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
+        canvas.restore();
+        mFocusPaint.setColor(color);
+    }
+
+    private void drawLine(Canvas canvas, int angle, Paint p) {
+        convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
+        convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
+        canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
+                mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
+    }
+
+    private static void convertCart(int angle, int radius, Point out) {
+        double a = 2 * Math.PI * (angle % 360) / 360;
+        out.x = (int) (radius * Math.cos(a) + 0.5);
+        out.y = (int) (radius * Math.sin(a) + 0.5);
+    }
+
+    @Override
+    public void showStart() {
+        if (mState == STATE_PIE) return;
+        cancelFocus();
+        mStartAnimationAngle = 67;
+        int range = getRandomRange();
+        startAnimation(SCALING_UP_TIME,
+                false, mStartAnimationAngle, mStartAnimationAngle + range);
+        mState = STATE_FOCUSING;
+    }
+
+    @Override
+    public void showSuccess(boolean timeout) {
+        if (mState == STATE_FOCUSING) {
+            startAnimation(SCALING_DOWN_TIME,
+                    timeout, mStartAnimationAngle);
+            mState = STATE_FINISHING;
+            mFocused = true;
+        }
+    }
+
+    @Override
+    public void showFail(boolean timeout) {
+        if (mState == STATE_FOCUSING) {
+            startAnimation(SCALING_DOWN_TIME,
+                    timeout, mStartAnimationAngle);
+            mState = STATE_FINISHING;
+            mFocused = false;
+        }
+    }
+
+    private void cancelFocus() {
+        mFocusCancelled = true;
+        mOverlay.removeCallbacks(mDisappear);
+        if (mAnimation != null && !mAnimation.hasEnded()) {
+            mAnimation.cancel();
+        }
+        mFocusCancelled = false;
+        mFocused = false;
+        mState = STATE_IDLE;
+    }
+
+    @Override
+    public void clear() {
+        if (mState == STATE_PIE) return;
+        cancelFocus();
+        mOverlay.post(mDisappear);
+    }
+
+    private void startAnimation(long duration, boolean timeout,
+            float toScale) {
+        startAnimation(duration, timeout, mDialAngle,
+                toScale);
+    }
+
+    private void startAnimation(long duration, boolean timeout,
+            float fromScale, float toScale) {
+        setVisible(true);
+        mAnimation.reset();
+        mAnimation.setDuration(duration);
+        mAnimation.setScale(fromScale, toScale);
+        mAnimation.setAnimationListener(timeout ? mEndAction : null);
+        mOverlay.startAnimation(mAnimation);
+        update();
+    }
+
+    private class EndAction implements Animation.AnimationListener {
+        @Override
+        public void onAnimationEnd(Animation animation) {
+            // Keep the focus indicator for some time.
+            if (!mFocusCancelled) {
+                mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
+            }
+        }
+
+        @Override
+        public void onAnimationRepeat(Animation animation) {
+        }
+
+        @Override
+        public void onAnimationStart(Animation animation) {
+        }
+    }
+
+    private class Disappear implements Runnable {
+        @Override
+        public void run() {
+            if (mState == STATE_PIE) return;
+            setVisible(false);
+            mFocusX = mCenterX;
+            mFocusY = mCenterY;
+            mState = STATE_IDLE;
+            setCircle(mFocusX, mFocusY);
+            mFocused = false;
+        }
+    }
+
+    private class ScaleAnimation extends Animation {
+        private float mFrom = 1f;
+        private float mTo = 1f;
+
+        public ScaleAnimation() {
+            setFillAfter(true);
+        }
+
+        public void setScale(float from, float to) {
+            mFrom = from;
+            mTo = to;
+        }
+
+        @Override
+        protected void applyTransformation(float interpolatedTime, Transformation t) {
+            mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
+        }
+    }
+
+}
diff --git a/src/com/android/camera/ui/PopupManager.java b/src/com/android/camera/ui/PopupManager.java
new file mode 100644
index 0000000..0dcf34f
--- /dev/null
+++ b/src/com/android/camera/ui/PopupManager.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.content.Context;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * A manager which notifies the event of a new popup in order to dismiss the
+ * old popup if exists.
+ */
+public class PopupManager {
+    private static HashMap<Context, PopupManager> sMap =
+            new HashMap<Context, PopupManager>();
+
+    public interface OnOtherPopupShowedListener {
+        public void onOtherPopupShowed();
+    }
+
+    private PopupManager() {}
+
+    private ArrayList<OnOtherPopupShowedListener> mListeners = new ArrayList<OnOtherPopupShowedListener>();
+
+    public void notifyShowPopup(View view) {
+        for (OnOtherPopupShowedListener listener : mListeners) {
+            if ((View) listener != view) {
+                listener.onOtherPopupShowed();
+            }
+        }
+    }
+
+    public void setOnOtherPopupShowedListener(OnOtherPopupShowedListener listener) {
+        mListeners.add(listener);
+    }
+
+    public static PopupManager getInstance(Context context) {
+        PopupManager instance = sMap.get(context);
+        if (instance == null) {
+            instance = new PopupManager();
+            sMap.put(context, instance);
+        }
+        return instance;
+    }
+
+    public static void removeInstance(Context context) {
+        PopupManager instance = sMap.get(context);
+        sMap.remove(context);
+    }
+}
diff --git a/src/com/android/camera/ui/PreviewSurfaceView.java b/src/com/android/camera/ui/PreviewSurfaceView.java
new file mode 100644
index 0000000..9a428e2
--- /dev/null
+++ b/src/com/android/camera/ui/PreviewSurfaceView.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.common.ApiHelper;
+
+public class PreviewSurfaceView extends SurfaceView {
+    public PreviewSurfaceView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setZOrderMediaOverlay(true);
+        getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+    }
+
+    public void shrink() {
+        setLayoutSize(1);
+    }
+
+    public void expand() {
+        setLayoutSize(ViewGroup.LayoutParams.MATCH_PARENT);
+    }
+
+    private void setLayoutSize(int size) {
+        ViewGroup.LayoutParams p = getLayoutParams();
+        if (p.width != size || p.height != size) {
+            p.width = size;
+            p.height = size;
+            setLayoutParams(p);
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/RenderOverlay.java b/src/com/android/camera/ui/RenderOverlay.java
new file mode 100644
index 0000000..d82ce18
--- /dev/null
+++ b/src/com/android/camera/ui/RenderOverlay.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.PreviewGestures;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RenderOverlay extends FrameLayout {
+
+    private static final String TAG = "CAM_Overlay";
+
+    interface Renderer {
+
+        public boolean handlesTouch();
+        public boolean onTouchEvent(MotionEvent evt);
+        public void setOverlay(RenderOverlay overlay);
+        public void layout(int left, int top, int right, int bottom);
+        public void draw(Canvas canvas);
+
+    }
+
+    private RenderView mRenderView;
+    private List<Renderer> mClients;
+    private PreviewGestures mGestures;
+    // reverse list of touch clients
+    private List<Renderer> mTouchClients;
+    private int[] mPosition = new int[2];
+
+    public RenderOverlay(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mRenderView = new RenderView(context);
+        addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT));
+        mClients = new ArrayList<Renderer>(10);
+        mTouchClients = new ArrayList<Renderer>(10);
+        setWillNotDraw(false);
+    }
+
+    public void setGestures(PreviewGestures gestures) {
+        mGestures = gestures;
+    }
+
+    public void addRenderer(Renderer renderer) {
+        mClients.add(renderer);
+        renderer.setOverlay(this);
+        if (renderer.handlesTouch()) {
+            mTouchClients.add(0, renderer);
+        }
+        renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+    }
+
+    public void addRenderer(int pos, Renderer renderer) {
+        mClients.add(pos, renderer);
+        renderer.setOverlay(this);
+        renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+    }
+
+    public void remove(Renderer renderer) {
+        mClients.remove(renderer);
+        renderer.setOverlay(null);
+    }
+
+    public int getClientSize() {
+        return mClients.size();
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent m) {
+        if (mGestures != null) {
+            if (!mGestures.isEnabled()) return false;
+            mGestures.dispatchTouch(m);
+        }
+        return true;
+    }
+
+    public boolean directDispatchTouch(MotionEvent m, Renderer target) {
+        mRenderView.setTouchTarget(target);
+        boolean res = mRenderView.dispatchTouchEvent(m);
+        mRenderView.setTouchTarget(null);
+        return res;
+    }
+
+    private void adjustPosition() {
+        getLocationInWindow(mPosition);
+    }
+
+    public int getWindowPositionX() {
+        return mPosition[0];
+    }
+
+    public int getWindowPositionY() {
+        return mPosition[1];
+    }
+
+    public void update() {
+        mRenderView.invalidate();
+    }
+
+    private class RenderView extends View {
+
+        private Renderer mTouchTarget;
+
+        public RenderView(Context context) {
+            super(context);
+            setWillNotDraw(false);
+        }
+
+        public void setTouchTarget(Renderer target) {
+            mTouchTarget = target;
+        }
+
+        @Override
+        public boolean dispatchTouchEvent(MotionEvent evt) {
+
+            if (mTouchTarget != null) {
+                return mTouchTarget.onTouchEvent(evt);
+            }
+            if (mTouchClients != null) {
+                boolean res = false;
+                for (Renderer client : mTouchClients) {
+                    res |= client.onTouchEvent(evt);
+                }
+                return res;
+            }
+            return false;
+        }
+
+        @Override
+        public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+            adjustPosition();
+            super.onLayout(changed, left,  top, right, bottom);
+            if (mClients == null) return;
+            for (Renderer renderer : mClients) {
+                renderer.layout(left, top, right, bottom);
+            }
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            super.draw(canvas);
+            if (mClients == null) return;
+            boolean redraw = false;
+            for (Renderer renderer : mClients) {
+                renderer.draw(canvas);
+                redraw = redraw || ((OverlayRenderer) renderer).isVisible();
+            }
+            if (redraw) {
+                invalidate();
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/camera/ui/Rotatable.java b/src/com/android/camera/ui/Rotatable.java
new file mode 100644
index 0000000..6d428b8
--- /dev/null
+++ b/src/com/android/camera/ui/Rotatable.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+public interface Rotatable {
+    // Set parameter 'animation' to true to have animation when rotation.
+    public void setOrientation(int orientation, boolean animation);
+}
diff --git a/src/com/android/camera/ui/RotatableLayout.java b/src/com/android/camera/ui/RotatableLayout.java
new file mode 100644
index 0000000..965d62a
--- /dev/null
+++ b/src/com/android/camera/ui/RotatableLayout.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2013 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.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+
+/* RotatableLayout rotates itself as well as all its children when orientation
+ * changes. Specifically, when going from portrait to landscape, camera
+ * controls move from the bottom of the screen to right side of the screen
+ * (i.e. counter clockwise). Similarly, when the screen changes to portrait, we
+ * need to move the controls from right side to the bottom of the screen, which
+ * is a clockwise rotation.
+ */
+
+public class RotatableLayout extends FrameLayout {
+
+    private static final String TAG = "RotatableLayout";
+    // Initial orientation of the layout (ORIENTATION_PORTRAIT, or ORIENTATION_LANDSCAPE)
+    private int mInitialOrientation;
+    private int mPrevRotation;
+    private RotationListener mListener = null;
+    public interface RotationListener {
+        public void onRotation(int rotation);
+    }
+    public RotatableLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mInitialOrientation = getResources().getConfiguration().orientation;
+    }
+
+    public RotatableLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mInitialOrientation = getResources().getConfiguration().orientation;
+    }
+
+    public RotatableLayout(Context context) {
+        super(context);
+        mInitialOrientation = getResources().getConfiguration().orientation;
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        mPrevRotation = Util.getDisplayRotation((Activity) getContext());
+        // check if there is any rotation before the view is attached to window
+        int currentOrientation = getResources().getConfiguration().orientation;
+        int orientation = getUnifiedRotation();
+        if (mInitialOrientation == currentOrientation && orientation < 180) {
+            return;
+        }
+
+        if (mInitialOrientation == Configuration.ORIENTATION_LANDSCAPE
+                && currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
+            rotateLayout(true);
+        } else if (mInitialOrientation == Configuration.ORIENTATION_PORTRAIT
+                && currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+            rotateLayout(false);
+        }
+        // In reverse landscape and reverse portrait, camera controls will be laid out
+        // on the wrong side of the screen. We need to make adjustment to move the controls
+        // to the USB side
+        if (orientation >= 180) {
+            flipChildren();
+        }
+    }
+
+    protected int getUnifiedRotation() {
+        // all the layout code assumes camera device orientation to be portrait
+        // adjust rotation for landscape
+        int orientation = getResources().getConfiguration().orientation;
+        int rotation = Util.getDisplayRotation((Activity) getContext());
+        int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT
+                : Configuration.ORIENTATION_LANDSCAPE;
+        if (camOrientation != orientation) {
+            return (rotation + 90) % 360;
+        }
+        return rotation;
+    }
+
+    public void checkLayoutFlip() {
+        int currentRotation = Util.getDisplayRotation((Activity) getContext());
+        if ((currentRotation - mPrevRotation + 360) % 360 == 180) {
+            mPrevRotation = currentRotation;
+            flipChildren();
+            getParent().requestLayout();
+        }
+    }
+
+    @Override
+    public void onWindowVisibilityChanged(int visibility) {
+        if (visibility == View.VISIBLE) {
+            // Make sure when coming back from onPause, the layout is rotated correctly
+            checkLayoutFlip();
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        int rotation = Util.getDisplayRotation((Activity) getContext());
+        int diff = (rotation - mPrevRotation + 360) % 360;
+        if ( diff == 0) {
+            // No rotation
+            return;
+        } else if (diff == 180) {
+            // 180-degree rotation
+            mPrevRotation = rotation;
+            flipChildren();
+            return;
+        }
+        // 90 or 270-degree rotation
+        boolean clockwise = isClockWiseRotation(mPrevRotation, rotation);
+        mPrevRotation = rotation;
+        rotateLayout(clockwise);
+    }
+
+    protected void rotateLayout(boolean clockwise) {
+        // Change the size of the layout
+        ViewGroup.LayoutParams lp = getLayoutParams();
+        int width = lp.width;
+        int height = lp.height;
+        lp.height = width;
+        lp.width = height;
+        setLayoutParams(lp);
+
+        // rotate all the children
+        rotateChildren(clockwise);
+    }
+
+    protected void rotateChildren(boolean clockwise) {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            rotate(child, clockwise);
+        }
+        if (mListener != null) mListener.onRotation(clockwise ? 90 : 270);
+    }
+
+    protected void flipChildren() {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            flip(child);
+        }
+        if (mListener != null) mListener.onRotation(180);
+    }
+
+    public void setRotationListener(RotationListener listener) {
+        mListener = listener;
+    }
+
+    public static boolean isClockWiseRotation(int prevRotation, int currentRotation) {
+        if (prevRotation == (currentRotation + 90) % 360) {
+            return true;
+        }
+        return false;
+    }
+
+    public static void rotate(View view, boolean isClockwise) {
+        if (isClockwise) {
+            rotateClockwise(view);
+        } else {
+            rotateCounterClockwise(view);
+        }
+    }
+
+    private static boolean contains(int value, int mask) {
+        return (value & mask) == mask;
+    }
+
+    public static void rotateClockwise(View view) {
+        if (view == null) return;
+        LayoutParams lp = (LayoutParams) view.getLayoutParams();
+        int gravity = lp.gravity;
+        int ngravity = 0;
+        // rotate gravity
+        if (contains(gravity, Gravity.LEFT)) {
+            ngravity |= Gravity.TOP;
+        }
+        if (contains(gravity, Gravity.RIGHT)) {
+            ngravity |= Gravity.BOTTOM;
+        }
+        if (contains(gravity, Gravity.TOP)) {
+            ngravity |= Gravity.RIGHT;
+        }
+        if (contains(gravity, Gravity.BOTTOM)) {
+            ngravity |= Gravity.LEFT;
+        }
+        if (contains(gravity, Gravity.CENTER)) {
+            ngravity |= Gravity.CENTER;
+        }
+        if (contains(gravity, Gravity.CENTER_HORIZONTAL)) {
+            ngravity |= Gravity.CENTER_VERTICAL;
+        }
+        if (contains(gravity, Gravity.CENTER_VERTICAL)) {
+            ngravity |= Gravity.CENTER_HORIZONTAL;
+        }
+        lp.gravity = ngravity;
+        int ml = lp.leftMargin;
+        int mr = lp.rightMargin;
+        int mt = lp.topMargin;
+        int mb = lp.bottomMargin;
+        lp.leftMargin = mb;
+        lp.rightMargin = mt;
+        lp.topMargin = ml;
+        lp.bottomMargin = mr;
+        int width = lp.width;
+        int height = lp.height;
+        lp.width = height;
+        lp.height = width;
+        view.setLayoutParams(lp);
+    }
+
+    public static void rotateCounterClockwise(View view) {
+        if (view == null) return;
+        LayoutParams lp = (LayoutParams) view.getLayoutParams();
+        int gravity = lp.gravity;
+        int ngravity = 0;
+        // change gravity
+        if (contains(gravity, Gravity.RIGHT)) {
+            ngravity |= Gravity.TOP;
+        }
+        if (contains(gravity, Gravity.LEFT)) {
+            ngravity |= Gravity.BOTTOM;
+        }
+        if (contains(gravity, Gravity.TOP)) {
+            ngravity |= Gravity.LEFT;
+        }
+        if (contains(gravity, Gravity.BOTTOM)) {
+            ngravity |= Gravity.RIGHT;
+        }
+        if (contains(gravity, Gravity.CENTER)) {
+            ngravity |= Gravity.CENTER;
+        }
+        if (contains(gravity, Gravity.CENTER_HORIZONTAL)) {
+            ngravity |= Gravity.CENTER_VERTICAL;
+        }
+        if (contains(gravity, Gravity.CENTER_VERTICAL)) {
+            ngravity |= Gravity.CENTER_HORIZONTAL;
+        }
+        lp.gravity = ngravity;
+        int ml = lp.leftMargin;
+        int mr = lp.rightMargin;
+        int mt = lp.topMargin;
+        int mb = lp.bottomMargin;
+        lp.leftMargin = mt;
+        lp.rightMargin = mb;
+        lp.topMargin = mr;
+        lp.bottomMargin = ml;
+        int width = lp.width;
+        int height = lp.height;
+        lp.width = height;
+        lp.height = width;
+        view.setLayoutParams(lp);
+    }
+
+    // Rotate a given view 180 degrees
+    public static void flip(View view) {
+        rotateClockwise(view);
+        rotateClockwise(view);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/camera/ui/RotateImageView.java b/src/com/android/camera/ui/RotateImageView.java
new file mode 100644
index 0000000..05e1a7c
--- /dev/null
+++ b/src/com/android/camera/ui/RotateImageView.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.ThumbnailUtils;
+import android.util.AttributeSet;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+
+/**
+ * A @{code ImageView} which can rotate it's content.
+ */
+public class RotateImageView extends TwoStateImageView implements Rotatable {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "RotateImageView";
+
+    private static final int ANIMATION_SPEED = 270; // 270 deg/sec
+
+    private int mCurrentDegree = 0; // [0, 359]
+    private int mStartDegree = 0;
+    private int mTargetDegree = 0;
+
+    private boolean mClockwise = false, mEnableAnimation = true;
+
+    private long mAnimationStartTime = 0;
+    private long mAnimationEndTime = 0;
+
+    public RotateImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public RotateImageView(Context context) {
+        super(context);
+    }
+
+    protected int getDegree() {
+        return mTargetDegree;
+    }
+
+    // Rotate the view counter-clockwise
+    @Override
+    public void setOrientation(int degree, boolean animation) {
+        mEnableAnimation = animation;
+        // make sure in the range of [0, 359]
+        degree = degree >= 0 ? degree % 360 : degree % 360 + 360;
+        if (degree == mTargetDegree) return;
+
+        mTargetDegree = degree;
+        if (mEnableAnimation) {
+            mStartDegree = mCurrentDegree;
+            mAnimationStartTime = AnimationUtils.currentAnimationTimeMillis();
+
+            int diff = mTargetDegree - mCurrentDegree;
+            diff = diff >= 0 ? diff : 360 + diff; // make it in range [0, 359]
+
+            // Make it in range [-179, 180]. That's the shorted distance between the
+            // two angles
+            diff = diff > 180 ? diff - 360 : diff;
+
+            mClockwise = diff >= 0;
+            mAnimationEndTime = mAnimationStartTime
+                    + Math.abs(diff) * 1000 / ANIMATION_SPEED;
+        } else {
+            mCurrentDegree = mTargetDegree;
+        }
+
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        Drawable drawable = getDrawable();
+        if (drawable == null) return;
+
+        Rect bounds = drawable.getBounds();
+        int w = bounds.right - bounds.left;
+        int h = bounds.bottom - bounds.top;
+
+        if (w == 0 || h == 0) return; // nothing to draw
+
+        if (mCurrentDegree != mTargetDegree) {
+            long time = AnimationUtils.currentAnimationTimeMillis();
+            if (time < mAnimationEndTime) {
+                int deltaTime = (int)(time - mAnimationStartTime);
+                int degree = mStartDegree + ANIMATION_SPEED
+                        * (mClockwise ? deltaTime : -deltaTime) / 1000;
+                degree = degree >= 0 ? degree % 360 : degree % 360 + 360;
+                mCurrentDegree = degree;
+                invalidate();
+            } else {
+                mCurrentDegree = mTargetDegree;
+            }
+        }
+
+        int left = getPaddingLeft();
+        int top = getPaddingTop();
+        int right = getPaddingRight();
+        int bottom = getPaddingBottom();
+        int width = getWidth() - left - right;
+        int height = getHeight() - top - bottom;
+
+        int saveCount = canvas.getSaveCount();
+
+        // Scale down the image first if required.
+        if ((getScaleType() == ImageView.ScaleType.FIT_CENTER) &&
+                ((width < w) || (height < h))) {
+            float ratio = Math.min((float) width / w, (float) height / h);
+            canvas.scale(ratio, ratio, width / 2.0f, height / 2.0f);
+        }
+        canvas.translate(left + width / 2, top + height / 2);
+        canvas.rotate(-mCurrentDegree);
+        canvas.translate(-w / 2, -h / 2);
+        drawable.draw(canvas);
+        canvas.restoreToCount(saveCount);
+    }
+
+    private Bitmap mThumb;
+    private Drawable[] mThumbs;
+    private TransitionDrawable mThumbTransition;
+
+    public void setBitmap(Bitmap bitmap) {
+        // Make sure uri and original are consistently both null or both
+        // non-null.
+        if (bitmap == null) {
+            mThumb = null;
+            mThumbs = null;
+            setImageDrawable(null);
+            setVisibility(GONE);
+            return;
+        }
+
+        LayoutParams param = getLayoutParams();
+        final int miniThumbWidth = param.width
+                - getPaddingLeft() - getPaddingRight();
+        final int miniThumbHeight = param.height
+                - getPaddingTop() - getPaddingBottom();
+        mThumb = ThumbnailUtils.extractThumbnail(
+                bitmap, miniThumbWidth, miniThumbHeight);
+        Drawable drawable;
+        if (mThumbs == null || !mEnableAnimation) {
+            mThumbs = new Drawable[2];
+            mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb);
+            setImageDrawable(mThumbs[1]);
+        } else {
+            mThumbs[0] = mThumbs[1];
+            mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb);
+            mThumbTransition = new TransitionDrawable(mThumbs);
+            setImageDrawable(mThumbTransition);
+            mThumbTransition.startTransition(500);
+        }
+        setVisibility(VISIBLE);
+    }
+}
diff --git a/src/com/android/camera/ui/RotateLayout.java b/src/com/android/camera/ui/RotateLayout.java
new file mode 100644
index 0000000..86f5c81
--- /dev/null
+++ b/src/com/android/camera/ui/RotateLayout.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.MotionEventHelper;
+
+// A RotateLayout is designed to display a single item and provides the
+// capabilities to rotate the item.
+public class RotateLayout extends ViewGroup implements Rotatable {
+    @SuppressWarnings("unused")
+    private static final String TAG = "RotateLayout";
+    private int mOrientation;
+    private Matrix mMatrix = new Matrix();
+    protected View mChild;
+
+    public RotateLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        // The transparent background here is a workaround of the render issue
+        // happened when the view is rotated as the device's orientation
+        // changed. The view looks fine in landscape. After rotation, the view
+        // is invisible.
+        setBackgroundResource(android.R.color.transparent);
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    @Override
+    protected void onFinishInflate() {
+        mChild = getChildAt(0);
+        if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+            mChild.setPivotX(0);
+            mChild.setPivotY(0);
+        }
+    }
+
+    @Override
+    protected void onLayout(
+            boolean change, int left, int top, int right, int bottom) {
+        int width = right - left;
+        int height = bottom - top;
+        switch (mOrientation) {
+            case 0:
+            case 180:
+                mChild.layout(0, 0, width, height);
+                break;
+            case 90:
+            case 270:
+                mChild.layout(0, 0, height, width);
+                break;
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+            final int w = getMeasuredWidth();
+            final int h = getMeasuredHeight();
+            switch (mOrientation) {
+                case 0:
+                    mMatrix.setTranslate(0, 0);
+                    break;
+                case 90:
+                    mMatrix.setTranslate(0, -h);
+                    break;
+                case 180:
+                    mMatrix.setTranslate(-w, -h);
+                    break;
+                case 270:
+                    mMatrix.setTranslate(-w, 0);
+                    break;
+            }
+            mMatrix.postRotate(mOrientation);
+            event = MotionEventHelper.transformEvent(event, mMatrix);
+        }
+        return super.dispatchTouchEvent(event);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+            super.dispatchDraw(canvas);
+        } else {
+            canvas.save();
+            int w = getMeasuredWidth();
+            int h = getMeasuredHeight();
+            switch (mOrientation) {
+                case 0:
+                    canvas.translate(0, 0);
+                    break;
+                case 90:
+                    canvas.translate(0, h);
+                    break;
+                case 180:
+                    canvas.translate(w, h);
+                    break;
+                case 270:
+                    canvas.translate(w, 0);
+                    break;
+            }
+            canvas.rotate(-mOrientation, 0, 0);
+            super.dispatchDraw(canvas);
+            canvas.restore();
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        int w = 0, h = 0;
+        switch(mOrientation) {
+            case 0:
+            case 180:
+                measureChild(mChild, widthSpec, heightSpec);
+                w = mChild.getMeasuredWidth();
+                h = mChild.getMeasuredHeight();
+                break;
+            case 90:
+            case 270:
+                measureChild(mChild, heightSpec, widthSpec);
+                w = mChild.getMeasuredHeight();
+                h = mChild.getMeasuredWidth();
+                break;
+        }
+        setMeasuredDimension(w, h);
+
+        if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+            switch (mOrientation) {
+                case 0:
+                    mChild.setTranslationX(0);
+                    mChild.setTranslationY(0);
+                    break;
+                case 90:
+                    mChild.setTranslationX(0);
+                    mChild.setTranslationY(h);
+                    break;
+                case 180:
+                    mChild.setTranslationX(w);
+                    mChild.setTranslationY(h);
+                    break;
+                case 270:
+                    mChild.setTranslationX(w);
+                    mChild.setTranslationY(0);
+                    break;
+            }
+            mChild.setRotation(-mOrientation);
+        }
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    // Rotate the view counter-clockwise
+    @Override
+    public void setOrientation(int orientation, boolean animation) {
+        orientation = orientation % 360;
+        if (mOrientation == orientation) return;
+        mOrientation = orientation;
+        requestLayout();
+    }
+
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    @Override
+    public ViewParent invalidateChildInParent(int[] location, Rect r) {
+        if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES && mOrientation != 0) {
+            // The workaround invalidates the entire rotate layout. After
+            // rotation, the correct area to invalidate may be larger than the
+            // size of the child. Ex: ListView. There is no way to invalidate
+            // only the necessary area.
+            r.set(0, 0, getWidth(), getHeight());
+        }
+        return super.invalidateChildInParent(location, r);
+    }
+}
diff --git a/src/com/android/camera/ui/RotateTextToast.java b/src/com/android/camera/ui/RotateTextToast.java
new file mode 100644
index 0000000..c78a258
--- /dev/null
+++ b/src/com/android/camera/ui/RotateTextToast.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+
+public class RotateTextToast {
+    private static final int TOAST_DURATION = 5000; // milliseconds
+    ViewGroup mLayoutRoot;
+    RotateLayout mToast;
+    Handler mHandler;
+
+    public RotateTextToast(Activity activity, int textResourceId, int orientation) {
+        mLayoutRoot = (ViewGroup) activity.getWindow().getDecorView();
+        LayoutInflater inflater = activity.getLayoutInflater();
+        View v = inflater.inflate(R.layout.rotate_text_toast, mLayoutRoot);
+        mToast = (RotateLayout) v.findViewById(R.id.rotate_toast);
+        TextView tv = (TextView) mToast.findViewById(R.id.message);
+        tv.setText(textResourceId);
+        mToast.setOrientation(orientation, false);
+        mHandler = new Handler();
+    }
+
+    private final Runnable mRunnable = new Runnable() {
+        @Override
+        public void run() {
+            Util.fadeOut(mToast);
+            mLayoutRoot.removeView(mToast);
+            mToast = null;
+        }
+    };
+
+    public void show() {
+        mToast.setVisibility(View.VISIBLE);
+        mHandler.postDelayed(mRunnable, TOAST_DURATION);
+    }
+}
diff --git a/src/com/android/camera/ui/Switch.java b/src/com/android/camera/ui/Switch.java
new file mode 100644
index 0000000..ac21758
--- /dev/null
+++ b/src/com/android/camera/ui/Switch.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.CompoundButton;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.Arrays;
+
+/**
+ * A Switch is a two-state toggle switch widget that can select between two
+ * options. The user may drag the "thumb" back and forth to choose the selected option,
+ * or simply tap to toggle as if it were a checkbox.
+ */
+public class Switch extends CompoundButton {
+    private static final int TOUCH_MODE_IDLE = 0;
+    private static final int TOUCH_MODE_DOWN = 1;
+    private static final int TOUCH_MODE_DRAGGING = 2;
+
+    private Drawable mThumbDrawable;
+    private Drawable mTrackDrawable;
+    private int mThumbTextPadding;
+    private int mSwitchMinWidth;
+    private int mSwitchTextMaxWidth;
+    private int mSwitchPadding;
+    private CharSequence mTextOn;
+    private CharSequence mTextOff;
+
+    private int mTouchMode;
+    private int mTouchSlop;
+    private float mTouchX;
+    private float mTouchY;
+    private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private int mMinFlingVelocity;
+
+    private float mThumbPosition;
+    private int mSwitchWidth;
+    private int mSwitchHeight;
+    private int mThumbWidth; // Does not include padding
+
+    private int mSwitchLeft;
+    private int mSwitchTop;
+    private int mSwitchRight;
+    private int mSwitchBottom;
+
+    private TextPaint mTextPaint;
+    private ColorStateList mTextColors;
+    private Layout mOnLayout;
+    private Layout mOffLayout;
+
+    @SuppressWarnings("hiding")
+    private final Rect mTempRect = new Rect();
+
+    private static final int[] CHECKED_STATE_SET = {
+        android.R.attr.state_checked
+    };
+
+    /**
+     * Construct a new Switch with default styling, overriding specific style
+     * attributes as requested.
+     *
+     * @param context The Context that will determine this widget's theming.
+     * @param attrs Specification of attributes that should deviate from default styling.
+     */
+    public Switch(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.switchStyle);
+    }
+
+    /**
+     * Construct a new Switch with a default style determined by the given theme attribute,
+     * overriding specific style attributes as requested.
+     *
+     * @param context The Context that will determine this widget's theming.
+     * @param attrs Specification of attributes that should deviate from the default styling.
+     * @param defStyle An attribute ID within the active theme containing a reference to the
+     *                 default style for this widget. e.g. android.R.attr.switchStyle.
+     */
+    public Switch(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+        Resources res = getResources();
+        DisplayMetrics dm = res.getDisplayMetrics();
+        mTextPaint.density = dm.density;
+        mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark);
+        mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark);
+        mTextOn = res.getString(R.string.capital_on);
+        mTextOff = res.getString(R.string.capital_off);
+        mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding);
+        mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width);
+        mSwitchTextMaxWidth = res.getDimensionPixelSize(R.dimen.switch_text_max_width);
+        mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding);
+        setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small);
+
+        ViewConfiguration config = ViewConfiguration.get(context);
+        mTouchSlop = config.getScaledTouchSlop();
+        mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
+
+        // Refresh display with current params
+        refreshDrawableState();
+        setChecked(isChecked());
+    }
+
+    /**
+     * Sets the switch text color, size, style, hint color, and highlight color
+     * from the specified TextAppearance resource.
+     */
+    public void setSwitchTextAppearance(Context context, int resid) {
+        Resources res = getResources();
+        mTextColors = getTextColors();
+        int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size);
+        if (ts != mTextPaint.getTextSize()) {
+            mTextPaint.setTextSize(ts);
+            requestLayout();
+        }
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        if (mOnLayout == null) {
+            mOnLayout = makeLayout(mTextOn, mSwitchTextMaxWidth);
+        }
+        if (mOffLayout == null) {
+            mOffLayout = makeLayout(mTextOff, mSwitchTextMaxWidth);
+        }
+
+        mTrackDrawable.getPadding(mTempRect);
+        final int maxTextWidth = Math.min(mSwitchTextMaxWidth,
+                Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()));
+        final int switchWidth = Math.max(mSwitchMinWidth,
+                maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
+        final int switchHeight = mTrackDrawable.getIntrinsicHeight();
+
+        mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
+
+        mSwitchWidth = switchWidth;
+        mSwitchHeight = switchHeight;
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        final int measuredHeight = getMeasuredHeight();
+        final int measuredWidth = getMeasuredWidth();
+        if (measuredHeight < switchHeight) {
+            setMeasuredDimension(measuredWidth, switchHeight);
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    @Override
+    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+        super.onPopulateAccessibilityEvent(event);
+        CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText();
+        if (!TextUtils.isEmpty(text)) {
+            event.getText().add(text);
+        }
+    }
+
+    private Layout makeLayout(CharSequence text, int maxWidth) {
+        int actual_width = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint));
+        StaticLayout l = new StaticLayout(text, 0, text.length(), mTextPaint,
+                actual_width,
+                Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true,
+                TextUtils.TruncateAt.END,
+                (int) Math.min(actual_width, maxWidth));
+        return l;
+    }
+
+    /**
+     * @return true if (x, y) is within the target area of the switch thumb
+     */
+    private boolean hitThumb(float x, float y) {
+        mThumbDrawable.getPadding(mTempRect);
+        final int thumbTop = mSwitchTop - mTouchSlop;
+        final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
+        final int thumbRight = thumbLeft + mThumbWidth +
+                mTempRect.left + mTempRect.right + mTouchSlop;
+        final int thumbBottom = mSwitchBottom + mTouchSlop;
+        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        mVelocityTracker.addMovement(ev);
+        final int action = ev.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                if (isEnabled() && hitThumb(x, y)) {
+                    mTouchMode = TOUCH_MODE_DOWN;
+                    mTouchX = x;
+                    mTouchY = y;
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                switch (mTouchMode) {
+                    case TOUCH_MODE_IDLE:
+                        // Didn't target the thumb, treat normally.
+                        break;
+
+                    case TOUCH_MODE_DOWN: {
+                        final float x = ev.getX();
+                        final float y = ev.getY();
+                        if (Math.abs(x - mTouchX) > mTouchSlop ||
+                                Math.abs(y - mTouchY) > mTouchSlop) {
+                            mTouchMode = TOUCH_MODE_DRAGGING;
+                            getParent().requestDisallowInterceptTouchEvent(true);
+                            mTouchX = x;
+                            mTouchY = y;
+                            return true;
+                        }
+                        break;
+                    }
+
+                    case TOUCH_MODE_DRAGGING: {
+                        final float x = ev.getX();
+                        final float dx = x - mTouchX;
+                        float newPos = Math.max(0,
+                                Math.min(mThumbPosition + dx, getThumbScrollRange()));
+                        if (newPos != mThumbPosition) {
+                            mThumbPosition = newPos;
+                            mTouchX = x;
+                            invalidate();
+                        }
+                        return true;
+                    }
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL: {
+                if (mTouchMode == TOUCH_MODE_DRAGGING) {
+                    stopDrag(ev);
+                    return true;
+                }
+                mTouchMode = TOUCH_MODE_IDLE;
+                mVelocityTracker.clear();
+                break;
+            }
+        }
+
+        return super.onTouchEvent(ev);
+    }
+
+    private void cancelSuperTouch(MotionEvent ev) {
+        MotionEvent cancel = MotionEvent.obtain(ev);
+        cancel.setAction(MotionEvent.ACTION_CANCEL);
+        super.onTouchEvent(cancel);
+        cancel.recycle();
+    }
+
+    /**
+     * Called from onTouchEvent to end a drag operation.
+     *
+     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
+     */
+    private void stopDrag(MotionEvent ev) {
+        mTouchMode = TOUCH_MODE_IDLE;
+        // Up and not canceled, also checks the switch has not been disabled during the drag
+        boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
+
+        cancelSuperTouch(ev);
+
+        if (commitChange) {
+            boolean newState;
+            mVelocityTracker.computeCurrentVelocity(1000);
+            float xvel = mVelocityTracker.getXVelocity();
+            if (Math.abs(xvel) > mMinFlingVelocity) {
+                newState = xvel > 0;
+            } else {
+                newState = getTargetCheckedState();
+            }
+            animateThumbToCheckedState(newState);
+        } else {
+            animateThumbToCheckedState(isChecked());
+        }
+    }
+
+    private void animateThumbToCheckedState(boolean newCheckedState) {
+        setChecked(newCheckedState);
+    }
+
+    private boolean getTargetCheckedState() {
+        return mThumbPosition >= getThumbScrollRange() / 2;
+    }
+
+    private void setThumbPosition(boolean checked) {
+        mThumbPosition = checked ? getThumbScrollRange() : 0;
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        super.setChecked(checked);
+        setThumbPosition(checked);
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        setThumbPosition(isChecked());
+
+        int switchRight;
+        int switchLeft;
+
+        switchRight = getWidth() - getPaddingRight();
+        switchLeft = switchRight - mSwitchWidth;
+
+        int switchTop = 0;
+        int switchBottom = 0;
+        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
+            default:
+            case Gravity.TOP:
+                switchTop = getPaddingTop();
+                switchBottom = switchTop + mSwitchHeight;
+                break;
+
+            case Gravity.CENTER_VERTICAL:
+                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
+                        mSwitchHeight / 2;
+                switchBottom = switchTop + mSwitchHeight;
+                break;
+
+            case Gravity.BOTTOM:
+                switchBottom = getHeight() - getPaddingBottom();
+                switchTop = switchBottom - mSwitchHeight;
+                break;
+        }
+
+        mSwitchLeft = switchLeft;
+        mSwitchTop = switchTop;
+        mSwitchBottom = switchBottom;
+        mSwitchRight = switchRight;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Draw the switch
+        int switchLeft = mSwitchLeft;
+        int switchTop = mSwitchTop;
+        int switchRight = mSwitchRight;
+        int switchBottom = mSwitchBottom;
+
+        mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
+        mTrackDrawable.draw(canvas);
+
+        canvas.save();
+
+        mTrackDrawable.getPadding(mTempRect);
+        int switchInnerLeft = switchLeft + mTempRect.left;
+        int switchInnerTop = switchTop + mTempRect.top;
+        int switchInnerRight = switchRight - mTempRect.right;
+        int switchInnerBottom = switchBottom - mTempRect.bottom;
+        canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
+
+        mThumbDrawable.getPadding(mTempRect);
+        final int thumbPos = (int) (mThumbPosition + 0.5f);
+        int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
+        int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
+
+        mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
+        mThumbDrawable.draw(canvas);
+
+        // mTextColors should not be null, but just in case
+        if (mTextColors != null) {
+            mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
+                    mTextColors.getDefaultColor()));
+        }
+        mTextPaint.drawableState = getDrawableState();
+
+        Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
+
+        canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getEllipsizedWidth() / 2,
+                (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
+        switchText.draw(canvas);
+
+        canvas.restore();
+    }
+
+    @Override
+    public int getCompoundPaddingRight() {
+        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
+        if (!TextUtils.isEmpty(getText())) {
+            padding += mSwitchPadding;
+        }
+        return padding;
+    }
+
+    private int getThumbScrollRange() {
+        if (mTrackDrawable == null) {
+            return 0;
+        }
+        mTrackDrawable.getPadding(mTempRect);
+        return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
+    }
+
+    @Override
+    protected int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+        if (isChecked()) {
+            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+        }
+        return drawableState;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        int[] myDrawableState = getDrawableState();
+
+        // Set the state of the Drawable
+        // Drawable may be null when checked state is set from XML, from super constructor
+        if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
+        if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
+
+        invalidate();
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        mThumbDrawable.jumpToCurrentState();
+        mTrackDrawable.jumpToCurrentState();
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        event.setClassName(Switch.class.getName());
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setClassName(Switch.class.getName());
+        CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+        if (!TextUtils.isEmpty(switchText)) {
+            CharSequence oldText = info.getText();
+            if (TextUtils.isEmpty(oldText)) {
+                info.setText(switchText);
+            } else {
+                StringBuilder newText = new StringBuilder();
+                newText.append(oldText).append(' ').append(switchText);
+                info.setText(newText);
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/TimeIntervalPopup.java b/src/com/android/camera/ui/TimeIntervalPopup.java
new file mode 100644
index 0000000..18ad9f5
--- /dev/null
+++ b/src/com/android/camera/ui/TimeIntervalPopup.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.NumberPicker;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.camera.IconListPreference;
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/**
+ * This is a popup window that allows users to turn on/off time lapse feature,
+ * and to select a time interval for taking a time lapse video.
+ */
+public class TimeIntervalPopup extends AbstractSettingPopup {
+    private static final String TAG = "TimeIntervalPopup";
+    private NumberPicker mNumberSpinner;
+    private NumberPicker mUnitSpinner;
+    private Switch mTimeLapseSwitch;
+    private final String[] mUnits;
+    private final String[] mDurations;
+    private IconListPreference mPreference;
+    private Listener mListener;
+    private Button mConfirmButton;
+    private TextView mHelpText;
+    private View mTimePicker;
+
+    static public interface Listener {
+        public void onListPrefChanged(ListPreference pref);
+    }
+
+    public void setSettingChangedListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public TimeIntervalPopup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        Resources res = context.getResources();
+        mUnits = res.getStringArray(R.array.pref_video_time_lapse_frame_interval_units);
+        mDurations = res
+                .getStringArray(R.array.pref_video_time_lapse_frame_interval_duration_values);
+    }
+
+    public void initialize(IconListPreference preference) {
+        mPreference = preference;
+
+        // Set title.
+        mTitle.setText(mPreference.getTitle());
+
+        // Duration
+        int durationCount = mDurations.length;
+        mNumberSpinner = (NumberPicker) findViewById(R.id.duration);
+        mNumberSpinner.setMinValue(0);
+        mNumberSpinner.setMaxValue(durationCount - 1);
+        mNumberSpinner.setDisplayedValues(mDurations);
+        mNumberSpinner.setWrapSelectorWheel(false);
+
+        // Units for duration (i.e. seconds, minutes, etc)
+        mUnitSpinner = (NumberPicker) findViewById(R.id.duration_unit);
+        mUnitSpinner.setMinValue(0);
+        mUnitSpinner.setMaxValue(mUnits.length - 1);
+        mUnitSpinner.setDisplayedValues(mUnits);
+        mUnitSpinner.setWrapSelectorWheel(false);
+
+        mTimePicker = findViewById(R.id.time_interval_picker);
+        mTimeLapseSwitch = (Switch) findViewById(R.id.time_lapse_switch);
+        mHelpText = (TextView) findViewById(R.id.set_time_interval_help_text);
+        mConfirmButton = (Button) findViewById(R.id.time_lapse_interval_set_button);
+
+        // Disable focus on the spinners to prevent keyboard from coming up
+        mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
+        mUnitSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
+
+        mTimeLapseSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                setTimeSelectionEnabled(isChecked);
+            }
+        });
+        mConfirmButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                updateInputState();
+            }
+        });
+    }
+
+    private void restoreSetting() {
+        int index = mPreference.findIndexOfValue(mPreference.getValue());
+        if (index == -1) {
+            Log.e(TAG, "Invalid preference value.");
+            mPreference.print();
+            throw new IllegalArgumentException();
+        } else if (index == 0) {
+            // default choice: time lapse off
+            mTimeLapseSwitch.setChecked(false);
+            setTimeSelectionEnabled(false);
+        } else {
+            mTimeLapseSwitch.setChecked(true);
+            setTimeSelectionEnabled(true);
+            int durationCount = mNumberSpinner.getMaxValue() + 1;
+            int unit = (index - 1) / durationCount;
+            int number = (index - 1) % durationCount;
+            mUnitSpinner.setValue(unit);
+            mNumberSpinner.setValue(number);
+        }
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        if (visibility == View.VISIBLE) {
+            if (getVisibility() != View.VISIBLE) {
+                // Set the number pickers and on/off switch to be consistent
+                // with the preference
+                restoreSetting();
+            }
+        }
+        super.setVisibility(visibility);
+    }
+
+    protected void setTimeSelectionEnabled(boolean enabled) {
+        mHelpText.setVisibility(enabled ? GONE : VISIBLE);
+        mTimePicker.setVisibility(enabled ? VISIBLE : GONE);
+    }
+
+    @Override
+    public void reloadPreference() {
+    }
+
+    private void updateInputState() {
+        if (mTimeLapseSwitch.isChecked()) {
+            int newId = mUnitSpinner.getValue() * (mNumberSpinner.getMaxValue() + 1)
+                    + mNumberSpinner.getValue() + 1;
+            mPreference.setValueIndex(newId);
+        } else {
+            mPreference.setValueIndex(0);
+        }
+
+        if (mListener != null) {
+            mListener.onListPrefChanged(mPreference);
+        }
+    }
+}
diff --git a/src/com/android/camera/ui/TwoStateImageView.java b/src/com/android/camera/ui/TwoStateImageView.java
new file mode 100644
index 0000000..cd5b27f
--- /dev/null
+++ b/src/com/android/camera/ui/TwoStateImageView.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2011 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A @{code ImageView} which change the opacity of the icon if disabled.
+ */
+public class TwoStateImageView extends ImageView {
+    private static final int ENABLED_ALPHA = 255;
+    private static final int DISABLED_ALPHA = (int) (255 * 0.4);
+    private boolean mFilterEnabled = true;
+
+    public TwoStateImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public TwoStateImageView(Context context) {
+        this(context, null);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        if (mFilterEnabled) {
+            if (enabled) {
+                setAlpha(ENABLED_ALPHA);
+            } else {
+                setAlpha(DISABLED_ALPHA);
+            }
+        }
+    }
+
+    public void enableFilter(boolean enabled) {
+        mFilterEnabled = enabled;
+    }
+}
diff --git a/src/com/android/camera/ui/ZoomRenderer.java b/src/com/android/camera/ui/ZoomRenderer.java
new file mode 100644
index 0000000..86b82b4
--- /dev/null
+++ b/src/com/android/camera/ui/ZoomRenderer.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2012 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.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.view.ScaleGestureDetector;
+
+import com.android.gallery3d.R;
+
+public class ZoomRenderer extends OverlayRenderer
+        implements ScaleGestureDetector.OnScaleGestureListener {
+
+    private static final String TAG = "CAM_Zoom";
+
+    private int mMaxZoom;
+    private int mMinZoom;
+    private OnZoomChangedListener mListener;
+
+    private ScaleGestureDetector mDetector;
+    private Paint mPaint;
+    private Paint mTextPaint;
+    private int mCircleSize;
+    private int mCenterX;
+    private int mCenterY;
+    private float mMaxCircle;
+    private float mMinCircle;
+    private int mInnerStroke;
+    private int mOuterStroke;
+    private int mZoomSig;
+    private int mZoomFraction;
+    private Rect mTextBounds;
+
+    public interface OnZoomChangedListener {
+        void onZoomStart();
+        void onZoomEnd();
+        void onZoomValueChanged(int index);  // only for immediate zoom
+    }
+
+    public ZoomRenderer(Context ctx) {
+        Resources res = ctx.getResources();
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setColor(Color.WHITE);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mTextPaint = new Paint(mPaint);
+        mTextPaint.setStyle(Paint.Style.FILL);
+        mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.zoom_font_size));
+        mTextPaint.setTextAlign(Paint.Align.LEFT);
+        mTextPaint.setAlpha(192);
+        mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
+        mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
+        mDetector = new ScaleGestureDetector(ctx, this);
+        mMinCircle = res.getDimensionPixelSize(R.dimen.zoom_ring_min);
+        mTextBounds = new Rect();
+        setVisible(false);
+    }
+
+    // set from module
+    public void setZoomMax(int zoomMaxIndex) {
+        mMaxZoom = zoomMaxIndex;
+        mMinZoom = 0;
+    }
+
+    public void setZoom(int index) {
+        mCircleSize = (int) (mMinCircle + index * (mMaxCircle - mMinCircle) / (mMaxZoom - mMinZoom));
+    }
+
+    public void setZoomValue(int value) {
+        value = value / 10;
+        mZoomSig = value / 10;
+        mZoomFraction = value % 10;
+    }
+
+    public void setOnZoomChangeListener(OnZoomChangedListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void layout(int l, int t, int r, int b) {
+        super.layout(l, t, r, b);
+        mCenterX = (r - l) / 2;
+        mCenterY = (b - t) / 2;
+        mMaxCircle = Math.min(getWidth(), getHeight());
+        mMaxCircle = (mMaxCircle - mMinCircle) / 2;
+    }
+
+    public boolean isScaling() {
+        return mDetector.isInProgress();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        mPaint.setStrokeWidth(mInnerStroke);
+        canvas.drawCircle(mCenterX, mCenterY, mMinCircle, mPaint);
+        canvas.drawCircle(mCenterX, mCenterY, mMaxCircle, mPaint);
+        canvas.drawLine(mCenterX - mMinCircle, mCenterY,
+                mCenterX - mMaxCircle - 4, mCenterY, mPaint);
+        mPaint.setStrokeWidth(mOuterStroke);
+        canvas.drawCircle((float) mCenterX, (float) mCenterY,
+                (float) mCircleSize, mPaint);
+        String txt = mZoomSig+"."+mZoomFraction+"x";
+        mTextPaint.getTextBounds(txt, 0, txt.length(), mTextBounds);
+        canvas.drawText(txt, mCenterX - mTextBounds.centerX(), mCenterY - mTextBounds.centerY(),
+                mTextPaint);
+    }
+
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+        final float sf = detector.getScaleFactor();
+        float circle = (int) (mCircleSize * sf * sf);
+        circle = Math.max(mMinCircle, circle);
+        circle = Math.min(mMaxCircle, circle);
+        if (mListener != null && (int) circle != mCircleSize) {
+            mCircleSize = (int) circle;
+            int zoom = mMinZoom + (int) ((mCircleSize - mMinCircle) * (mMaxZoom - mMinZoom) / (mMaxCircle - mMinCircle));
+            mListener.onZoomValueChanged(zoom);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        setVisible(true);
+        if (mListener != null) {
+            mListener.onZoomStart();
+        }
+        update();
+        return true;
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+        setVisible(false);
+        if (mListener != null) {
+            mListener.onZoomEnd();
+        }
+    }
+
+}
diff --git a/src/com/android/gallery3d/anim/AlphaAnimation.java b/src/com/android/gallery3d/anim/AlphaAnimation.java
new file mode 100644
index 0000000..f9f4cbd
--- /dev/null
+++ b/src/com/android/gallery3d/anim/AlphaAnimation.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.anim;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public class AlphaAnimation extends CanvasAnimation {
+    private final float mStartAlpha;
+    private final float mEndAlpha;
+    private float mCurrentAlpha;
+
+    public AlphaAnimation(float from, float to) {
+        mStartAlpha = from;
+        mEndAlpha = to;
+        mCurrentAlpha = from;
+    }
+
+    @Override
+    public void apply(GLCanvas canvas) {
+        canvas.multiplyAlpha(mCurrentAlpha);
+    }
+
+    @Override
+    public int getCanvasSaveFlags() {
+        return GLCanvas.SAVE_FLAG_ALPHA;
+    }
+
+    @Override
+    protected void onCalculate(float progress) {
+        mCurrentAlpha = Utils.clamp(mStartAlpha
+                + (mEndAlpha - mStartAlpha) * progress, 0f, 1f);
+    }
+}
diff --git a/src/com/android/gallery3d/anim/Animation.java b/src/com/android/gallery3d/anim/Animation.java
new file mode 100644
index 0000000..cc117bb
--- /dev/null
+++ b/src/com/android/gallery3d/anim/Animation.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.anim;
+
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.common.Utils;
+
+// Animation calculates a value according to the current input time.
+//
+// 1. First we need to use setDuration(int) to set the duration of the
+//    animation. The duration is in milliseconds.
+// 2. Then we should call start(). The actual start time is the first value
+//    passed to calculate(long).
+// 3. Each time we want to get an animation value, we call
+//    calculate(long currentTimeMillis) to ask the Animation to calculate it.
+//    The parameter passed to calculate(long) should be nonnegative.
+// 4. Use get() to get that value.
+//
+// In step 3, onCalculate(float progress) is called so subclasses can calculate
+// the value according to progress (progress is a value in [0,1]).
+//
+// Before onCalculate(float) is called, There is an optional interpolator which
+// can change the progress value. The interpolator can be set by
+// setInterpolator(Interpolator). If the interpolator is used, the value passed
+// to onCalculate may be (for example, the overshoot effect).
+//
+// The isActive() method returns true after the animation start() is called and
+// before calculate is passed a value which reaches the duration of the
+// animation.
+//
+// The start() method can be called again to restart the Animation.
+//
+abstract public class Animation {
+    private static final long ANIMATION_START = -1;
+    private static final long NO_ANIMATION = -2;
+
+    private long mStartTime = NO_ANIMATION;
+    private int mDuration;
+    private Interpolator mInterpolator;
+
+    public void setInterpolator(Interpolator interpolator) {
+        mInterpolator = interpolator;
+    }
+
+    public void setDuration(int duration) {
+        mDuration = duration;
+    }
+
+    public void start() {
+        mStartTime = ANIMATION_START;
+    }
+
+    public void setStartTime(long time) {
+        mStartTime = time;
+    }
+
+    public boolean isActive() {
+        return mStartTime != NO_ANIMATION;
+    }
+
+    public void forceStop() {
+        mStartTime = NO_ANIMATION;
+    }
+
+    public boolean calculate(long currentTimeMillis) {
+        if (mStartTime == NO_ANIMATION) return false;
+        if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis;
+        int elapse = (int) (currentTimeMillis - mStartTime);
+        float x = Utils.clamp((float) elapse / mDuration, 0f, 1f);
+        Interpolator i = mInterpolator;
+        onCalculate(i != null ? i.getInterpolation(x) : x);
+        if (elapse >= mDuration) mStartTime = NO_ANIMATION;
+        return mStartTime != NO_ANIMATION;
+    }
+
+    abstract protected void onCalculate(float progress);
+}
diff --git a/src/com/android/gallery3d/anim/CanvasAnimation.java b/src/com/android/gallery3d/anim/CanvasAnimation.java
new file mode 100644
index 0000000..cdc66c6
--- /dev/null
+++ b/src/com/android/gallery3d/anim/CanvasAnimation.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.anim;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public abstract class CanvasAnimation extends Animation {
+
+    public abstract int getCanvasSaveFlags();
+    public abstract void apply(GLCanvas canvas);
+}
diff --git a/src/com/android/gallery3d/anim/FloatAnimation.java b/src/com/android/gallery3d/anim/FloatAnimation.java
new file mode 100644
index 0000000..1294ec2
--- /dev/null
+++ b/src/com/android/gallery3d/anim/FloatAnimation.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.anim;
+
+public class FloatAnimation extends Animation {
+
+    private final float mFrom;
+    private final float mTo;
+    private float mCurrent;
+
+    public FloatAnimation(float from, float to, int duration) {
+        mFrom = from;
+        mTo = to;
+        mCurrent = from;
+        setDuration(duration);
+    }
+
+    @Override
+    protected void onCalculate(float progress) {
+        mCurrent = mFrom + (mTo - mFrom) * progress;
+    }
+
+    public float get() {
+        return mCurrent;
+    }
+}
diff --git a/src/com/android/gallery3d/anim/StateTransitionAnimation.java b/src/com/android/gallery3d/anim/StateTransitionAnimation.java
new file mode 100644
index 0000000..bf8a544
--- /dev/null
+++ b/src/com/android/gallery3d/anim/StateTransitionAnimation.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.anim;
+
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.TiledScreenNail;
+
+public class StateTransitionAnimation extends Animation {
+
+    public static class Spec {
+        public static final Spec OUTGOING;
+        public static final Spec INCOMING;
+        public static final Spec PHOTO_INCOMING;
+
+        private static final Interpolator DEFAULT_INTERPOLATOR =
+                new DecelerateInterpolator();
+
+        public int duration = 330;
+        public float backgroundAlphaFrom = 0;
+        public float backgroundAlphaTo = 0;
+        public float backgroundScaleFrom = 0;
+        public float backgroundScaleTo = 0;
+        public float contentAlphaFrom = 1;
+        public float contentAlphaTo = 1;
+        public float contentScaleFrom = 1;
+        public float contentScaleTo = 1;
+        public float overlayAlphaFrom = 0;
+        public float overlayAlphaTo = 0;
+        public float overlayScaleFrom = 0;
+        public float overlayScaleTo = 0;
+        public Interpolator interpolator = DEFAULT_INTERPOLATOR;
+
+        static {
+            OUTGOING = new Spec();
+            OUTGOING.backgroundAlphaFrom = 0.5f;
+            OUTGOING.backgroundAlphaTo = 0f;
+            OUTGOING.backgroundScaleFrom = 1f;
+            OUTGOING.backgroundScaleTo = 0f;
+            OUTGOING.contentAlphaFrom = 0.5f;
+            OUTGOING.contentAlphaTo = 1f;
+            OUTGOING.contentScaleFrom = 3f;
+            OUTGOING.contentScaleTo = 1f;
+
+            INCOMING = new Spec();
+            INCOMING.overlayAlphaFrom = 1f;
+            INCOMING.overlayAlphaTo = 0f;
+            INCOMING.overlayScaleFrom = 1f;
+            INCOMING.overlayScaleTo = 3f;
+            INCOMING.contentAlphaFrom = 0f;
+            INCOMING.contentAlphaTo = 1f;
+            INCOMING.contentScaleFrom = 0.25f;
+            INCOMING.contentScaleTo = 1f;
+
+            PHOTO_INCOMING = INCOMING;
+        }
+
+        private static Spec specForTransition(Transition t) {
+            switch (t) {
+                case Outgoing:
+                    return Spec.OUTGOING;
+                case Incoming:
+                    return Spec.INCOMING;
+                case PhotoIncoming:
+                    return Spec.PHOTO_INCOMING;
+                case None:
+                default:
+                    return null;
+            }
+        }
+    }
+
+    public static enum Transition { None, Outgoing, Incoming, PhotoIncoming }
+
+    private final Spec mTransitionSpec;
+    private float mCurrentContentScale;
+    private float mCurrentContentAlpha;
+    private float mCurrentBackgroundScale;
+    private float mCurrentBackgroundAlpha;
+    private float mCurrentOverlayScale;
+    private float mCurrentOverlayAlpha;
+    private RawTexture mOldScreenTexture;
+
+    public StateTransitionAnimation(Transition t, RawTexture oldScreen) {
+        this(Spec.specForTransition(t), oldScreen);
+    }
+
+    public StateTransitionAnimation(Spec spec, RawTexture oldScreen) {
+        mTransitionSpec = spec != null ? spec : Spec.OUTGOING;
+        setDuration(mTransitionSpec.duration);
+        setInterpolator(mTransitionSpec.interpolator);
+        mOldScreenTexture = oldScreen;
+        TiledScreenNail.disableDrawPlaceholder();
+    }
+
+    @Override
+    public boolean calculate(long currentTimeMillis) {
+        boolean retval = super.calculate(currentTimeMillis);
+        if (!isActive()) {
+            if (mOldScreenTexture != null) {
+                mOldScreenTexture.recycle();
+                mOldScreenTexture = null;
+            }
+            TiledScreenNail.enableDrawPlaceholder();
+        }
+        return retval;
+    }
+
+    @Override
+    protected void onCalculate(float progress) {
+        mCurrentContentScale = mTransitionSpec.contentScaleFrom
+                + (mTransitionSpec.contentScaleTo - mTransitionSpec.contentScaleFrom) * progress;
+        mCurrentContentAlpha = mTransitionSpec.contentAlphaFrom
+                + (mTransitionSpec.contentAlphaTo - mTransitionSpec.contentAlphaFrom) * progress;
+        mCurrentBackgroundAlpha = mTransitionSpec.backgroundAlphaFrom
+                + (mTransitionSpec.backgroundAlphaTo - mTransitionSpec.backgroundAlphaFrom)
+                * progress;
+        mCurrentBackgroundScale = mTransitionSpec.backgroundScaleFrom
+                + (mTransitionSpec.backgroundScaleTo - mTransitionSpec.backgroundScaleFrom)
+                * progress;
+        mCurrentOverlayScale = mTransitionSpec.overlayScaleFrom
+                + (mTransitionSpec.overlayScaleTo - mTransitionSpec.overlayScaleFrom) * progress;
+        mCurrentOverlayAlpha = mTransitionSpec.overlayAlphaFrom
+                + (mTransitionSpec.overlayAlphaTo - mTransitionSpec.overlayAlphaFrom) * progress;
+    }
+
+    private void applyOldTexture(GLView view, GLCanvas canvas, float alpha, float scale, boolean clear) {
+        if (mOldScreenTexture == null)
+            return;
+        if (clear) canvas.clearBuffer(view.getBackgroundColor());
+        canvas.save();
+        canvas.setAlpha(alpha);
+        int xOffset = view.getWidth() / 2;
+        int yOffset = view.getHeight() / 2;
+        canvas.translate(xOffset, yOffset);
+        canvas.scale(scale, scale, 1);
+        mOldScreenTexture.draw(canvas, -xOffset, -yOffset);
+        canvas.restore();
+    }
+
+    public void applyBackground(GLView view, GLCanvas canvas) {
+        if (mCurrentBackgroundAlpha > 0f) {
+            applyOldTexture(view, canvas, mCurrentBackgroundAlpha, mCurrentBackgroundScale, true);
+        }
+    }
+
+    public void applyContentTransform(GLView view, GLCanvas canvas) {
+        int xOffset = view.getWidth() / 2;
+        int yOffset = view.getHeight() / 2;
+        canvas.translate(xOffset, yOffset);
+        canvas.scale(mCurrentContentScale, mCurrentContentScale, 1);
+        canvas.translate(-xOffset, -yOffset);
+        canvas.setAlpha(mCurrentContentAlpha);
+    }
+
+    public void applyOverlay(GLView view, GLCanvas canvas) {
+        if (mCurrentOverlayAlpha > 0f) {
+            applyOldTexture(view, canvas, mCurrentOverlayAlpha, mCurrentOverlayScale, false);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
new file mode 100644
index 0000000..ac39aa5
--- /dev/null
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class AbstractGalleryActivity extends Activity implements GalleryContext {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AbstractGalleryActivity";
+    private GLRootView mGLRootView;
+    private StateManager mStateManager;
+    private GalleryActionBar mActionBar;
+    private OrientationManager mOrientationManager;
+    private TransitionStore mTransitionStore = new TransitionStore();
+    private boolean mDisableToggleStatusBar;
+    private PanoramaViewHelper mPanoramaViewHelper;
+
+    private AlertDialog mAlertDialog = null;
+    private BroadcastReceiver mMountReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (getExternalCacheDir() != null) onStorageReady();
+        }
+    };
+    private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mOrientationManager = new OrientationManager(this);
+        toggleStatusBarByOrientation();
+        getWindow().setBackgroundDrawable(null);
+        mPanoramaViewHelper = new PanoramaViewHelper(this);
+        mPanoramaViewHelper.onCreate();
+        doBindBatchService();
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        mGLRootView.lockRenderThread();
+        try {
+            super.onSaveInstanceState(outState);
+            getStateManager().saveState(outState);
+        } finally {
+            mGLRootView.unlockRenderThread();
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        mStateManager.onConfigurationChange(config);
+        getGalleryActionBar().onConfigurationChanged();
+        invalidateOptionsMenu();
+        toggleStatusBarByOrientation();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        return getStateManager().createOptionsMenu(menu);
+    }
+
+    @Override
+    public Context getAndroidContext() {
+        return this;
+    }
+
+    @Override
+    public DataManager getDataManager() {
+        return ((GalleryApp) getApplication()).getDataManager();
+    }
+
+    @Override
+    public ThreadPool getThreadPool() {
+        return ((GalleryApp) getApplication()).getThreadPool();
+    }
+
+    public synchronized StateManager getStateManager() {
+        if (mStateManager == null) {
+            mStateManager = new StateManager(this);
+        }
+        return mStateManager;
+    }
+
+    public GLRoot getGLRoot() {
+        return mGLRootView;
+    }
+
+    public OrientationManager getOrientationManager() {
+        return mOrientationManager;
+    }
+
+    @Override
+    public void setContentView(int resId) {
+        super.setContentView(resId);
+        mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
+    }
+
+    protected void onStorageReady() {
+        if (mAlertDialog != null) {
+            mAlertDialog.dismiss();
+            mAlertDialog = null;
+            unregisterReceiver(mMountReceiver);
+        }
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        if (getExternalCacheDir() == null) {
+            OnCancelListener onCancel = new OnCancelListener() {
+                @Override
+                public void onCancel(DialogInterface dialog) {
+                    finish();
+                }
+            };
+            OnClickListener onClick = new OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    dialog.cancel();
+                }
+            };
+            AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                    .setTitle(R.string.no_external_storage_title)
+                    .setMessage(R.string.no_external_storage)
+                    .setNegativeButton(android.R.string.cancel, onClick)
+                    .setOnCancelListener(onCancel);
+            if (ApiHelper.HAS_SET_ICON_ATTRIBUTE) {
+                setAlertDialogIconAttribute(builder);
+            } else {
+                builder.setIcon(android.R.drawable.ic_dialog_alert);
+            }
+            mAlertDialog = builder.show();
+            registerReceiver(mMountReceiver, mMountFilter);
+        }
+        mPanoramaViewHelper.onStart();
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static void setAlertDialogIconAttribute(
+            AlertDialog.Builder builder) {
+        builder.setIconAttribute(android.R.attr.alertDialogIcon);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mAlertDialog != null) {
+            unregisterReceiver(mMountReceiver);
+            mAlertDialog.dismiss();
+            mAlertDialog = null;
+        }
+        mPanoramaViewHelper.onStop();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mGLRootView.lockRenderThread();
+        try {
+            getStateManager().resume();
+            getDataManager().resume();
+        } finally {
+            mGLRootView.unlockRenderThread();
+        }
+        mGLRootView.onResume();
+        mOrientationManager.resume();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mOrientationManager.pause();
+        mGLRootView.onPause();
+        mGLRootView.lockRenderThread();
+        try {
+            getStateManager().pause();
+            getDataManager().pause();
+        } finally {
+            mGLRootView.unlockRenderThread();
+        }
+        GalleryBitmapPool.getInstance().clear();
+        MediaItem.getBytesBufferPool().clear();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mGLRootView.lockRenderThread();
+        try {
+            getStateManager().destroy();
+        } finally {
+            mGLRootView.unlockRenderThread();
+        }
+        doUnbindBatchService();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        mGLRootView.lockRenderThread();
+        try {
+            getStateManager().notifyActivityResult(
+                    requestCode, resultCode, data);
+        } finally {
+            mGLRootView.unlockRenderThread();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        // send the back event to the top sub-state
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            getStateManager().onBackPressed();
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    public GalleryActionBar getGalleryActionBar() {
+        if (mActionBar == null) {
+            mActionBar = new GalleryActionBar(this);
+        }
+        return mActionBar;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            return getStateManager().itemSelected(item);
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    protected void disableToggleStatusBar() {
+        mDisableToggleStatusBar = true;
+    }
+
+    // Shows status bar in portrait view, hide in landscape view
+    private void toggleStatusBarByOrientation() {
+        if (mDisableToggleStatusBar) return;
+
+        Window win = getWindow();
+        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+            win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        } else {
+            win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        }
+    }
+
+    public TransitionStore getTransitionStore() {
+        return mTransitionStore;
+    }
+
+    public PanoramaViewHelper getPanoramaViewHelper() {
+        return mPanoramaViewHelper;
+    }
+
+    protected boolean isFullscreen() {
+        return (getWindow().getAttributes().flags
+                & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0;
+    }
+
+    private BatchService mBatchService;
+    private boolean mBatchServiceIsBound = false;
+    private ServiceConnection mBatchServiceConnection = new ServiceConnection() {
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            mBatchService = ((BatchService.LocalBinder)service).getService();
+        }
+
+        public void onServiceDisconnected(ComponentName className) {
+            mBatchService = null;
+        }
+    };
+
+    private void doBindBatchService() {
+        bindService(new Intent(this, BatchService.class), mBatchServiceConnection, Context.BIND_AUTO_CREATE);
+        mBatchServiceIsBound = true;
+    }
+
+    private void doUnbindBatchService() {
+        if (mBatchServiceIsBound) {
+            // Detach our existing connection.
+            unbindService(mBatchServiceConnection);
+            mBatchServiceIsBound = false;
+        }
+    }
+
+    public ThreadPool getBatchServiceThreadPoolIfAvailable() {
+        if (mBatchServiceIsBound && mBatchService != null) {
+            return mBatchService.getThreadPool();
+        } else {
+            throw new RuntimeException("Batch service unavailable");
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
new file mode 100644
index 0000000..2f1e0c9
--- /dev/null
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PreparePageFadeoutTexture;
+import com.android.gallery3d.util.GalleryUtils;
+
+abstract public class ActivityState {
+    protected static final int FLAG_HIDE_ACTION_BAR = 1;
+    protected static final int FLAG_HIDE_STATUS_BAR = 2;
+    protected static final int FLAG_SCREEN_ON_WHEN_PLUGGED = 4;
+    protected static final int FLAG_SCREEN_ON_ALWAYS = 8;
+    protected static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 16;
+    protected static final int FLAG_SHOW_WHEN_LOCKED = 32;
+
+    protected AbstractGalleryActivity mActivity;
+    protected Bundle mData;
+    protected int mFlags;
+
+    protected ResultEntry mReceivedResults;
+    protected ResultEntry mResult;
+
+    protected static class ResultEntry {
+        public int requestCode;
+        public int resultCode = Activity.RESULT_CANCELED;
+        public Intent resultData;
+    }
+
+    private boolean mDestroyed = false;
+    private boolean mPlugged = false;
+    boolean mIsFinishing = false;
+
+    private static final String KEY_TRANSITION_IN = "transition-in";
+
+    private StateTransitionAnimation.Transition mNextTransition =
+            StateTransitionAnimation.Transition.None;
+    private StateTransitionAnimation mIntroAnimation;
+    private GLView mContentPane;
+
+    protected ActivityState() {
+    }
+
+    protected void setContentPane(GLView content) {
+        mContentPane = content;
+        if (mIntroAnimation != null) {
+            mContentPane.setIntroAnimation(mIntroAnimation);
+            mIntroAnimation = null;
+        }
+        mContentPane.setBackgroundColor(getBackgroundColor());
+        mActivity.getGLRoot().setContentPane(mContentPane);
+    }
+
+    void initialize(AbstractGalleryActivity activity, Bundle data) {
+        mActivity = activity;
+        mData = data;
+    }
+
+    public Bundle getData() {
+        return mData;
+    }
+
+    protected void onBackPressed() {
+        mActivity.getStateManager().finishState(this);
+    }
+
+    protected void setStateResult(int resultCode, Intent data) {
+        if (mResult == null) return;
+        mResult.resultCode = resultCode;
+        mResult.resultData = data;
+    }
+
+    protected void onConfigurationChanged(Configuration config) {
+    }
+
+    protected void onSaveState(Bundle outState) {
+    }
+
+    protected void onStateResult(int requestCode, int resultCode, Intent data) {
+    }
+
+    protected float[] mBackgroundColor;
+
+    protected int getBackgroundColorId() {
+        return R.color.default_background;
+    }
+
+    protected float[] getBackgroundColor() {
+        return mBackgroundColor;
+    }
+
+    protected void onCreate(Bundle data, Bundle storedState) {
+        mBackgroundColor = GalleryUtils.intColorToFloatARGBArray(
+                mActivity.getResources().getColor(getBackgroundColorId()));
+    }
+
+    protected void clearStateResult() {
+    }
+
+    BroadcastReceiver mPowerIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+                boolean plugged = (0 != intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0));
+
+                if (plugged != mPlugged) {
+                    mPlugged = plugged;
+                    setScreenFlags();
+                }
+            }
+        }
+    };
+
+    private void setScreenFlags() {
+        final Window win = mActivity.getWindow();
+        final WindowManager.LayoutParams params = win.getAttributes();
+        if ((0 != (mFlags & FLAG_SCREEN_ON_ALWAYS)) ||
+                (mPlugged && 0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED))) {
+            params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+        } else {
+            params.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+        }
+        if (0 != (mFlags & FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)) {
+            params.flags |= WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
+        } else {
+            params.flags &= ~WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
+        }
+        if (0 != (mFlags & FLAG_SHOW_WHEN_LOCKED)) {
+            params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+        } else {
+            params.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+        }
+        win.setAttributes(params);
+    }
+
+    protected void transitionOnNextPause(Class<? extends ActivityState> outgoing,
+            Class<? extends ActivityState> incoming, StateTransitionAnimation.Transition hint) {
+        if (outgoing == SinglePhotoPage.class && incoming == AlbumPage.class) {
+            mNextTransition = StateTransitionAnimation.Transition.Outgoing;
+        } else if (outgoing == AlbumPage.class && incoming == SinglePhotoPage.class) {
+            mNextTransition = StateTransitionAnimation.Transition.PhotoIncoming;
+        } else {
+            mNextTransition = hint;
+        }
+    }
+
+    protected void performHapticFeedback(int feedbackConstant) {
+        mActivity.getWindow().getDecorView().performHapticFeedback(feedbackConstant,
+                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+    }
+
+    protected void onPause() {
+        if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
+            ((Activity) mActivity).unregisterReceiver(mPowerIntentReceiver);
+        }
+        if (mNextTransition != StateTransitionAnimation.Transition.None) {
+            mActivity.getTransitionStore().put(KEY_TRANSITION_IN, mNextTransition);
+            PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mContentPane);
+            mNextTransition = StateTransitionAnimation.Transition.None;
+        }
+    }
+
+    // should only be called by StateManager
+    void resume() {
+        AbstractGalleryActivity activity = mActivity;
+        ActionBar actionBar = activity.getActionBar();
+        if (actionBar != null) {
+            if ((mFlags & FLAG_HIDE_ACTION_BAR) != 0) {
+                actionBar.hide();
+            } else {
+                actionBar.show();
+            }
+            int stateCount = mActivity.getStateManager().getStateCount();
+            mActivity.getGalleryActionBar().setDisplayOptions(stateCount > 1, true);
+            // Default behavior, this can be overridden in ActivityState's onResume.
+            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+        }
+
+        activity.invalidateOptionsMenu();
+
+        setScreenFlags();
+
+        boolean lightsOut = ((mFlags & FLAG_HIDE_STATUS_BAR) != 0);
+        mActivity.getGLRoot().setLightsOutMode(lightsOut);
+
+        ResultEntry entry = mReceivedResults;
+        if (entry != null) {
+            mReceivedResults = null;
+            onStateResult(entry.requestCode, entry.resultCode, entry.resultData);
+        }
+
+        if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
+            // we need to know whether the device is plugged in to do this correctly
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+            activity.registerReceiver(mPowerIntentReceiver, filter);
+        }
+
+        onResume();
+
+        // the transition store should be cleared after resume;
+        mActivity.getTransitionStore().clear();
+    }
+
+    // a subclass of ActivityState should override the method to resume itself
+    protected void onResume() {
+        RawTexture fade = mActivity.getTransitionStore().get(
+                PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
+        mNextTransition = mActivity.getTransitionStore().get(
+                KEY_TRANSITION_IN, StateTransitionAnimation.Transition.None);
+        if (mNextTransition != StateTransitionAnimation.Transition.None) {
+            mIntroAnimation = new StateTransitionAnimation(mNextTransition, fade);
+            mNextTransition = StateTransitionAnimation.Transition.None;
+        }
+    }
+
+    protected boolean onCreateActionBar(Menu menu) {
+        // TODO: we should return false if there is no menu to show
+        //       this is a workaround for a bug in system
+        return true;
+    }
+
+    protected boolean onItemSelected(MenuItem item) {
+        return false;
+    }
+
+    protected void onDestroy() {
+        mDestroyed = true;
+    }
+
+    boolean isDestroyed() {
+        return mDestroyed;
+    }
+
+    public boolean isFinishing() {
+        return mIsFinishing;
+    }
+
+    protected MenuInflater getSupportMenuInflater() {
+        return mActivity.getMenuInflater();
+    }
+}
diff --git a/src/com/android/gallery3d/app/AlbumDataLoader.java b/src/com/android/gallery3d/app/AlbumDataLoader.java
new file mode 100644
index 0000000..28a8228
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumDataLoader.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class AlbumDataLoader {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumDataAdapter";
+    private static final int DATA_CACHE_SIZE = 1000;
+
+    private static final int MSG_LOAD_START = 1;
+    private static final int MSG_LOAD_FINISH = 2;
+    private static final int MSG_RUN_OBJECT = 3;
+
+    private static final int MIN_LOAD_COUNT = 32;
+    private static final int MAX_LOAD_COUNT = 64;
+
+    private final MediaItem[] mData;
+    private final long[] mItemVersion;
+    private final long[] mSetVersion;
+
+    public static interface DataListener {
+        public void onContentChanged(int index);
+        public void onSizeChanged(int size);
+    }
+
+    private int mActiveStart = 0;
+    private int mActiveEnd = 0;
+
+    private int mContentStart = 0;
+    private int mContentEnd = 0;
+
+    private final MediaSet mSource;
+    private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+
+    private final Handler mMainHandler;
+    private int mSize = 0;
+
+    private DataListener mDataListener;
+    private MySourceListener mSourceListener = new MySourceListener();
+    private LoadingListener mLoadingListener;
+
+    private ReloadTask mReloadTask;
+    // the data version on which last loading failed
+    private long mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+
+    public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) {
+        mSource = mediaSet;
+
+        mData = new MediaItem[DATA_CACHE_SIZE];
+        mItemVersion = new long[DATA_CACHE_SIZE];
+        mSetVersion = new long[DATA_CACHE_SIZE];
+        Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
+        Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
+
+        mMainHandler = new SynchronizedHandler(context.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_RUN_OBJECT:
+                        ((Runnable) message.obj).run();
+                        return;
+                    case MSG_LOAD_START:
+                        if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
+                        return;
+                    case MSG_LOAD_FINISH:
+                        if (mLoadingListener != null) {
+                            boolean loadingFailed =
+                                    (mFailedVersion != MediaObject.INVALID_DATA_VERSION);
+                            mLoadingListener.onLoadingFinished(loadingFailed);
+                        }
+                        return;
+                }
+            }
+        };
+    }
+
+    public void resume() {
+        mSource.addContentListener(mSourceListener);
+        mReloadTask = new ReloadTask();
+        mReloadTask.start();
+    }
+
+    public void pause() {
+        mReloadTask.terminate();
+        mReloadTask = null;
+        mSource.removeContentListener(mSourceListener);
+    }
+
+    public MediaItem get(int index) {
+        if (!isActive(index)) {
+            return mSource.getMediaItem(index, 1).get(0);
+        }
+        return mData[index % mData.length];
+    }
+
+    public int getActiveStart() {
+        return mActiveStart;
+    }
+
+    public boolean isActive(int index) {
+        return index >= mActiveStart && index < mActiveEnd;
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    // Returns the index of the MediaItem with the given path or
+    // -1 if the path is not cached
+    public int findItem(Path id) {
+        for (int i = mContentStart; i < mContentEnd; i++) {
+            MediaItem item = mData[i % DATA_CACHE_SIZE];
+            if (item != null && id == item.getPath()) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private void clearSlot(int slotIndex) {
+        mData[slotIndex] = null;
+        mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+        mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+    }
+
+    private void setContentWindow(int contentStart, int contentEnd) {
+        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+        int end = mContentEnd;
+        int start = mContentStart;
+
+        // We need change the content window before calling reloadData(...)
+        synchronized (this) {
+            mContentStart = contentStart;
+            mContentEnd = contentEnd;
+        }
+        long[] itemVersion = mItemVersion;
+        long[] setVersion = mSetVersion;
+        if (contentStart >= end || start >= contentEnd) {
+            for (int i = start, n = end; i < n; ++i) {
+                clearSlot(i % DATA_CACHE_SIZE);
+            }
+        } else {
+            for (int i = start; i < contentStart; ++i) {
+                clearSlot(i % DATA_CACHE_SIZE);
+            }
+            for (int i = contentEnd, n = end; i < n; ++i) {
+                clearSlot(i % DATA_CACHE_SIZE);
+            }
+        }
+        if (mReloadTask != null) mReloadTask.notifyDirty();
+    }
+
+    public void setActiveWindow(int start, int end) {
+        if (start == mActiveStart && end == mActiveEnd) return;
+
+        Utils.assertTrue(start <= end
+                && end - start <= mData.length && end <= mSize);
+
+        int length = mData.length;
+        mActiveStart = start;
+        mActiveEnd = end;
+
+        // If no data is visible, keep the cache content
+        if (start == end) return;
+
+        int contentStart = Utils.clamp((start + end) / 2 - length / 2,
+                0, Math.max(0, mSize - length));
+        int contentEnd = Math.min(contentStart + length, mSize);
+        if (mContentStart > start || mContentEnd < end
+                || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
+            setContentWindow(contentStart, contentEnd);
+        }
+    }
+
+    private class MySourceListener implements ContentListener {
+        @Override
+        public void onContentDirty() {
+            if (mReloadTask != null) mReloadTask.notifyDirty();
+        }
+    }
+
+    public void setDataListener(DataListener listener) {
+        mDataListener = listener;
+    }
+
+    public void setLoadingListener(LoadingListener listener) {
+        mLoadingListener = listener;
+    }
+
+    private <T> T executeAndWait(Callable<T> callable) {
+        FutureTask<T> task = new FutureTask<T>(callable);
+        mMainHandler.sendMessage(
+                mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+        try {
+            return task.get();
+        } catch (InterruptedException e) {
+            return null;
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class UpdateInfo {
+        public long version;
+        public int reloadStart;
+        public int reloadCount;
+
+        public int size;
+        public ArrayList<MediaItem> items;
+    }
+
+    private class GetUpdateInfo implements Callable<UpdateInfo> {
+        private final long mVersion;
+
+        public GetUpdateInfo(long version) {
+            mVersion = version;
+        }
+
+        @Override
+        public UpdateInfo call() throws Exception {
+            if (mFailedVersion == mVersion) {
+                // previous loading failed, return null to pause loading
+                return null;
+            }
+            UpdateInfo info = new UpdateInfo();
+            long version = mVersion;
+            info.version = mSourceVersion;
+            info.size = mSize;
+            long setVersion[] = mSetVersion;
+            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+                int index = i % DATA_CACHE_SIZE;
+                if (setVersion[index] != version) {
+                    info.reloadStart = i;
+                    info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
+                    return info;
+                }
+            }
+            return mSourceVersion == mVersion ? null : info;
+        }
+    }
+
+    private class UpdateContent implements Callable<Void> {
+
+        private UpdateInfo mUpdateInfo;
+
+        public UpdateContent(UpdateInfo info) {
+            mUpdateInfo = info;
+        }
+
+        @Override
+        public Void call() throws Exception {
+            UpdateInfo info = mUpdateInfo;
+            mSourceVersion = info.version;
+            if (mSize != info.size) {
+                mSize = info.size;
+                if (mDataListener != null) mDataListener.onSizeChanged(mSize);
+                if (mContentEnd > mSize) mContentEnd = mSize;
+                if (mActiveEnd > mSize) mActiveEnd = mSize;
+            }
+
+            ArrayList<MediaItem> items = info.items;
+
+            mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+            if ((items == null) || items.isEmpty()) {
+                if (info.reloadCount > 0) {
+                    mFailedVersion = info.version;
+                    Log.d(TAG, "loading failed: " + mFailedVersion);
+                }
+                return null;
+            }
+            int start = Math.max(info.reloadStart, mContentStart);
+            int end = Math.min(info.reloadStart + items.size(), mContentEnd);
+
+            for (int i = start; i < end; ++i) {
+                int index = i % DATA_CACHE_SIZE;
+                mSetVersion[index] = info.version;
+                MediaItem updateItem = items.get(i - info.reloadStart);
+                long itemVersion = updateItem.getDataVersion();
+                if (mItemVersion[index] != itemVersion) {
+                    mItemVersion[index] = itemVersion;
+                    mData[index] = updateItem;
+                    if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
+                        mDataListener.onContentChanged(i);
+                    }
+                }
+            }
+            return null;
+        }
+    }
+
+    /*
+     * The thread model of ReloadTask
+     *      *
+     * [Reload Task]       [Main Thread]
+     *       |                   |
+     * getUpdateInfo() -->       |           (synchronous call)
+     *     (wait) <----    getUpdateInfo()
+     *       |                   |
+     *   Load Data               |
+     *       |                   |
+     * updateContent() -->       |           (synchronous call)
+     *     (wait)          updateContent()
+     *       |                   |
+     *       |                   |
+     */
+    private class ReloadTask extends Thread {
+
+        private volatile boolean mActive = true;
+        private volatile boolean mDirty = true;
+        private boolean mIsLoading = false;
+
+        private void updateLoading(boolean loading) {
+            if (mIsLoading == loading) return;
+            mIsLoading = loading;
+            mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+        }
+
+        @Override
+        public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+            boolean updateComplete = false;
+            while (mActive) {
+                synchronized (this) {
+                    if (mActive && !mDirty && updateComplete) {
+                        updateLoading(false);
+                        if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
+                            Log.d(TAG, "reload pause");
+                        }
+                        Utils.waitWithoutInterrupt(this);
+                        if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
+                            Log.d(TAG, "reload resume");
+                        }
+                        continue;
+                    }
+                    mDirty = false;
+                }
+                updateLoading(true);
+                long version = mSource.reload();
+                UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+                updateComplete = info == null;
+                if (updateComplete) continue;
+                if (info.version != version) {
+                    info.size = mSource.getMediaItemCount();
+                    info.version = version;
+                }
+                if (info.reloadCount > 0) {
+                    info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount);
+                }
+                executeAndWait(new UpdateContent(info));
+            }
+            updateLoading(false);
+        }
+
+        public synchronized void notifyDirty() {
+            mDirty = true;
+            notifyAll();
+        }
+
+        public synchronized void terminate() {
+            mActive = false;
+            notifyAll();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
new file mode 100644
index 0000000..658abbb
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.glrenderer.FadeTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumSlotRenderer;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
+import com.android.gallery3d.ui.RelativePosition;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+
+public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
+        SelectionManager.SelectionListener, MediaSet.SyncListener, GalleryActionBar.OnAlbumModeSelectedListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumPage";
+
+    public static final String KEY_MEDIA_PATH = "media-path";
+    public static final String KEY_PARENT_MEDIA_PATH = "parent-media-path";
+    public static final String KEY_SET_CENTER = "set-center";
+    public static final String KEY_AUTO_SELECT_ALL = "auto-select-all";
+    public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu";
+    public static final String KEY_EMPTY_ALBUM = "empty-album";
+    public static final String KEY_RESUME_ANIMATION = "resume_animation";
+
+    private static final int REQUEST_SLIDESHOW = 1;
+    public static final int REQUEST_PHOTO = 2;
+    private static final int REQUEST_DO_ANIMATION = 3;
+
+    private static final int BIT_LOADING_RELOAD = 1;
+    private static final int BIT_LOADING_SYNC = 2;
+
+    private static final float USER_DISTANCE_METER = 0.3f;
+
+    private boolean mIsActive = false;
+    private AlbumSlotRenderer mAlbumView;
+    private Path mMediaSetPath;
+    private String mParentMediaSetString;
+    private SlotView mSlotView;
+
+    private AlbumDataLoader mAlbumDataAdapter;
+
+    protected SelectionManager mSelectionManager;
+
+    private boolean mGetContent;
+    private boolean mShowClusterMenu;
+
+    private ActionModeHandler mActionModeHandler;
+    private int mFocusIndex = 0;
+    private DetailsHelper mDetailsHelper;
+    private MyDetailsSource mDetailsSource;
+    private MediaSet mMediaSet;
+    private boolean mShowDetails;
+    private float mUserDistance; // in pixel
+    private Future<Integer> mSyncTask = null;
+    private boolean mLaunchedFromPhotoPage;
+    private boolean mInCameraApp;
+    private boolean mInCameraAndWantQuitOnPause;
+
+    private int mLoadingBits = 0;
+    private boolean mInitialSynced = false;
+    private int mSyncResult;
+    private boolean mLoadingFailed;
+    private RelativePosition mOpenCenter = new RelativePosition();
+
+    private Handler mHandler;
+    private static final int MSG_PICK_PHOTO = 0;
+
+    private PhotoFallbackEffect mResumeEffect;
+    private PhotoFallbackEffect.PositionProvider mPositionProvider =
+            new PhotoFallbackEffect.PositionProvider() {
+        @Override
+        public Rect getPosition(int index) {
+            Rect rect = mSlotView.getSlotRect(index);
+            Rect bounds = mSlotView.bounds();
+            rect.offset(bounds.left - mSlotView.getScrollX(),
+                    bounds.top - mSlotView.getScrollY());
+            return rect;
+        }
+
+        @Override
+        public int getItemIndex(Path path) {
+            int start = mSlotView.getVisibleStart();
+            int end = mSlotView.getVisibleEnd();
+            for (int i = start; i < end; ++i) {
+                MediaItem item = mAlbumDataAdapter.get(i);
+                if (item != null && item.getPath() == path) return i;
+            }
+            return -1;
+        }
+    };
+
+    @Override
+    protected int getBackgroundColorId() {
+        return R.color.album_background;
+    }
+
+    private final GLView mRootPane = new GLView() {
+        private final float mMatrix[] = new float[16];
+
+        @Override
+        protected void onLayout(
+                boolean changed, int left, int top, int right, int bottom) {
+
+            int slotViewTop = mActivity.getGalleryActionBar().getHeight();
+            int slotViewBottom = bottom - top;
+            int slotViewRight = right - left;
+
+            if (mShowDetails) {
+                mDetailsHelper.layout(left, slotViewTop, right, bottom);
+            } else {
+                mAlbumView.setHighlightItemPath(null);
+            }
+
+            // Set the mSlotView as a reference point to the open animation
+            mOpenCenter.setReferencePosition(0, slotViewTop);
+            mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+            GalleryUtils.setViewPointMatrix(mMatrix,
+                    (right - left) / 2, (bottom - top) / 2, -mUserDistance);
+        }
+
+        @Override
+        protected void render(GLCanvas canvas) {
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            canvas.multiplyMatrix(mMatrix, 0);
+            super.render(canvas);
+
+            if (mResumeEffect != null) {
+                boolean more = mResumeEffect.draw(canvas);
+                if (!more) {
+                    mResumeEffect = null;
+                    mAlbumView.setSlotFilter(null);
+                }
+                // We want to render one more time even when no more effect
+                // required. So that the animated thumbnails could be draw
+                // with declarations in super.render().
+                invalidate();
+            }
+            canvas.restore();
+        }
+    };
+
+    // This are the transitions we want:
+    //
+    // +--------+           +------------+    +-------+    +----------+
+    // | Camera |---------->| Fullscreen |--->| Album |--->| AlbumSet |
+    // |  View  | thumbnail |   Photo    | up | Page  | up |   Page   |
+    // +--------+           +------------+    +-------+    +----------+
+    //     ^                      |               |            ^  |
+    //     |                      |               |            |  |         close
+    //     +----------back--------+               +----back----+  +--back->  app
+    //
+    @Override
+    protected void onBackPressed() {
+        if (mShowDetails) {
+            hideDetails();
+        } else if (mSelectionManager.inSelectionMode()) {
+            mSelectionManager.leaveSelectionMode();
+        } else {
+            if(mLaunchedFromPhotoPage) {
+                mActivity.getTransitionStore().putIfNotPresent(
+                        PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+                        PhotoPage.MSG_ALBUMPAGE_RESUMED);
+            }
+            // TODO: fix this regression
+            // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+            if (mInCameraApp) {
+                super.onBackPressed();
+            } else {
+                onUpPressed();
+            }
+        }
+    }
+
+    private void onUpPressed() {
+        if (mInCameraApp) {
+            GalleryUtils.startGalleryActivity(mActivity);
+        } else if (mActivity.getStateManager().getStateCount() > 1) {
+            super.onBackPressed();
+        } else if (mParentMediaSetString != null) {
+            Bundle data = new Bundle(getData());
+            data.putString(AlbumSetPage.KEY_MEDIA_PATH, mParentMediaSetString);
+            mActivity.getStateManager().switchState(
+                    this, AlbumSetPage.class, data);
+        }
+    }
+
+    private void onDown(int index) {
+        mAlbumView.setPressedIndex(index);
+    }
+
+    private void onUp(boolean followedByLongPress) {
+        if (followedByLongPress) {
+            // Avoid showing press-up animations for long-press.
+            mAlbumView.setPressedIndex(-1);
+        } else {
+            mAlbumView.setPressedUp();
+        }
+    }
+
+    private void onSingleTapUp(int slotIndex) {
+        if (!mIsActive) return;
+
+        if (mSelectionManager.inSelectionMode()) {
+            MediaItem item = mAlbumDataAdapter.get(slotIndex);
+            if (item == null) return; // Item not ready yet, ignore the click
+            mSelectionManager.toggle(item.getPath());
+            mSlotView.invalidate();
+        } else {
+            // Render transition in pressed state
+            mAlbumView.setPressedIndex(slotIndex);
+            mAlbumView.setPressedUp();
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_PHOTO, slotIndex, 0),
+                    FadeTexture.DURATION);
+        }
+    }
+
+    private void pickPhoto(int slotIndex) {
+        pickPhoto(slotIndex, false);
+    }
+
+    private void pickPhoto(int slotIndex, boolean startInFilmstrip) {
+        if (!mIsActive) return;
+
+        if (!startInFilmstrip) {
+            // Launch photos in lights out mode
+            mActivity.getGLRoot().setLightsOutMode(true);
+        }
+
+        MediaItem item = mAlbumDataAdapter.get(slotIndex);
+        if (item == null) return; // Item not ready yet, ignore the click
+        if (mGetContent) {
+            onGetContent(item);
+        } else if (mLaunchedFromPhotoPage) {
+            TransitionStore transitions = mActivity.getTransitionStore();
+            transitions.put(
+                    PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+                    PhotoPage.MSG_ALBUMPAGE_PICKED);
+            transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+            onBackPressed();
+        } else {
+            // Get into the PhotoPage.
+            // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+            Bundle data = new Bundle();
+            data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+            data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+                    mSlotView.getSlotRect(slotIndex, mRootPane));
+            data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+                    mMediaSetPath.toString());
+            data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+                    item.getPath().toString());
+            data.putInt(PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+                    PhotoPage.MSG_ALBUMPAGE_STARTED);
+            data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP,
+                    startInFilmstrip);
+            data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, mMediaSet.isCameraRoll());
+            if (startInFilmstrip) {
+                mActivity.getStateManager().switchState(this, FilmstripPage.class, data);
+            } else {
+                mActivity.getStateManager().startStateForResult(
+                            SinglePhotoPage.class, REQUEST_PHOTO, data);
+            }
+        }
+    }
+
+    private void onGetContent(final MediaItem item) {
+        DataManager dm = mActivity.getDataManager();
+        Activity activity = mActivity;
+        if (mData.getString(Gallery.EXTRA_CROP) != null) {
+            Uri uri = dm.getContentUri(item.getPath());
+            Intent intent = new Intent(CropActivity.CROP_ACTION, uri)
+                    .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+                    .putExtras(getData());
+            if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) {
+                intent.putExtra(CropExtras.KEY_RETURN_DATA, true);
+            }
+            activity.startActivity(intent);
+            activity.finish();
+        } else {
+            Intent intent = new Intent(null, item.getContentUri())
+                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            activity.setResult(Activity.RESULT_OK, intent);
+            activity.finish();
+        }
+    }
+
+    public void onLongTap(int slotIndex) {
+        if (mGetContent) return;
+        MediaItem item = mAlbumDataAdapter.get(slotIndex);
+        if (item == null) return;
+        mSelectionManager.setAutoLeaveSelectionMode(true);
+        mSelectionManager.toggle(item.getPath());
+        mSlotView.invalidate();
+    }
+
+    @Override
+    public void doCluster(int clusterType) {
+        String basePath = mMediaSet.getPath().toString();
+        String newPath = FilterUtils.newClusterPath(basePath, clusterType);
+        Bundle data = new Bundle(getData());
+        data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+        if (mShowClusterMenu) {
+            Context context = mActivity.getAndroidContext();
+            data.putString(AlbumSetPage.KEY_SET_TITLE, mMediaSet.getName());
+            data.putString(AlbumSetPage.KEY_SET_SUBTITLE,
+                    GalleryActionBar.getClusterByTypeString(context, clusterType));
+        }
+
+        // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+        mActivity.getStateManager().startStateForResult(
+                AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
+    }
+
+    @Override
+    protected void onCreate(Bundle data, Bundle restoreState) {
+        super.onCreate(data, restoreState);
+        mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+        initializeViews();
+        initializeData(data);
+        mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false);
+        mShowClusterMenu = data.getBoolean(KEY_SHOW_CLUSTER_MENU, false);
+        mDetailsSource = new MyDetailsSource();
+        Context context = mActivity.getAndroidContext();
+
+        if (data.getBoolean(KEY_AUTO_SELECT_ALL)) {
+            mSelectionManager.selectAll();
+        }
+
+        mLaunchedFromPhotoPage =
+                mActivity.getStateManager().hasStateClass(FilmstripPage.class);
+        mInCameraApp = data.getBoolean(PhotoPage.KEY_APP_BRIDGE, false);
+
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_PICK_PHOTO: {
+                        pickPhoto(message.arg1);
+                        break;
+                    }
+                    default:
+                        throw new AssertionError(message.what);
+                }
+            }
+        };
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mIsActive = true;
+
+        mResumeEffect = mActivity.getTransitionStore().get(KEY_RESUME_ANIMATION);
+        if (mResumeEffect != null) {
+            mAlbumView.setSlotFilter(mResumeEffect);
+            mResumeEffect.setPositionProvider(mPositionProvider);
+            mResumeEffect.start();
+        }
+
+        setContentPane(mRootPane);
+
+        boolean enableHomeButton = (mActivity.getStateManager().getStateCount() > 1) |
+                mParentMediaSetString != null;
+        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+        actionBar.setDisplayOptions(enableHomeButton, false);
+        if (!mGetContent) {
+            actionBar.enableAlbumModeMenu(GalleryActionBar.ALBUM_GRID_MODE_SELECTED, this);
+        }
+
+        // Set the reload bit here to prevent it exit this page in clearLoadingBit().
+        setLoadingBit(BIT_LOADING_RELOAD);
+        mLoadingFailed = false;
+        mAlbumDataAdapter.resume();
+
+        mAlbumView.resume();
+        mAlbumView.setPressedIndex(-1);
+        mActionModeHandler.resume();
+        if (!mInitialSynced) {
+            setLoadingBit(BIT_LOADING_SYNC);
+            mSyncTask = mMediaSet.requestSync(this);
+        }
+        mInCameraAndWantQuitOnPause = mInCameraApp;
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mIsActive = false;
+
+        if (mSelectionManager.inSelectionMode()) {
+            mSelectionManager.leaveSelectionMode();
+        }
+        mAlbumView.setSlotFilter(null);
+        mActionModeHandler.pause();
+        mAlbumDataAdapter.pause();
+        mAlbumView.pause();
+        DetailsHelper.pause();
+        if (!mGetContent) {
+            mActivity.getGalleryActionBar().disableAlbumModeMenu(true);
+        }
+
+        if (mSyncTask != null) {
+            mSyncTask.cancel();
+            mSyncTask = null;
+            clearLoadingBit(BIT_LOADING_SYNC);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mAlbumDataAdapter != null) {
+            mAlbumDataAdapter.setLoadingListener(null);
+        }
+        mActionModeHandler.destroy();
+    }
+
+    private void initializeViews() {
+        mSelectionManager = new SelectionManager(mActivity, false);
+        mSelectionManager.setSelectionListener(this);
+        Config.AlbumPage config = Config.AlbumPage.get(mActivity);
+        mSlotView = new SlotView(mActivity, config.slotViewSpec);
+        mAlbumView = new AlbumSlotRenderer(mActivity, mSlotView,
+                mSelectionManager, config.placeholderColor);
+        mSlotView.setSlotRenderer(mAlbumView);
+        mRootPane.addComponent(mSlotView);
+        mSlotView.setListener(new SlotView.SimpleListener() {
+            @Override
+            public void onDown(int index) {
+                AlbumPage.this.onDown(index);
+            }
+
+            @Override
+            public void onUp(boolean followedByLongPress) {
+                AlbumPage.this.onUp(followedByLongPress);
+            }
+
+            @Override
+            public void onSingleTapUp(int slotIndex) {
+                AlbumPage.this.onSingleTapUp(slotIndex);
+            }
+
+            @Override
+            public void onLongTap(int slotIndex) {
+                AlbumPage.this.onLongTap(slotIndex);
+            }
+        });
+        mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
+        mActionModeHandler.setActionModeListener(new ActionModeListener() {
+            @Override
+            public boolean onActionItemClicked(MenuItem item) {
+                return onItemSelected(item);
+            }
+        });
+    }
+
+    private void initializeData(Bundle data) {
+        mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH));
+        mParentMediaSetString = data.getString(KEY_PARENT_MEDIA_PATH);
+        mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath);
+        if (mMediaSet == null) {
+            Utils.fail("MediaSet is null. Path = %s", mMediaSetPath);
+        }
+        mSelectionManager.setSourceMediaSet(mMediaSet);
+        mAlbumDataAdapter = new AlbumDataLoader(mActivity, mMediaSet);
+        mAlbumDataAdapter.setLoadingListener(new MyLoadingListener());
+        mAlbumView.setModel(mAlbumDataAdapter);
+    }
+
+    private void showDetails() {
+        mShowDetails = true;
+        if (mDetailsHelper == null) {
+            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
+            mDetailsHelper.setCloseListener(new CloseListener() {
+                @Override
+                public void onClose() {
+                    hideDetails();
+                }
+            });
+        }
+        mDetailsHelper.show();
+    }
+
+    private void hideDetails() {
+        mShowDetails = false;
+        mDetailsHelper.hide();
+        mAlbumView.setHighlightItemPath(null);
+        mSlotView.invalidate();
+    }
+
+    @Override
+    protected boolean onCreateActionBar(Menu menu) {
+        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+        MenuInflater inflator = getSupportMenuInflater();
+        if (mGetContent) {
+            inflator.inflate(R.menu.pickup, menu);
+            int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS,
+                    DataManager.INCLUDE_IMAGE);
+            actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+        } else {
+            inflator.inflate(R.menu.album, menu);
+            actionBar.setTitle(mMediaSet.getName());
+
+            FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true);
+
+            menu.findItem(R.id.action_group_by).setVisible(mShowClusterMenu);
+            menu.findItem(R.id.action_camera).setVisible(
+                    MediaSetUtils.isCameraSource(mMediaSetPath)
+                    && GalleryUtils.isCameraAvailable(mActivity));
+
+        }
+        actionBar.setSubtitle(null);
+        return true;
+    }
+
+    private void prepareAnimationBackToFilmstrip(int slotIndex) {
+        if (mAlbumDataAdapter == null || !mAlbumDataAdapter.isActive(slotIndex)) return;
+        MediaItem item = mAlbumDataAdapter.get(slotIndex);
+        if (item == null) return;
+        TransitionStore transitions = mActivity.getTransitionStore();
+        transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+        transitions.put(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+                mSlotView.getSlotRect(slotIndex, mRootPane));
+    }
+
+    private void switchToFilmstrip() {
+        if (mAlbumDataAdapter.size() < 1) return;
+        int targetPhoto = mSlotView.getVisibleStart();
+        prepareAnimationBackToFilmstrip(targetPhoto);
+        if(mLaunchedFromPhotoPage) {
+            onBackPressed();
+        } else {
+            pickPhoto(targetPhoto, true);
+        }
+    }
+
+    @Override
+    protected boolean onItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home: {
+                onUpPressed();
+                return true;
+            }
+            case R.id.action_cancel:
+                mActivity.getStateManager().finishState(this);
+                return true;
+            case R.id.action_select:
+                mSelectionManager.setAutoLeaveSelectionMode(false);
+                mSelectionManager.enterSelectionMode();
+                return true;
+            case R.id.action_group_by: {
+                mActivity.getGalleryActionBar().showClusterDialog(this);
+                return true;
+            }
+            case R.id.action_slideshow: {
+                mInCameraAndWantQuitOnPause = false;
+                Bundle data = new Bundle();
+                data.putString(SlideshowPage.KEY_SET_PATH,
+                        mMediaSetPath.toString());
+                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+                mActivity.getStateManager().startStateForResult(
+                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
+                return true;
+            }
+            case R.id.action_details: {
+                if (mShowDetails) {
+                    hideDetails();
+                } else {
+                    showDetails();
+                }
+                return true;
+            }
+            case R.id.action_camera: {
+                GalleryUtils.startCameraActivity(mActivity);
+                return true;
+            }
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    protected void onStateResult(int request, int result, Intent data) {
+        switch (request) {
+            case REQUEST_SLIDESHOW: {
+                // data could be null, if there is no images in the album
+                if (data == null) return;
+                mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
+                mSlotView.setCenterIndex(mFocusIndex);
+                break;
+            }
+            case REQUEST_PHOTO: {
+                if (data == null) return;
+                mFocusIndex = data.getIntExtra(PhotoPage.KEY_RETURN_INDEX_HINT, 0);
+                mSlotView.makeSlotVisible(mFocusIndex);
+                break;
+            }
+            case REQUEST_DO_ANIMATION: {
+                mSlotView.startRisingAnimation();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onSelectionModeChange(int mode) {
+        switch (mode) {
+            case SelectionManager.ENTER_SELECTION_MODE: {
+                mActionModeHandler.startActionMode();
+                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                break;
+            }
+            case SelectionManager.LEAVE_SELECTION_MODE: {
+                mActionModeHandler.finishActionMode();
+                mRootPane.invalidate();
+                break;
+            }
+            case SelectionManager.SELECT_ALL_MODE: {
+                mActionModeHandler.updateSupportedOperation();
+                mRootPane.invalidate();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onSelectionChange(Path path, boolean selected) {
+        int count = mSelectionManager.getSelectedCount();
+        String format = mActivity.getResources().getQuantityString(
+                R.plurals.number_of_items_selected, count);
+        mActionModeHandler.setTitle(String.format(format, count));
+        mActionModeHandler.updateSupportedOperation(path, selected);
+    }
+
+    @Override
+    public void onSyncDone(final MediaSet mediaSet, final int resultCode) {
+        Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result="
+                + resultCode);
+        ((Activity) mActivity).runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                GLRoot root = mActivity.getGLRoot();
+                root.lockRenderThread();
+                mSyncResult = resultCode;
+                try {
+                    if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+                        mInitialSynced = true;
+                    }
+                    clearLoadingBit(BIT_LOADING_SYNC);
+                    showSyncErrorIfNecessary(mLoadingFailed);
+                } finally {
+                    root.unlockRenderThread();
+                }
+            }
+        });
+    }
+
+    // Show sync error toast when all the following conditions are met:
+    // (1) both loading and sync are done,
+    // (2) sync result is error,
+    // (3) the page is still active, and
+    // (4) no photo is shown or loading fails.
+    private void showSyncErrorIfNecessary(boolean loadingFailed) {
+        if ((mLoadingBits == 0) && (mSyncResult == MediaSet.SYNC_RESULT_ERROR) && mIsActive
+                && (loadingFailed || (mAlbumDataAdapter.size() == 0))) {
+            Toast.makeText(mActivity, R.string.sync_album_error,
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+
+    private void setLoadingBit(int loadTaskBit) {
+        mLoadingBits |= loadTaskBit;
+    }
+
+    private void clearLoadingBit(int loadTaskBit) {
+        mLoadingBits &= ~loadTaskBit;
+        if (mLoadingBits == 0 && mIsActive) {
+            if (mAlbumDataAdapter.size() == 0) {
+                Intent result = new Intent();
+                result.putExtra(KEY_EMPTY_ALBUM, true);
+                setStateResult(Activity.RESULT_OK, result);
+                mActivity.getStateManager().finishState(this);
+            }
+        }
+    }
+
+    private class MyLoadingListener implements LoadingListener {
+        @Override
+        public void onLoadingStarted() {
+            setLoadingBit(BIT_LOADING_RELOAD);
+            mLoadingFailed = false;
+        }
+
+        @Override
+        public void onLoadingFinished(boolean loadingFailed) {
+            clearLoadingBit(BIT_LOADING_RELOAD);
+            mLoadingFailed = loadingFailed;
+            showSyncErrorIfNecessary(loadingFailed);
+        }
+    }
+
+    private class MyDetailsSource implements DetailsHelper.DetailsSource {
+        private int mIndex;
+
+        @Override
+        public int size() {
+            return mAlbumDataAdapter.size();
+        }
+
+        @Override
+        public int setIndex() {
+            Path id = mSelectionManager.getSelected(false).get(0);
+            mIndex = mAlbumDataAdapter.findItem(id);
+            return mIndex;
+        }
+
+        @Override
+        public MediaDetails getDetails() {
+            // this relies on setIndex() being called beforehand
+            MediaObject item = mAlbumDataAdapter.get(mIndex);
+            if (item != null) {
+                mAlbumView.setHighlightItemPath(item.getPath());
+                return item.getDetails();
+            } else {
+                return null;
+            }
+        }
+    }
+
+    @Override
+    public void onAlbumModeSelected(int mode) {
+        if (mode == GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED) {
+            switchToFilmstrip();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java
new file mode 100644
index 0000000..65eb772
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPicker.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+
+public class AlbumPicker extends PickerActivity {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setTitle(R.string.select_album);
+        Intent intent = getIntent();
+        Bundle extras = intent.getExtras();
+        Bundle data = extras == null ? new Bundle() : new Bundle(extras);
+
+        data.putBoolean(Gallery.KEY_GET_ALBUM, true);
+        data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+                getDataManager().getTopSetPath(DataManager.INCLUDE_IMAGE));
+        getStateManager().startState(AlbumSetPage.class, data);
+    }
+}
diff --git a/src/com/android/gallery3d/app/AlbumSetDataLoader.java b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
new file mode 100644
index 0000000..cf380f8
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class AlbumSetDataLoader {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumSetDataAdapter";
+
+    private static final int INDEX_NONE = -1;
+
+    private static final int MIN_LOAD_COUNT = 4;
+
+    private static final int MSG_LOAD_START = 1;
+    private static final int MSG_LOAD_FINISH = 2;
+    private static final int MSG_RUN_OBJECT = 3;
+
+    public static interface DataListener {
+        public void onContentChanged(int index);
+        public void onSizeChanged(int size);
+    }
+
+    private final MediaSet[] mData;
+    private final MediaItem[] mCoverItem;
+    private final int[] mTotalCount;
+    private final long[] mItemVersion;
+    private final long[] mSetVersion;
+
+    private int mActiveStart = 0;
+    private int mActiveEnd = 0;
+
+    private int mContentStart = 0;
+    private int mContentEnd = 0;
+
+    private final MediaSet mSource;
+    private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+    private int mSize;
+
+    private DataListener mDataListener;
+    private LoadingListener mLoadingListener;
+    private ReloadTask mReloadTask;
+
+    private final Handler mMainHandler;
+
+    private final MySourceListener mSourceListener = new MySourceListener();
+
+    public AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize) {
+        mSource = Utils.checkNotNull(albumSet);
+        mCoverItem = new MediaItem[cacheSize];
+        mData = new MediaSet[cacheSize];
+        mTotalCount = new int[cacheSize];
+        mItemVersion = new long[cacheSize];
+        mSetVersion = new long[cacheSize];
+        Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
+        Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
+
+        mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_RUN_OBJECT:
+                        ((Runnable) message.obj).run();
+                        return;
+                    case MSG_LOAD_START:
+                        if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
+                        return;
+                    case MSG_LOAD_FINISH:
+                        if (mLoadingListener != null) mLoadingListener.onLoadingFinished(false);
+                        return;
+                }
+            }
+        };
+    }
+
+    public void pause() {
+        mReloadTask.terminate();
+        mReloadTask = null;
+        mSource.removeContentListener(mSourceListener);
+    }
+
+    public void resume() {
+        mSource.addContentListener(mSourceListener);
+        mReloadTask = new ReloadTask();
+        mReloadTask.start();
+    }
+
+    private void assertIsActive(int index) {
+        if (index < mActiveStart && index >= mActiveEnd) {
+            throw new IllegalArgumentException(String.format(
+                    "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+        }
+    }
+
+    public MediaSet getMediaSet(int index) {
+        assertIsActive(index);
+        return mData[index % mData.length];
+    }
+
+    public MediaItem getCoverItem(int index) {
+        assertIsActive(index);
+        return mCoverItem[index % mCoverItem.length];
+    }
+
+    public int getTotalCount(int index) {
+        assertIsActive(index);
+        return mTotalCount[index % mTotalCount.length];
+    }
+
+    public int getActiveStart() {
+        return mActiveStart;
+    }
+
+    public boolean isActive(int index) {
+        return index >= mActiveStart && index < mActiveEnd;
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    // Returns the index of the MediaSet with the given path or
+    // -1 if the path is not cached
+    public int findSet(Path id) {
+        int length = mData.length;
+        for (int i = mContentStart; i < mContentEnd; i++) {
+            MediaSet set = mData[i % length];
+            if (set != null && id == set.getPath()) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private void clearSlot(int slotIndex) {
+        mData[slotIndex] = null;
+        mCoverItem[slotIndex] = null;
+        mTotalCount[slotIndex] = 0;
+        mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+        mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+    }
+
+    private void setContentWindow(int contentStart, int contentEnd) {
+        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+        int length = mCoverItem.length;
+
+        int start = this.mContentStart;
+        int end = this.mContentEnd;
+
+        mContentStart = contentStart;
+        mContentEnd = contentEnd;
+
+        if (contentStart >= end || start >= contentEnd) {
+            for (int i = start, n = end; i < n; ++i) {
+                clearSlot(i % length);
+            }
+        } else {
+            for (int i = start; i < contentStart; ++i) {
+                clearSlot(i % length);
+            }
+            for (int i = contentEnd, n = end; i < n; ++i) {
+                clearSlot(i % length);
+            }
+        }
+        mReloadTask.notifyDirty();
+    }
+
+    public void setActiveWindow(int start, int end) {
+        if (start == mActiveStart && end == mActiveEnd) return;
+
+        Utils.assertTrue(start <= end
+                && end - start <= mCoverItem.length && end <= mSize);
+
+        mActiveStart = start;
+        mActiveEnd = end;
+
+        int length = mCoverItem.length;
+        // If no data is visible, keep the cache content
+        if (start == end) return;
+
+        int contentStart = Utils.clamp((start + end) / 2 - length / 2,
+                0, Math.max(0, mSize - length));
+        int contentEnd = Math.min(contentStart + length, mSize);
+        if (mContentStart > start || mContentEnd < end
+                || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
+            setContentWindow(contentStart, contentEnd);
+        }
+    }
+
+    private class MySourceListener implements ContentListener {
+        @Override
+        public void onContentDirty() {
+            mReloadTask.notifyDirty();
+        }
+    }
+
+    public void setModelListener(DataListener listener) {
+        mDataListener = listener;
+    }
+
+    public void setLoadingListener(LoadingListener listener) {
+        mLoadingListener = listener;
+    }
+
+    private static class UpdateInfo {
+        public long version;
+        public int index;
+
+        public int size;
+        public MediaSet item;
+        public MediaItem cover;
+        public int totalCount;
+    }
+
+    private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+        private final long mVersion;
+
+        public GetUpdateInfo(long version) {
+            mVersion = version;
+        }
+
+        private int getInvalidIndex(long version) {
+            long setVersion[] = mSetVersion;
+            int length = setVersion.length;
+            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+                int index = i % length;
+                if (setVersion[i % length] != version) return i;
+            }
+            return INDEX_NONE;
+        }
+
+        @Override
+        public UpdateInfo call() throws Exception {
+            int index = getInvalidIndex(mVersion);
+            if (index == INDEX_NONE && mSourceVersion == mVersion) return null;
+            UpdateInfo info = new UpdateInfo();
+            info.version = mSourceVersion;
+            info.index = index;
+            info.size = mSize;
+            return info;
+        }
+    }
+
+    private class UpdateContent implements Callable<Void> {
+        private final UpdateInfo mUpdateInfo;
+
+        public UpdateContent(UpdateInfo info) {
+            mUpdateInfo = info;
+        }
+
+        @Override
+        public Void call() {
+            // Avoid notifying listeners of status change after pause
+            // Otherwise gallery will be in inconsistent state after resume.
+            if (mReloadTask == null) return null;
+            UpdateInfo info = mUpdateInfo;
+            mSourceVersion = info.version;
+            if (mSize != info.size) {
+                mSize = info.size;
+                if (mDataListener != null) mDataListener.onSizeChanged(mSize);
+                if (mContentEnd > mSize) mContentEnd = mSize;
+                if (mActiveEnd > mSize) mActiveEnd = mSize;
+            }
+            // Note: info.index could be INDEX_NONE, i.e., -1
+            if (info.index >= mContentStart && info.index < mContentEnd) {
+                int pos = info.index % mCoverItem.length;
+                mSetVersion[pos] = info.version;
+                long itemVersion = info.item.getDataVersion();
+                if (mItemVersion[pos] == itemVersion) return null;
+                mItemVersion[pos] = itemVersion;
+                mData[pos] = info.item;
+                mCoverItem[pos] = info.cover;
+                mTotalCount[pos] = info.totalCount;
+                if (mDataListener != null
+                        && info.index >= mActiveStart && info.index < mActiveEnd) {
+                    mDataListener.onContentChanged(info.index);
+                }
+            }
+            return null;
+        }
+    }
+
+    private <T> T executeAndWait(Callable<T> callable) {
+        FutureTask<T> task = new FutureTask<T>(callable);
+        mMainHandler.sendMessage(
+                mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+        try {
+            return task.get();
+        } catch (InterruptedException e) {
+            return null;
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // TODO: load active range first
+    private class ReloadTask extends Thread {
+        private volatile boolean mActive = true;
+        private volatile boolean mDirty = true;
+        private volatile boolean mIsLoading = false;
+
+        private void updateLoading(boolean loading) {
+            if (mIsLoading == loading) return;
+            mIsLoading = loading;
+            mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+        }
+
+        @Override
+        public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+            boolean updateComplete = false;
+            while (mActive) {
+                synchronized (this) {
+                    if (mActive && !mDirty && updateComplete) {
+                        if (!mSource.isLoading()) updateLoading(false);
+                        Utils.waitWithoutInterrupt(this);
+                        continue;
+                    }
+                }
+                mDirty = false;
+                updateLoading(true);
+
+                long version = mSource.reload();
+                UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+                updateComplete = info == null;
+                if (updateComplete) continue;
+                if (info.version != version) {
+                    info.version = version;
+                    info.size = mSource.getSubMediaSetCount();
+
+                    // If the size becomes smaller after reload(), we may
+                    // receive from GetUpdateInfo an index which is too
+                    // big. Because the main thread is not aware of the size
+                    // change until we call UpdateContent.
+                    if (info.index >= info.size) {
+                        info.index = INDEX_NONE;
+                    }
+                }
+                if (info.index != INDEX_NONE) {
+                    info.item = mSource.getSubMediaSet(info.index);
+                    if (info.item == null) continue;
+                    info.cover = info.item.getCoverMediaItem();
+                    info.totalCount = info.item.getTotalMediaItemCount();
+                }
+                executeAndWait(new UpdateContent(info));
+            }
+            updateLoading(false);
+        }
+
+        public synchronized void notifyDirty() {
+            mDirty = true;
+            notifyAll();
+        }
+
+        public synchronized void terminate() {
+            mActive = false;
+            notifyAll();
+        }
+    }
+}
+
+
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
new file mode 100644
index 0000000..dd9d8ec
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -0,0 +1,764 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.FadeTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.settings.GallerySettings;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumSetSlotRenderer;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.HelpUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class AlbumSetPage extends ActivityState implements
+        SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
+        EyePosition.EyePositionListener, MediaSet.SyncListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumSetPage";
+
+    private static final int MSG_PICK_ALBUM = 1;
+
+    public static final String KEY_MEDIA_PATH = "media-path";
+    public static final String KEY_SET_TITLE = "set-title";
+    public static final String KEY_SET_SUBTITLE = "set-subtitle";
+    public static final String KEY_SELECTED_CLUSTER_TYPE = "selected-cluster";
+
+    private static final int DATA_CACHE_SIZE = 256;
+    private static final int REQUEST_DO_ANIMATION = 1;
+
+    private static final int BIT_LOADING_RELOAD = 1;
+    private static final int BIT_LOADING_SYNC = 2;
+
+    private boolean mIsActive = false;
+    private SlotView mSlotView;
+    private AlbumSetSlotRenderer mAlbumSetView;
+    private Config.AlbumSetPage mConfig;
+
+    private MediaSet mMediaSet;
+    private String mTitle;
+    private String mSubtitle;
+    private boolean mShowClusterMenu;
+    private GalleryActionBar mActionBar;
+    private int mSelectedAction;
+
+    protected SelectionManager mSelectionManager;
+    private AlbumSetDataLoader mAlbumSetDataAdapter;
+
+    private boolean mGetContent;
+    private boolean mGetAlbum;
+    private ActionModeHandler mActionModeHandler;
+    private DetailsHelper mDetailsHelper;
+    private MyDetailsSource mDetailsSource;
+    private boolean mShowDetails;
+    private EyePosition mEyePosition;
+    private Handler mHandler;
+
+    // The eyes' position of the user, the origin is at the center of the
+    // device and the unit is in pixels.
+    private float mX;
+    private float mY;
+    private float mZ;
+
+    private Future<Integer> mSyncTask = null;
+
+    private int mLoadingBits = 0;
+    private boolean mInitialSynced = false;
+
+    private Button mCameraButton;
+    private boolean mShowedEmptyToastForSelf = false;
+
+    @Override
+    protected int getBackgroundColorId() {
+        return R.color.albumset_background;
+    }
+
+    private final GLView mRootPane = new GLView() {
+        private final float mMatrix[] = new float[16];
+
+        @Override
+        protected void onLayout(
+                boolean changed, int left, int top, int right, int bottom) {
+            mEyePosition.resetPosition();
+
+            int slotViewTop = mActionBar.getHeight() + mConfig.paddingTop;
+            int slotViewBottom = bottom - top - mConfig.paddingBottom;
+            int slotViewRight = right - left;
+
+            if (mShowDetails) {
+                mDetailsHelper.layout(left, slotViewTop, right, bottom);
+            } else {
+                mAlbumSetView.setHighlightItemPath(null);
+            }
+
+            mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+        }
+
+        @Override
+        protected void render(GLCanvas canvas) {
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            GalleryUtils.setViewPointMatrix(mMatrix,
+                    getWidth() / 2 + mX, getHeight() / 2 + mY, mZ);
+            canvas.multiplyMatrix(mMatrix, 0);
+            super.render(canvas);
+            canvas.restore();
+        }
+    };
+
+    @Override
+    public void onEyePositionChanged(float x, float y, float z) {
+        mRootPane.lockRendering();
+        mX = x;
+        mY = y;
+        mZ = z;
+        mRootPane.unlockRendering();
+        mRootPane.invalidate();
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mShowDetails) {
+            hideDetails();
+        } else if (mSelectionManager.inSelectionMode()) {
+            mSelectionManager.leaveSelectionMode();
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    private void getSlotCenter(int slotIndex, int center[]) {
+        Rect offset = new Rect();
+        mRootPane.getBoundsOf(mSlotView, offset);
+        Rect r = mSlotView.getSlotRect(slotIndex);
+        int scrollX = mSlotView.getScrollX();
+        int scrollY = mSlotView.getScrollY();
+        center[0] = offset.left + (r.left + r.right) / 2 - scrollX;
+        center[1] = offset.top + (r.top + r.bottom) / 2 - scrollY;
+    }
+
+    public void onSingleTapUp(int slotIndex) {
+        if (!mIsActive) return;
+
+        if (mSelectionManager.inSelectionMode()) {
+            MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+            if (targetSet == null) return; // Content is dirty, we shall reload soon
+            mSelectionManager.toggle(targetSet.getPath());
+            mSlotView.invalidate();
+        } else {
+            // Show pressed-up animation for the single-tap.
+            mAlbumSetView.setPressedIndex(slotIndex);
+            mAlbumSetView.setPressedUp();
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_ALBUM, slotIndex, 0),
+                    FadeTexture.DURATION);
+        }
+    }
+
+    private static boolean albumShouldOpenInFilmstrip(MediaSet album) {
+        int itemCount = album.getMediaItemCount();
+        ArrayList<MediaItem> list = (itemCount == 1) ? album.getMediaItem(0, 1) : null;
+        // open in film strip only if there's one item in the album and the item exists
+        return (list != null && !list.isEmpty());
+    }
+
+    WeakReference<Toast> mEmptyAlbumToast = null;
+
+    private void showEmptyAlbumToast(int toastLength) {
+        Toast toast;
+        if (mEmptyAlbumToast != null) {
+            toast = mEmptyAlbumToast.get();
+            if (toast != null) {
+                toast.show();
+                return;
+            }
+        }
+        toast = Toast.makeText(mActivity, R.string.empty_album, toastLength);
+        mEmptyAlbumToast = new WeakReference<Toast>(toast);
+        toast.show();
+    }
+
+    private void hideEmptyAlbumToast() {
+        if (mEmptyAlbumToast != null) {
+            Toast toast = mEmptyAlbumToast.get();
+            if (toast != null) toast.cancel();
+        }
+    }
+
+    private void pickAlbum(int slotIndex) {
+        if (!mIsActive) return;
+
+        MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+        if (targetSet == null) return; // Content is dirty, we shall reload soon
+        if (targetSet.getTotalMediaItemCount() == 0) {
+            showEmptyAlbumToast(Toast.LENGTH_SHORT);
+            return;
+        }
+        hideEmptyAlbumToast();
+
+        String mediaPath = targetSet.getPath().toString();
+
+        Bundle data = new Bundle(getData());
+        int[] center = new int[2];
+        getSlotCenter(slotIndex, center);
+        data.putIntArray(AlbumPage.KEY_SET_CENTER, center);
+        if (mGetAlbum && targetSet.isLeafAlbum()) {
+            Activity activity = mActivity;
+            Intent result = new Intent()
+                    .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString());
+            activity.setResult(Activity.RESULT_OK, result);
+            activity.finish();
+        } else if (targetSet.getSubMediaSetCount() > 0) {
+            data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
+            mActivity.getStateManager().startStateForResult(
+                    AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
+        } else {
+            if (!mGetContent && albumShouldOpenInFilmstrip(targetSet)) {
+                data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+                        mSlotView.getSlotRect(slotIndex, mRootPane));
+                data.putInt(PhotoPage.KEY_INDEX_HINT, 0);
+                data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+                        mediaPath);
+                data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP, true);
+                data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, targetSet.isCameraRoll());
+                mActivity.getStateManager().startStateForResult(
+                        FilmstripPage.class, AlbumPage.REQUEST_PHOTO, data);
+                return;
+            }
+            data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath);
+
+            // We only show cluster menu in the first AlbumPage in stack
+            boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+            data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum);
+            mActivity.getStateManager().startStateForResult(
+                    AlbumPage.class, REQUEST_DO_ANIMATION, data);
+        }
+    }
+
+    private void onDown(int index) {
+        mAlbumSetView.setPressedIndex(index);
+    }
+
+    private void onUp(boolean followedByLongPress) {
+        if (followedByLongPress) {
+            // Avoid showing press-up animations for long-press.
+            mAlbumSetView.setPressedIndex(-1);
+        } else {
+            mAlbumSetView.setPressedUp();
+        }
+    }
+
+    public void onLongTap(int slotIndex) {
+        if (mGetContent || mGetAlbum) return;
+        MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+        if (set == null) return;
+        mSelectionManager.setAutoLeaveSelectionMode(true);
+        mSelectionManager.toggle(set.getPath());
+        mSlotView.invalidate();
+    }
+
+    @Override
+    public void doCluster(int clusterType) {
+        String basePath = mMediaSet.getPath().toString();
+        String newPath = FilterUtils.switchClusterPath(basePath, clusterType);
+        Bundle data = new Bundle(getData());
+        data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+        data.putInt(KEY_SELECTED_CLUSTER_TYPE, clusterType);
+        mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+    }
+
+    @Override
+    public void onCreate(Bundle data, Bundle restoreState) {
+        super.onCreate(data, restoreState);
+        initializeViews();
+        initializeData(data);
+        Context context = mActivity.getAndroidContext();
+        mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false);
+        mGetAlbum = data.getBoolean(Gallery.KEY_GET_ALBUM, false);
+        mTitle = data.getString(AlbumSetPage.KEY_SET_TITLE);
+        mSubtitle = data.getString(AlbumSetPage.KEY_SET_SUBTITLE);
+        mEyePosition = new EyePosition(context, this);
+        mDetailsSource = new MyDetailsSource();
+        mActionBar = mActivity.getGalleryActionBar();
+        mSelectedAction = data.getInt(AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE,
+                FilterUtils.CLUSTER_BY_ALBUM);
+
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_PICK_ALBUM: {
+                        pickAlbum(message.arg1);
+                        break;
+                    }
+                    default: throw new AssertionError(message.what);
+                }
+            }
+        };
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        cleanupCameraButton();
+        mActionModeHandler.destroy();
+    }
+
+    private boolean setupCameraButton() {
+        if (!GalleryUtils.isCameraAvailable(mActivity)) return false;
+        RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+                .findViewById(R.id.gallery_root);
+        if (galleryRoot == null) return false;
+
+        mCameraButton = new Button(mActivity);
+        mCameraButton.setText(R.string.camera_label);
+        mCameraButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.frame_overlay_gallery_camera, 0, 0);
+        mCameraButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                GalleryUtils.startCameraActivity(mActivity);
+            }
+        });
+        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+                RelativeLayout.LayoutParams.WRAP_CONTENT,
+                RelativeLayout.LayoutParams.WRAP_CONTENT);
+        lp.addRule(RelativeLayout.CENTER_IN_PARENT);
+        galleryRoot.addView(mCameraButton, lp);
+        return true;
+    }
+
+    private void cleanupCameraButton() {
+        if (mCameraButton == null) return;
+        RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+                .findViewById(R.id.gallery_root);
+        if (galleryRoot == null) return;
+        galleryRoot.removeView(mCameraButton);
+        mCameraButton = null;
+    }
+
+    private void showCameraButton() {
+        if (mCameraButton == null && !setupCameraButton()) return;
+        mCameraButton.setVisibility(View.VISIBLE);
+    }
+
+    private void hideCameraButton() {
+        if (mCameraButton == null) return;
+        mCameraButton.setVisibility(View.GONE);
+    }
+
+    private void clearLoadingBit(int loadingBit) {
+        mLoadingBits &= ~loadingBit;
+        if (mLoadingBits == 0 && mIsActive) {
+            if (mAlbumSetDataAdapter.size() == 0) {
+                // If this is not the top of the gallery folder hierarchy,
+                // tell the parent AlbumSetPage instance to handle displaying
+                // the empty album toast, otherwise show it within this
+                // instance
+                if (mActivity.getStateManager().getStateCount() > 1) {
+                    Intent result = new Intent();
+                    result.putExtra(AlbumPage.KEY_EMPTY_ALBUM, true);
+                    setStateResult(Activity.RESULT_OK, result);
+                    mActivity.getStateManager().finishState(this);
+                } else {
+                    mShowedEmptyToastForSelf = true;
+                    showEmptyAlbumToast(Toast.LENGTH_LONG);
+                    mSlotView.invalidate();
+                    showCameraButton();
+                }
+                return;
+            }
+        }
+        // Hide the empty album toast if we are in the root instance of
+        // AlbumSetPage and the album is no longer empty (for instance,
+        // after a sync is completed and web albums have been synced)
+        if (mShowedEmptyToastForSelf) {
+            mShowedEmptyToastForSelf = false;
+            hideEmptyAlbumToast();
+            hideCameraButton();
+        }
+    }
+
+    private void setLoadingBit(int loadingBit) {
+        mLoadingBits |= loadingBit;
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mIsActive = false;
+        mAlbumSetDataAdapter.pause();
+        mAlbumSetView.pause();
+        mActionModeHandler.pause();
+        mEyePosition.pause();
+        DetailsHelper.pause();
+        // Call disableClusterMenu to avoid receiving callback after paused.
+        // Don't hide menu here otherwise the list menu will disappear earlier than
+        // the action bar, which is janky and unwanted behavior.
+        mActionBar.disableClusterMenu(false);
+        if (mSyncTask != null) {
+            mSyncTask.cancel();
+            mSyncTask = null;
+            clearLoadingBit(BIT_LOADING_SYNC);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mIsActive = true;
+        setContentPane(mRootPane);
+
+        // Set the reload bit here to prevent it exit this page in clearLoadingBit().
+        setLoadingBit(BIT_LOADING_RELOAD);
+        mAlbumSetDataAdapter.resume();
+
+        mAlbumSetView.resume();
+        mEyePosition.resume();
+        mActionModeHandler.resume();
+        if (mShowClusterMenu) {
+            mActionBar.enableClusterMenu(mSelectedAction, this);
+        }
+        if (!mInitialSynced) {
+            setLoadingBit(BIT_LOADING_SYNC);
+            mSyncTask = mMediaSet.requestSync(AlbumSetPage.this);
+        }
+    }
+
+    private void initializeData(Bundle data) {
+        String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);
+        mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+        mSelectionManager.setSourceMediaSet(mMediaSet);
+        mAlbumSetDataAdapter = new AlbumSetDataLoader(
+                mActivity, mMediaSet, DATA_CACHE_SIZE);
+        mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener());
+        mAlbumSetView.setModel(mAlbumSetDataAdapter);
+    }
+
+    private void initializeViews() {
+        mSelectionManager = new SelectionManager(mActivity, true);
+        mSelectionManager.setSelectionListener(this);
+
+        mConfig = Config.AlbumSetPage.get(mActivity);
+        mSlotView = new SlotView(mActivity, mConfig.slotViewSpec);
+        mAlbumSetView = new AlbumSetSlotRenderer(
+                mActivity, mSelectionManager, mSlotView, mConfig.labelSpec,
+                mConfig.placeholderColor);
+        mSlotView.setSlotRenderer(mAlbumSetView);
+        mSlotView.setListener(new SlotView.SimpleListener() {
+            @Override
+            public void onDown(int index) {
+                AlbumSetPage.this.onDown(index);
+            }
+
+            @Override
+            public void onUp(boolean followedByLongPress) {
+                AlbumSetPage.this.onUp(followedByLongPress);
+            }
+
+            @Override
+            public void onSingleTapUp(int slotIndex) {
+                AlbumSetPage.this.onSingleTapUp(slotIndex);
+            }
+
+            @Override
+            public void onLongTap(int slotIndex) {
+                AlbumSetPage.this.onLongTap(slotIndex);
+            }
+        });
+
+        mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
+        mActionModeHandler.setActionModeListener(new ActionModeListener() {
+            @Override
+            public boolean onActionItemClicked(MenuItem item) {
+                return onItemSelected(item);
+            }
+        });
+        mRootPane.addComponent(mSlotView);
+    }
+
+    @Override
+    protected boolean onCreateActionBar(Menu menu) {
+        Activity activity = mActivity;
+        final boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+        MenuInflater inflater = getSupportMenuInflater();
+
+        if (mGetContent) {
+            inflater.inflate(R.menu.pickup, menu);
+            int typeBits = mData.getInt(
+                    Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE);
+            mActionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+        } else  if (mGetAlbum) {
+            inflater.inflate(R.menu.pickup, menu);
+            mActionBar.setTitle(R.string.select_album);
+        } else {
+            inflater.inflate(R.menu.albumset, menu);
+            boolean wasShowingClusterMenu = mShowClusterMenu;
+            mShowClusterMenu = !inAlbum;
+            boolean selectAlbums = !inAlbum &&
+                    mActionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM;
+            MenuItem selectItem = menu.findItem(R.id.action_select);
+            selectItem.setTitle(activity.getString(
+                    selectAlbums ? R.string.select_album : R.string.select_group));
+
+            MenuItem cameraItem = menu.findItem(R.id.action_camera);
+            cameraItem.setVisible(GalleryUtils.isCameraAvailable(activity));
+
+            FilterUtils.setupMenuItems(mActionBar, mMediaSet.getPath(), false);
+
+            Intent helpIntent = HelpUtils.getHelpIntent(activity);
+
+            MenuItem helpItem = menu.findItem(R.id.action_general_help);
+            helpItem.setVisible(helpIntent != null);
+            if (helpIntent != null) helpItem.setIntent(helpIntent);
+
+            mActionBar.setTitle(mTitle);
+            mActionBar.setSubtitle(mSubtitle);
+            if (mShowClusterMenu != wasShowingClusterMenu) {
+                if (mShowClusterMenu) {
+                    mActionBar.enableClusterMenu(mSelectedAction, this);
+                } else {
+                    mActionBar.disableClusterMenu(true);
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    protected boolean onItemSelected(MenuItem item) {
+        Activity activity = mActivity;
+        switch (item.getItemId()) {
+            case R.id.action_cancel:
+                activity.setResult(Activity.RESULT_CANCELED);
+                activity.finish();
+                return true;
+            case R.id.action_select:
+                mSelectionManager.setAutoLeaveSelectionMode(false);
+                mSelectionManager.enterSelectionMode();
+                return true;
+            case R.id.action_details:
+                if (mAlbumSetDataAdapter.size() != 0) {
+                    if (mShowDetails) {
+                        hideDetails();
+                    } else {
+                        showDetails();
+                    }
+                } else {
+                    Toast.makeText(activity,
+                            activity.getText(R.string.no_albums_alert),
+                            Toast.LENGTH_SHORT).show();
+                }
+                return true;
+            case R.id.action_camera: {
+                GalleryUtils.startCameraActivity(activity);
+                return true;
+            }
+            case R.id.action_manage_offline: {
+                Bundle data = new Bundle();
+                String mediaPath = mActivity.getDataManager().getTopSetPath(
+                    DataManager.INCLUDE_ALL);
+                data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
+                mActivity.getStateManager().startState(ManageCachePage.class, data);
+                return true;
+            }
+            case R.id.action_sync_picasa_albums: {
+                PicasaSource.requestSync(activity);
+                return true;
+            }
+            case R.id.action_settings: {
+                activity.startActivity(new Intent(activity, GallerySettings.class));
+                return true;
+            }
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    protected void onStateResult(int requestCode, int resultCode, Intent data) {
+        if (data != null && data.getBooleanExtra(AlbumPage.KEY_EMPTY_ALBUM, false)) {
+            showEmptyAlbumToast(Toast.LENGTH_SHORT);
+        }
+        switch (requestCode) {
+            case REQUEST_DO_ANIMATION: {
+                mSlotView.startRisingAnimation();
+            }
+        }
+    }
+
+    private String getSelectedString() {
+        int count = mSelectionManager.getSelectedCount();
+        int action = mActionBar.getClusterTypeAction();
+        int string = action == FilterUtils.CLUSTER_BY_ALBUM
+                ? R.plurals.number_of_albums_selected
+                : R.plurals.number_of_groups_selected;
+        String format = mActivity.getResources().getQuantityString(string, count);
+        return String.format(format, count);
+    }
+
+    @Override
+    public void onSelectionModeChange(int mode) {
+        switch (mode) {
+            case SelectionManager.ENTER_SELECTION_MODE: {
+                mActionBar.disableClusterMenu(true);
+                mActionModeHandler.startActionMode();
+                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                break;
+            }
+            case SelectionManager.LEAVE_SELECTION_MODE: {
+                mActionModeHandler.finishActionMode();
+                if (mShowClusterMenu) {
+                    mActionBar.enableClusterMenu(mSelectedAction, this);
+                }
+                mRootPane.invalidate();
+                break;
+            }
+            case SelectionManager.SELECT_ALL_MODE: {
+                mActionModeHandler.updateSupportedOperation();
+                mRootPane.invalidate();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onSelectionChange(Path path, boolean selected) {
+        mActionModeHandler.setTitle(getSelectedString());
+        mActionModeHandler.updateSupportedOperation(path, selected);
+    }
+
+    private void hideDetails() {
+        mShowDetails = false;
+        mDetailsHelper.hide();
+        mAlbumSetView.setHighlightItemPath(null);
+        mSlotView.invalidate();
+    }
+
+    private void showDetails() {
+        mShowDetails = true;
+        if (mDetailsHelper == null) {
+            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
+            mDetailsHelper.setCloseListener(new CloseListener() {
+                @Override
+                public void onClose() {
+                    hideDetails();
+                }
+            });
+        }
+        mDetailsHelper.show();
+    }
+
+    @Override
+    public void onSyncDone(final MediaSet mediaSet, final int resultCode) {
+        if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
+            Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result="
+                    + resultCode);
+        }
+        ((Activity) mActivity).runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                GLRoot root = mActivity.getGLRoot();
+                root.lockRenderThread();
+                try {
+                    if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+                        mInitialSynced = true;
+                    }
+                    clearLoadingBit(BIT_LOADING_SYNC);
+                    if (resultCode == MediaSet.SYNC_RESULT_ERROR && mIsActive) {
+                        Log.w(TAG, "failed to load album set");
+                    }
+                } finally {
+                    root.unlockRenderThread();
+                }
+            }
+        });
+    }
+
+    private class MyLoadingListener implements LoadingListener {
+        @Override
+        public void onLoadingStarted() {
+            setLoadingBit(BIT_LOADING_RELOAD);
+        }
+
+        @Override
+        public void onLoadingFinished(boolean loadingFailed) {
+            clearLoadingBit(BIT_LOADING_RELOAD);
+        }
+    }
+
+    private class MyDetailsSource implements DetailsHelper.DetailsSource {
+        private int mIndex;
+
+        @Override
+        public int size() {
+            return mAlbumSetDataAdapter.size();
+        }
+
+        @Override
+        public int setIndex() {
+            Path id = mSelectionManager.getSelected(false).get(0);
+            mIndex = mAlbumSetDataAdapter.findSet(id);
+            return mIndex;
+        }
+
+        @Override
+        public MediaDetails getDetails() {
+            MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex);
+            if (item != null) {
+                mAlbumSetView.setHighlightItemPath(item.getPath());
+                return item.getDetails();
+            } else {
+                return null;
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/AppBridge.java b/src/com/android/gallery3d/app/AppBridge.java
new file mode 100644
index 0000000..ee55fa6
--- /dev/null
+++ b/src/com/android/gallery3d/app/AppBridge.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.gallery3d.ui.ScreenNail;
+
+// This is the bridge to connect a PhotoPage to the external environment.
+public abstract class AppBridge implements Parcelable {
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  These are requests sent from PhotoPage to the app
+    //////////////////////////////////////////////////////////////////////////
+
+    public abstract boolean isPanorama();
+    public abstract boolean isStaticCamera();
+    public abstract ScreenNail attachScreenNail();
+    public abstract void detachScreenNail();
+
+    // Return true if the tap is consumed.
+    public abstract boolean onSingleTapUp(int x, int y);
+
+    // This is used to notify that the screen nail will be drawn in full screen
+    // or not in next draw() call.
+    public abstract void onFullScreenChanged(boolean full);
+
+    //////////////////////////////////////////////////////////////////////////
+    //  These are requests send from app to PhotoPage
+    //////////////////////////////////////////////////////////////////////////
+
+    public interface Server {
+        // Set the camera frame relative to GLRootView.
+        public void setCameraRelativeFrame(Rect frame);
+        // Switch to the previous or next picture using the capture animation.
+        // The offset is -1 to switch to the previous picture, 1 to switch to
+        // the next picture.
+        public boolean switchWithCaptureAnimation(int offset);
+        // Enable or disable the swiping gestures (the default is enabled).
+        public void setSwipingEnabled(boolean enabled);
+        // Notify that the ScreenNail is changed.
+        public void notifyScreenNailChanged();
+        // Add a new media item to the secure album.
+        public void addSecureAlbumItem(boolean isVideo, int id);
+    }
+
+    // If server is null, the services are not available.
+    public abstract void setServer(Server server);
+}
diff --git a/src/com/android/gallery3d/app/BatchService.java b/src/com/android/gallery3d/app/BatchService.java
new file mode 100644
index 0000000..564001d
--- /dev/null
+++ b/src/com/android/gallery3d/app/BatchService.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.android.gallery3d.util.ThreadPool;
+
+public class BatchService extends Service {
+
+    public class LocalBinder extends Binder {
+        BatchService getService() {
+            return BatchService.this;
+        }
+    }
+
+    private final IBinder mBinder = new LocalBinder();
+    private ThreadPool mThreadPool = new ThreadPool(1, 1);
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    // The threadpool returned by getThreadPool must have only 1 thread
+    // running at a time, as MenuExecutor (atrociously) depends on this
+    // guarantee for synchronization.
+    public ThreadPool getThreadPool() {
+        return mThreadPool;
+    }
+}
diff --git a/src/com/android/gallery3d/app/CommonControllerOverlay.java b/src/com/android/gallery3d/app/CommonControllerOverlay.java
new file mode 100644
index 0000000..9adb4e7
--- /dev/null
+++ b/src/com/android/gallery3d/app/CommonControllerOverlay.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+/**
+ * The common playback controller for the Movie Player or Video Trimming.
+ */
+public abstract class CommonControllerOverlay extends FrameLayout implements
+        ControllerOverlay,
+        OnClickListener,
+        TimeBar.Listener {
+
+    protected enum State {
+        PLAYING,
+        PAUSED,
+        ENDED,
+        ERROR,
+        LOADING
+    }
+
+    private static final float ERROR_MESSAGE_RELATIVE_PADDING = 1.0f / 6;
+
+    protected Listener mListener;
+
+    protected final View mBackground;
+    protected TimeBar mTimeBar;
+
+    protected View mMainView;
+    protected final LinearLayout mLoadingView;
+    protected final TextView mErrorView;
+    protected final ImageView mPlayPauseReplayView;
+
+    protected State mState;
+
+    protected boolean mCanReplay = true;
+
+    public void setSeekable(boolean canSeek) {
+        mTimeBar.setSeekable(canSeek);
+    }
+
+    public CommonControllerOverlay(Context context) {
+        super(context);
+
+        mState = State.LOADING;
+        // TODO: Move the following layout code into xml file.
+        LayoutParams wrapContent =
+                new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+        LayoutParams matchParent =
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+
+        mBackground = new View(context);
+        mBackground.setBackgroundColor(context.getResources().getColor(R.color.darker_transparent));
+        addView(mBackground, matchParent);
+
+        // Depending on the usage, the timeBar can show a single scrubber, or
+        // multiple ones for trimming.
+        createTimeBar(context);
+        addView(mTimeBar, wrapContent);
+        mTimeBar.setContentDescription(
+                context.getResources().getString(R.string.accessibility_time_bar));
+        mLoadingView = new LinearLayout(context);
+        mLoadingView.setOrientation(LinearLayout.VERTICAL);
+        mLoadingView.setGravity(Gravity.CENTER_HORIZONTAL);
+        ProgressBar spinner = new ProgressBar(context);
+        spinner.setIndeterminate(true);
+        mLoadingView.addView(spinner, wrapContent);
+        TextView loadingText = createOverlayTextView(context);
+        loadingText.setText(R.string.loading_video);
+        mLoadingView.addView(loadingText, wrapContent);
+        addView(mLoadingView, wrapContent);
+
+        mPlayPauseReplayView = new ImageView(context);
+        mPlayPauseReplayView.setImageResource(R.drawable.ic_vidcontrol_play);
+        mPlayPauseReplayView.setContentDescription(
+                context.getResources().getString(R.string.accessibility_play_video));
+        mPlayPauseReplayView.setBackgroundResource(R.drawable.bg_vidcontrol);
+        mPlayPauseReplayView.setScaleType(ScaleType.CENTER);
+        mPlayPauseReplayView.setFocusable(true);
+        mPlayPauseReplayView.setClickable(true);
+        mPlayPauseReplayView.setOnClickListener(this);
+        addView(mPlayPauseReplayView, wrapContent);
+
+        mErrorView = createOverlayTextView(context);
+        addView(mErrorView, matchParent);
+
+        RelativeLayout.LayoutParams params =
+                new RelativeLayout.LayoutParams(
+                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        setLayoutParams(params);
+        hide();
+    }
+
+    abstract protected void createTimeBar(Context context);
+
+    private TextView createOverlayTextView(Context context) {
+        TextView view = new TextView(context);
+        view.setGravity(Gravity.CENTER);
+        view.setTextColor(0xFFFFFFFF);
+        view.setPadding(0, 15, 0, 15);
+        return view;
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        this.mListener = listener;
+    }
+
+    @Override
+    public void setCanReplay(boolean canReplay) {
+        this.mCanReplay = canReplay;
+    }
+
+    @Override
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public void showPlaying() {
+        mState = State.PLAYING;
+        showMainView(mPlayPauseReplayView);
+    }
+
+    @Override
+    public void showPaused() {
+        mState = State.PAUSED;
+        showMainView(mPlayPauseReplayView);
+    }
+
+    @Override
+    public void showEnded() {
+        mState = State.ENDED;
+        if (mCanReplay) showMainView(mPlayPauseReplayView);
+    }
+
+    @Override
+    public void showLoading() {
+        mState = State.LOADING;
+        showMainView(mLoadingView);
+    }
+
+    @Override
+    public void showErrorMessage(String message) {
+        mState = State.ERROR;
+        int padding = (int) (getMeasuredWidth() * ERROR_MESSAGE_RELATIVE_PADDING);
+        mErrorView.setPadding(
+                padding, mErrorView.getPaddingTop(), padding, mErrorView.getPaddingBottom());
+        mErrorView.setText(message);
+        showMainView(mErrorView);
+    }
+
+    @Override
+    public void setTimes(int currentTime, int totalTime,
+            int trimStartTime, int trimEndTime) {
+        mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime);
+    }
+
+    public void hide() {
+        mPlayPauseReplayView.setVisibility(View.INVISIBLE);
+        mLoadingView.setVisibility(View.INVISIBLE);
+        mBackground.setVisibility(View.INVISIBLE);
+        mTimeBar.setVisibility(View.INVISIBLE);
+        setVisibility(View.INVISIBLE);
+        setFocusable(true);
+        requestFocus();
+    }
+
+    private void showMainView(View view) {
+        mMainView = view;
+        mErrorView.setVisibility(mMainView == mErrorView ? View.VISIBLE : View.INVISIBLE);
+        mLoadingView.setVisibility(mMainView == mLoadingView ? View.VISIBLE : View.INVISIBLE);
+        mPlayPauseReplayView.setVisibility(
+                mMainView == mPlayPauseReplayView ? View.VISIBLE : View.INVISIBLE);
+        show();
+    }
+
+    @Override
+    public void show() {
+        updateViews();
+        setVisibility(View.VISIBLE);
+        setFocusable(false);
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (mListener != null) {
+            if (view == mPlayPauseReplayView) {
+                if (mState == State.ENDED) {
+                    if (mCanReplay) {
+                        mListener.onReplay();
+                    }
+                } else if (mState == State.PAUSED || mState == State.PLAYING) {
+                    mListener.onPlayPause();
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (super.onTouchEvent(event)) {
+            return true;
+        }
+        return false;
+    }
+
+    // The paddings of 4 sides which covered by system components. E.g.
+    // +-----------------+\
+    // | Action Bar | insets.top
+    // +-----------------+/
+    // | |
+    // | Content Area | insets.right = insets.left = 0
+    // | |
+    // +-----------------+\
+    // | Navigation Bar | insets.bottom
+    // +-----------------+/
+    // Please see View.fitSystemWindows() for more details.
+    private final Rect mWindowInsets = new Rect();
+
+    @Override
+    protected boolean fitSystemWindows(Rect insets) {
+        // We don't set the paddings of this View, otherwise,
+        // the content will get cropped outside window
+        mWindowInsets.set(insets);
+        return true;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        Rect insets = mWindowInsets;
+        int pl = insets.left; // the left paddings
+        int pr = insets.right;
+        int pt = insets.top;
+        int pb = insets.bottom;
+
+        int h = bottom - top;
+        int w = right - left;
+        boolean error = mErrorView.getVisibility() == View.VISIBLE;
+
+        int y = h - pb;
+        // Put both TimeBar and Background just above the bottom system
+        // component.
+        // But extend the background to the width of the screen, since we don't
+        // care if it will be covered by a system component and it looks better.
+        mBackground.layout(0, y - mTimeBar.getBarHeight(), w, y);
+        mTimeBar.layout(pl, y - mTimeBar.getPreferredHeight(), w - pr, y);
+
+        // Put the play/pause/next/ previous button in the center of the screen
+        layoutCenteredView(mPlayPauseReplayView, 0, 0, w, h);
+
+        if (mMainView != null) {
+            layoutCenteredView(mMainView, 0, 0, w, h);
+        }
+    }
+
+    private void layoutCenteredView(View view, int l, int t, int r, int b) {
+        int cw = view.getMeasuredWidth();
+        int ch = view.getMeasuredHeight();
+        int cl = (r - l - cw) / 2;
+        int ct = (b - t - ch) / 2;
+        view.layout(cl, ct, cl + cw, ct + ch);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        measureChildren(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    protected void updateViews() {
+        mBackground.setVisibility(View.VISIBLE);
+        mTimeBar.setVisibility(View.VISIBLE);
+        Resources resources = getContext().getResources();
+        int imageResource = R.drawable.ic_vidcontrol_reload;
+        String contentDescription = resources.getString(R.string.accessibility_reload_video);
+        if (mState == State.PAUSED) {
+            imageResource = R.drawable.ic_vidcontrol_play;
+            contentDescription = resources.getString(R.string.accessibility_play_video);
+        } else if (mState == State.PLAYING) {
+            imageResource = R.drawable.ic_vidcontrol_pause;
+            contentDescription = resources.getString(R.string.accessibility_pause_video);
+        }
+
+        mPlayPauseReplayView.setImageResource(imageResource);
+        mPlayPauseReplayView.setContentDescription(contentDescription);
+        mPlayPauseReplayView.setVisibility(
+                (mState != State.LOADING && mState != State.ERROR &&
+                !(mState == State.ENDED && !mCanReplay))
+                ? View.VISIBLE : View.GONE);
+        requestLayout();
+    }
+
+    // TimeBar listener
+
+    @Override
+    public void onScrubbingStart() {
+        mListener.onSeekStart();
+    }
+
+    @Override
+    public void onScrubbingMove(int time) {
+        mListener.onSeekMove(time);
+    }
+
+    @Override
+    public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) {
+        mListener.onSeekEnd(time, trimStartTime, trimEndTime);
+    }
+}
diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java
new file mode 100644
index 0000000..7183acc
--- /dev/null
+++ b/src/com/android/gallery3d/app/Config.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.AlbumSetSlotRenderer;
+import com.android.gallery3d.ui.SlotView;
+
+final class Config {
+    public static class AlbumSetPage {
+        private static AlbumSetPage sInstance;
+
+        public SlotView.Spec slotViewSpec;
+        public AlbumSetSlotRenderer.LabelSpec labelSpec;
+        public int paddingTop;
+        public int paddingBottom;
+        public int placeholderColor;
+
+        public static synchronized AlbumSetPage get(Context context) {
+            if (sInstance == null) {
+                sInstance = new AlbumSetPage(context);
+            }
+            return sInstance;
+        }
+
+        private AlbumSetPage(Context context) {
+            Resources r = context.getResources();
+
+            placeholderColor = r.getColor(R.color.albumset_placeholder);
+
+            slotViewSpec = new SlotView.Spec();
+            slotViewSpec.rowsLand = r.getInteger(R.integer.albumset_rows_land);
+            slotViewSpec.rowsPort = r.getInteger(R.integer.albumset_rows_port);
+            slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.albumset_slot_gap);
+            slotViewSpec.slotHeightAdditional = 0;
+
+            paddingTop = r.getDimensionPixelSize(R.dimen.albumset_padding_top);
+            paddingBottom = r.getDimensionPixelSize(R.dimen.albumset_padding_bottom);
+
+            labelSpec = new AlbumSetSlotRenderer.LabelSpec();
+            labelSpec.labelBackgroundHeight = r.getDimensionPixelSize(
+                    R.dimen.albumset_label_background_height);
+            labelSpec.titleOffset = r.getDimensionPixelSize(
+                    R.dimen.albumset_title_offset);
+            labelSpec.countOffset = r.getDimensionPixelSize(
+                    R.dimen.albumset_count_offset);
+            labelSpec.titleFontSize = r.getDimensionPixelSize(
+                    R.dimen.albumset_title_font_size);
+            labelSpec.countFontSize = r.getDimensionPixelSize(
+                    R.dimen.albumset_count_font_size);
+            labelSpec.leftMargin = r.getDimensionPixelSize(
+                    R.dimen.albumset_left_margin);
+            labelSpec.titleRightMargin = r.getDimensionPixelSize(
+                    R.dimen.albumset_title_right_margin);
+            labelSpec.iconSize = r.getDimensionPixelSize(
+                    R.dimen.albumset_icon_size);
+            labelSpec.backgroundColor = r.getColor(
+                    R.color.albumset_label_background);
+            labelSpec.titleColor = r.getColor(R.color.albumset_label_title);
+            labelSpec.countColor = r.getColor(R.color.albumset_label_count);
+        }
+    }
+
+    public static class AlbumPage {
+        private static AlbumPage sInstance;
+
+        public SlotView.Spec slotViewSpec;
+        public int placeholderColor;
+
+        public static synchronized AlbumPage get(Context context) {
+            if (sInstance == null) {
+                sInstance = new AlbumPage(context);
+            }
+            return sInstance;
+        }
+
+        private AlbumPage(Context context) {
+            Resources r = context.getResources();
+
+            placeholderColor = r.getColor(R.color.album_placeholder);
+
+            slotViewSpec = new SlotView.Spec();
+            slotViewSpec.rowsLand = r.getInteger(R.integer.album_rows_land);
+            slotViewSpec.rowsPort = r.getInteger(R.integer.album_rows_port);
+            slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.album_slot_gap);
+        }
+    }
+
+    public static class ManageCachePage extends AlbumSetPage {
+        private static ManageCachePage sInstance;
+
+        public final int cachePinSize;
+        public final int cachePinMargin;
+
+        public static synchronized ManageCachePage get(Context context) {
+            if (sInstance == null) {
+                sInstance = new ManageCachePage(context);
+            }
+            return sInstance;
+        }
+
+        public ManageCachePage(Context context) {
+            super(context);
+            Resources r = context.getResources();
+            cachePinSize = r.getDimensionPixelSize(R.dimen.cache_pin_size);
+            cachePinMargin = r.getDimensionPixelSize(R.dimen.cache_pin_margin);
+        }
+    }
+}
+
diff --git a/src/com/android/gallery3d/app/ControllerOverlay.java b/src/com/android/gallery3d/app/ControllerOverlay.java
new file mode 100644
index 0000000..078f59e
--- /dev/null
+++ b/src/com/android/gallery3d/app/ControllerOverlay.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.app;
+
+import android.view.View;
+
+public interface ControllerOverlay {
+
+  interface Listener {
+    void onPlayPause();
+    void onSeekStart();
+    void onSeekMove(int time);
+    void onSeekEnd(int time, int trimStartTime, int trimEndTime);
+    void onShown();
+    void onHidden();
+    void onReplay();
+  }
+
+  void setListener(Listener listener);
+
+  void setCanReplay(boolean canReplay);
+
+  /**
+   * @return The overlay view that should be added to the player.
+   */
+  View getView();
+
+  void show();
+
+  void showPlaying();
+
+  void showPaused();
+
+  void showEnded();
+
+  void showLoading();
+
+  void showErrorMessage(String message);
+
+  void setTimes(int currentTime, int totalTime,
+          int trimStartTime, int trimEndTime);
+}
diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java
new file mode 100644
index 0000000..7ca86e5
--- /dev/null
+++ b/src/com/android/gallery3d/app/DialogPicker.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+public class DialogPicker extends PickerActivity {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        int typeBits = GalleryUtils.determineTypeBits(this, getIntent());
+        setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+        Intent intent = getIntent();
+        Bundle extras = intent.getExtras();
+        Bundle data = extras == null ? new Bundle() : new Bundle(extras);
+
+        data.putBoolean(Gallery.KEY_GET_CONTENT, true);
+        data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+                getDataManager().getTopSetPath(typeBits));
+        getStateManager().startState(AlbumSetPage.class, data);
+    }
+}
diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java
new file mode 100644
index 0000000..d99d97b
--- /dev/null
+++ b/src/com/android/gallery3d/app/EyePosition.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.SystemClock;
+import android.util.FloatMath;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+public class EyePosition {
+    @SuppressWarnings("unused")
+    private static final String TAG = "EyePosition";
+
+    public interface EyePositionListener {
+        public void onEyePositionChanged(float x, float y, float z);
+    }
+
+    private static final float GYROSCOPE_THRESHOLD = 0.15f;
+    private static final float GYROSCOPE_LIMIT = 10f;
+    private static final int GYROSCOPE_SETTLE_DOWN = 15;
+    private static final float GYROSCOPE_RESTORE_FACTOR = 0.995f;
+
+    private static final float USER_ANGEL = (float) Math.toRadians(10);
+    private static final float USER_ANGEL_COS = FloatMath.cos(USER_ANGEL);
+    private static final float USER_ANGEL_SIN = FloatMath.sin(USER_ANGEL);
+    private static final float MAX_VIEW_RANGE = (float) 0.5;
+    private static final int NOT_STARTED = -1;
+
+    private static final float USER_DISTANCE_METER = 0.3f;
+
+    private Context mContext;
+    private EyePositionListener mListener;
+    private Display mDisplay;
+    // The eyes' position of the user, the origin is at the center of the
+    // device and the unit is in pixels.
+    private float mX;
+    private float mY;
+    private float mZ;
+
+    private final float mUserDistance; // in pixel
+    private final float mLimit;
+    private long mStartTime = NOT_STARTED;
+    private Sensor mSensor;
+    private PositionListener mPositionListener = new PositionListener();
+
+    private int mGyroscopeCountdown = 0;
+
+    public EyePosition(Context context, EyePositionListener listener) {
+        mContext = context;
+        mListener = listener;
+        mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+        mLimit = mUserDistance * MAX_VIEW_RANGE;
+
+        WindowManager wManager = (WindowManager) mContext
+                .getSystemService(Context.WINDOW_SERVICE);
+        mDisplay = wManager.getDefaultDisplay();
+
+        // The 3D effect where the photo albums fan out in 3D based on angle
+        // of device tilt is currently disabled.
+/*
+        SensorManager sManager = (SensorManager) mContext
+                .getSystemService(Context.SENSOR_SERVICE);
+        mSensor = sManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+        if (mSensor == null) {
+            Log.w(TAG, "no gyroscope, use accelerometer instead");
+            mSensor = sManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        }
+        if (mSensor == null) {
+            Log.w(TAG, "no sensor available");
+        }
+*/
+    }
+
+    public void resetPosition() {
+        mStartTime = NOT_STARTED;
+        mX = mY = 0;
+        mZ = -mUserDistance;
+        mListener.onEyePositionChanged(mX, mY, mZ);
+    }
+
+    /*
+     * We assume the user is at the following position
+     *
+     *              /|\  user's eye
+     *               |   /
+     *   -G(gravity) |  /
+     *               |_/
+     *             / |/_____\ -Y (-y direction of device)
+     *     user angel
+     */
+    private void onAccelerometerChanged(float gx, float gy, float gz) {
+
+        float x = gx, y = gy, z = gz;
+
+        switch (mDisplay.getRotation()) {
+            case Surface.ROTATION_90: x = -gy; y= gx; break;
+            case Surface.ROTATION_180: x = -gx; y = -gy; break;
+            case Surface.ROTATION_270: x = gy; y = -gx; break;
+        }
+
+        float temp = x * x + y * y + z * z;
+        float t = -y /temp;
+
+        float tx = t * x;
+        float ty = -1 + t * y;
+        float tz = t * z;
+
+        float length = FloatMath.sqrt(tx * tx + ty * ty + tz * tz);
+        float glength = FloatMath.sqrt(temp);
+
+        mX = Utils.clamp((x * USER_ANGEL_COS / glength
+                + tx * USER_ANGEL_SIN / length) * mUserDistance,
+                -mLimit, mLimit);
+        mY = -Utils.clamp((y * USER_ANGEL_COS / glength
+                + ty * USER_ANGEL_SIN / length) * mUserDistance,
+                -mLimit, mLimit);
+        mZ = -FloatMath.sqrt(
+                mUserDistance * mUserDistance - mX * mX - mY * mY);
+        mListener.onEyePositionChanged(mX, mY, mZ);
+    }
+
+    private void onGyroscopeChanged(float gx, float gy, float gz) {
+        long now = SystemClock.elapsedRealtime();
+        float distance = (gx > 0 ? gx : -gx) + (gy > 0 ? gy : - gy);
+        if (distance < GYROSCOPE_THRESHOLD
+                || distance > GYROSCOPE_LIMIT || mGyroscopeCountdown > 0) {
+            --mGyroscopeCountdown;
+            mStartTime = now;
+            float limit = mUserDistance / 20f;
+            if (mX > limit || mX < -limit || mY > limit || mY < -limit) {
+                mX *= GYROSCOPE_RESTORE_FACTOR;
+                mY *= GYROSCOPE_RESTORE_FACTOR;
+                mZ = (float) -Math.sqrt(
+                        mUserDistance * mUserDistance - mX * mX - mY * mY);
+                mListener.onEyePositionChanged(mX, mY, mZ);
+            }
+            return;
+        }
+
+        float t = (now - mStartTime) / 1000f * mUserDistance * (-mZ);
+        mStartTime = now;
+
+        float x = -gy, y = -gx;
+        switch (mDisplay.getRotation()) {
+            case Surface.ROTATION_90: x = -gx; y= gy; break;
+            case Surface.ROTATION_180: x = gy; y = gx; break;
+            case Surface.ROTATION_270: x = gx; y = -gy; break;
+        }
+
+        mX = Utils.clamp((float) (mX + x * t / Math.hypot(mZ, mX)),
+                -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR;
+        mY = Utils.clamp((float) (mY + y * t / Math.hypot(mZ, mY)),
+                -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR;
+
+        mZ = -FloatMath.sqrt(
+                mUserDistance * mUserDistance - mX * mX - mY * mY);
+        mListener.onEyePositionChanged(mX, mY, mZ);
+    }
+
+    private class PositionListener implements SensorEventListener {
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+        }
+
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            switch (event.sensor.getType()) {
+                case Sensor.TYPE_GYROSCOPE: {
+                    onGyroscopeChanged(
+                            event.values[0], event.values[1], event.values[2]);
+                    break;
+                }
+                case Sensor.TYPE_ACCELEROMETER: {
+                    onAccelerometerChanged(
+                            event.values[0], event.values[1], event.values[2]);
+                }
+            }
+        }
+    }
+
+    public void pause() {
+        if (mSensor != null) {
+            SensorManager sManager = (SensorManager) mContext
+                    .getSystemService(Context.SENSOR_SERVICE);
+            sManager.unregisterListener(mPositionListener);
+        }
+    }
+
+    public void resume() {
+        if (mSensor != null) {
+            SensorManager sManager = (SensorManager) mContext
+                    .getSystemService(Context.SENSOR_SERVICE);
+            sManager.registerListener(mPositionListener,
+                    mSensor, SensorManager.SENSOR_DELAY_GAME);
+        }
+
+        mStartTime = NOT_STARTED;
+        mGyroscopeCountdown = GYROSCOPE_SETTLE_DOWN;
+        mX = mY = 0;
+        mZ = -mUserDistance;
+        mListener.onEyePositionChanged(mX, mY, mZ);
+    }
+}
diff --git a/src/com/android/gallery3d/app/FilmstripPage.java b/src/com/android/gallery3d/app/FilmstripPage.java
new file mode 100644
index 0000000..a9726cd
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilmstripPage.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+public class FilmstripPage extends PhotoPage {
+
+}
diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java
new file mode 100644
index 0000000..bc28a9c
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilterUtils.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+
+// This class handles filtering and clustering.
+//
+// We allow at most only one filter operation at a time (Currently it
+// doesn't make sense to use more than one). Also each clustering operation
+// can be applied at most once. In addition, there is one more constraint
+// ("fixed set constraint") described below.
+//
+// A clustered album (not including album set) and its base sets are fixed.
+// For example,
+//
+// /cluster/{base_set}/time/7
+//
+// This set and all sets inside base_set (recursively) are fixed because
+// 1. We can not change this set to use another clustering condition (like
+//    changing "time" to "location").
+// 2. Neither can we change any set in the base_set.
+// The reason is in both cases the 7th set may not exist in the new clustering.
+// ---------------------
+// newPath operation: create a new path based on a source path and put an extra
+// condition on top of it:
+//
+// T = newFilterPath(S, filterType);
+// T = newClusterPath(S, clusterType);
+//
+// Similar functions can be used to replace the current condition (if there is one).
+//
+// T = switchFilterPath(S, filterType);
+// T = switchClusterPath(S, clusterType);
+//
+// For all fixed set in the path defined above, if some clusterType and
+// filterType are already used, they cannot not be used as parameter for these
+// functions. setupMenuItems() makes sure those types cannot be selected.
+//
+public class FilterUtils {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilterUtils";
+
+    public static final int CLUSTER_BY_ALBUM = 1;
+    public static final int CLUSTER_BY_TIME = 2;
+    public static final int CLUSTER_BY_LOCATION = 4;
+    public static final int CLUSTER_BY_TAG = 8;
+    public static final int CLUSTER_BY_SIZE = 16;
+    public static final int CLUSTER_BY_FACE = 32;
+
+    public static final int FILTER_IMAGE_ONLY = 1;
+    public static final int FILTER_VIDEO_ONLY = 2;
+    public static final int FILTER_ALL = 4;
+
+    // These are indices of the return values of getAppliedFilters().
+    // The _F suffix means "fixed".
+    private static final int CLUSTER_TYPE = 0;
+    private static final int FILTER_TYPE = 1;
+    private static final int CLUSTER_TYPE_F = 2;
+    private static final int FILTER_TYPE_F = 3;
+    private static final int CLUSTER_CURRENT_TYPE = 4;
+    private static final int FILTER_CURRENT_TYPE = 5;
+
+    public static void setupMenuItems(GalleryActionBar actionBar, Path path, boolean inAlbum) {
+        int[] result = new int[6];
+        getAppliedFilters(path, result);
+        int ctype = result[CLUSTER_TYPE];
+        int ftype = result[FILTER_TYPE];
+        int ftypef = result[FILTER_TYPE_F];
+        int ccurrent = result[CLUSTER_CURRENT_TYPE];
+        int fcurrent = result[FILTER_CURRENT_TYPE];
+
+        setMenuItemApplied(actionBar, CLUSTER_BY_TIME,
+                (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0);
+        setMenuItemApplied(actionBar, CLUSTER_BY_LOCATION,
+                (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0);
+        setMenuItemApplied(actionBar, CLUSTER_BY_TAG,
+                (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0);
+        setMenuItemApplied(actionBar, CLUSTER_BY_FACE,
+                (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0);
+
+        actionBar.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0);
+
+        setMenuItemApplied(actionBar, R.id.action_cluster_album, ctype == 0,
+                ccurrent == 0);
+
+        // A filtering is available if it's not applied, and the old filtering
+        // (if any) is not fixed.
+        setMenuItemAppliedEnabled(actionBar, R.string.show_images_only,
+                (ftype & FILTER_IMAGE_ONLY) != 0,
+                (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0,
+                (fcurrent & FILTER_IMAGE_ONLY) != 0);
+        setMenuItemAppliedEnabled(actionBar, R.string.show_videos_only,
+                (ftype & FILTER_VIDEO_ONLY) != 0,
+                (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0,
+                (fcurrent & FILTER_VIDEO_ONLY) != 0);
+        setMenuItemAppliedEnabled(actionBar, R.string.show_all,
+                ftype == 0, ftype != 0 && ftypef == 0, fcurrent == 0);
+    }
+
+    // Gets the filters applied in the path.
+    private static void getAppliedFilters(Path path, int[] result) {
+        getAppliedFilters(path, result, false);
+    }
+
+    private static void getAppliedFilters(Path path, int[] result, boolean underCluster) {
+        String[] segments = path.split();
+        // Recurse into sub media sets.
+        for (int i = 0; i < segments.length; i++) {
+            if (segments[i].startsWith("{")) {
+                String[] sets = Path.splitSequence(segments[i]);
+                for (int j = 0; j < sets.length; j++) {
+                    Path sub = Path.fromString(sets[j]);
+                    getAppliedFilters(sub, result, underCluster);
+                }
+            }
+        }
+
+        // update current selection
+        if (segments[0].equals("cluster")) {
+            // if this is a clustered album, set underCluster to true.
+            if (segments.length == 4) {
+                underCluster = true;
+            }
+
+            int ctype = toClusterType(segments[2]);
+            result[CLUSTER_TYPE] |= ctype;
+            result[CLUSTER_CURRENT_TYPE] = ctype;
+            if (underCluster) {
+                result[CLUSTER_TYPE_F] |= ctype;
+            }
+        }
+    }
+
+    private static int toClusterType(String s) {
+        if (s.equals("time")) {
+            return CLUSTER_BY_TIME;
+        } else if (s.equals("location")) {
+            return CLUSTER_BY_LOCATION;
+        } else if (s.equals("tag")) {
+            return CLUSTER_BY_TAG;
+        } else if (s.equals("size")) {
+            return CLUSTER_BY_SIZE;
+        } else if (s.equals("face")) {
+            return CLUSTER_BY_FACE;
+        }
+        return 0;
+    }
+
+    private static void setMenuItemApplied(
+            GalleryActionBar model, int id, boolean applied, boolean updateTitle) {
+        model.setClusterItemEnabled(id, !applied);
+    }
+
+    private static void setMenuItemAppliedEnabled(GalleryActionBar model, int id, boolean applied, boolean enabled, boolean updateTitle) {
+        model.setClusterItemEnabled(id, enabled);
+    }
+
+    // Add a specified filter to the path.
+    public static String newFilterPath(String base, int filterType) {
+        int mediaType;
+        switch (filterType) {
+            case FILTER_IMAGE_ONLY:
+                mediaType = MediaObject.MEDIA_TYPE_IMAGE;
+                break;
+            case FILTER_VIDEO_ONLY:
+                mediaType = MediaObject.MEDIA_TYPE_VIDEO;
+                break;
+            default:  /* FILTER_ALL */
+                return base;
+        }
+
+        return "/filter/mediatype/" + mediaType + "/{" + base + "}";
+    }
+
+    // Add a specified clustering to the path.
+    public static String newClusterPath(String base, int clusterType) {
+        String kind;
+        switch (clusterType) {
+            case CLUSTER_BY_TIME:
+                kind = "time";
+                break;
+            case CLUSTER_BY_LOCATION:
+                kind = "location";
+                break;
+            case CLUSTER_BY_TAG:
+                kind = "tag";
+                break;
+            case CLUSTER_BY_SIZE:
+                kind = "size";
+                break;
+            case CLUSTER_BY_FACE:
+                kind = "face";
+                break;
+            default: /* CLUSTER_BY_ALBUM */
+                return base;
+        }
+
+        return "/cluster/{" + base + "}/" + kind;
+    }
+
+    // Change the topmost clustering to the specified type.
+    public static String switchClusterPath(String base, int clusterType) {
+        return newClusterPath(removeOneClusterFromPath(base), clusterType);
+    }
+
+    // Remove the topmost clustering (if any) from the path.
+    private static String removeOneClusterFromPath(String base) {
+        boolean[] done = new boolean[1];
+        return removeOneClusterFromPath(base, done);
+    }
+
+    private static String removeOneClusterFromPath(String base, boolean[] done) {
+        if (done[0]) return base;
+
+        String[] segments = Path.split(base);
+        if (segments[0].equals("cluster")) {
+            done[0] = true;
+            return Path.splitSequence(segments[1])[0];
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < segments.length; i++) {
+            sb.append("/");
+            if (segments[i].startsWith("{")) {
+                sb.append("{");
+                String[] sets = Path.splitSequence(segments[i]);
+                for (int j = 0; j < sets.length; j++) {
+                    if (j > 0) {
+                        sb.append(",");
+                    }
+                    sb.append(removeOneClusterFromPath(sets[j], done));
+                }
+                sb.append("}");
+            } else {
+                sb.append(segments[i]);
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java
new file mode 100644
index 0000000..baef56b
--- /dev/null
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+
+public final class Gallery extends AbstractGalleryActivity implements OnCancelListener {
+    public static final String EXTRA_SLIDESHOW = "slideshow";
+    public static final String EXTRA_DREAM = "dream";
+    public static final String EXTRA_CROP = "crop";
+
+    public static final String ACTION_REVIEW = "com.android.camera.action.REVIEW";
+    public static final String KEY_GET_CONTENT = "get-content";
+    public static final String KEY_GET_ALBUM = "get-album";
+    public static final String KEY_TYPE_BITS = "type-bits";
+    public static final String KEY_MEDIA_TYPES = "mediaTypes";
+    public static final String KEY_DISMISS_KEYGUARD = "dismiss-keyguard";
+
+    private static final String TAG = "Gallery";
+    private Dialog mVersionCheckDialog;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        requestWindowFeature(Window.FEATURE_ACTION_BAR);
+        requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+        if (getIntent().getBooleanExtra(KEY_DISMISS_KEYGUARD, false)) {
+            getWindow().addFlags(
+                    WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+        }
+
+        setContentView(R.layout.main);
+
+        if (savedInstanceState != null) {
+            getStateManager().restoreFromState(savedInstanceState);
+        } else {
+            initializeByIntent();
+        }
+    }
+
+    private void initializeByIntent() {
+        Intent intent = getIntent();
+        String action = intent.getAction();
+
+        if (Intent.ACTION_GET_CONTENT.equalsIgnoreCase(action)) {
+            startGetContent(intent);
+        } else if (Intent.ACTION_PICK.equalsIgnoreCase(action)) {
+            // We do NOT really support the PICK intent. Handle it as
+            // the GET_CONTENT. However, we need to translate the type
+            // in the intent here.
+            Log.w(TAG, "action PICK is not supported");
+            String type = Utils.ensureNotNull(intent.getType());
+            if (type.startsWith("vnd.android.cursor.dir/")) {
+                if (type.endsWith("/image")) intent.setType("image/*");
+                if (type.endsWith("/video")) intent.setType("video/*");
+            }
+            startGetContent(intent);
+        } else if (Intent.ACTION_VIEW.equalsIgnoreCase(action)
+                || ACTION_REVIEW.equalsIgnoreCase(action)){
+            startViewAction(intent);
+        } else {
+            startDefaultPage();
+        }
+    }
+
+    public void startDefaultPage() {
+        PicasaSource.showSignInReminder(this);
+        Bundle data = new Bundle();
+        data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+                getDataManager().getTopSetPath(DataManager.INCLUDE_ALL));
+        getStateManager().startState(AlbumSetPage.class, data);
+        mVersionCheckDialog = PicasaSource.getVersionCheckDialog(this);
+        if (mVersionCheckDialog != null) {
+            mVersionCheckDialog.setOnCancelListener(this);
+        }
+    }
+
+    private void startGetContent(Intent intent) {
+        Bundle data = intent.getExtras() != null
+                ? new Bundle(intent.getExtras())
+                : new Bundle();
+        data.putBoolean(KEY_GET_CONTENT, true);
+        int typeBits = GalleryUtils.determineTypeBits(this, intent);
+        data.putInt(KEY_TYPE_BITS, typeBits);
+        data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+                getDataManager().getTopSetPath(typeBits));
+        getStateManager().startState(AlbumSetPage.class, data);
+    }
+
+    private String getContentType(Intent intent) {
+        String type = intent.getType();
+        if (type != null) {
+            return GalleryUtils.MIME_TYPE_PANORAMA360.equals(type)
+                ? MediaItem.MIME_TYPE_JPEG : type;
+        }
+
+        Uri uri = intent.getData();
+        try {
+            return getContentResolver().getType(uri);
+        } catch (Throwable t) {
+            Log.w(TAG, "get type fail", t);
+            return null;
+        }
+    }
+
+    private void startViewAction(Intent intent) {
+        Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
+        if (slideshow) {
+            getActionBar().hide();
+            DataManager manager = getDataManager();
+            Path path = manager.findPathByUri(intent.getData(), intent.getType());
+            if (path == null || manager.getMediaObject(path)
+                    instanceof MediaItem) {
+                path = Path.fromString(
+                        manager.getTopSetPath(DataManager.INCLUDE_IMAGE));
+            }
+            Bundle data = new Bundle();
+            data.putString(SlideshowPage.KEY_SET_PATH, path.toString());
+            data.putBoolean(SlideshowPage.KEY_RANDOM_ORDER, true);
+            data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+            if (intent.getBooleanExtra(EXTRA_DREAM, false)) {
+                data.putBoolean(SlideshowPage.KEY_DREAM, true);
+            }
+            getStateManager().startState(SlideshowPage.class, data);
+        } else {
+            Bundle data = new Bundle();
+            DataManager dm = getDataManager();
+            Uri uri = intent.getData();
+            String contentType = getContentType(intent);
+            if (contentType == null) {
+                Toast.makeText(this,
+                        R.string.no_such_item, Toast.LENGTH_LONG).show();
+                finish();
+                return;
+            }
+            if (uri == null) {
+                int typeBits = GalleryUtils.determineTypeBits(this, intent);
+                data.putInt(KEY_TYPE_BITS, typeBits);
+                data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+                        getDataManager().getTopSetPath(typeBits));
+                getStateManager().startState(AlbumSetPage.class, data);
+            } else if (contentType.startsWith(
+                    ContentResolver.CURSOR_DIR_BASE_TYPE)) {
+                int mediaType = intent.getIntExtra(KEY_MEDIA_TYPES, 0);
+                if (mediaType != 0) {
+                    uri = uri.buildUpon().appendQueryParameter(
+                            KEY_MEDIA_TYPES, String.valueOf(mediaType))
+                            .build();
+                }
+                Path setPath = dm.findPathByUri(uri, null);
+                MediaSet mediaSet = null;
+                if (setPath != null) {
+                    mediaSet = (MediaSet) dm.getMediaObject(setPath);
+                }
+                if (mediaSet != null) {
+                    if (mediaSet.isLeafAlbum()) {
+                        data.putString(AlbumPage.KEY_MEDIA_PATH, setPath.toString());
+                        data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+                                dm.getTopSetPath(DataManager.INCLUDE_ALL));
+                        getStateManager().startState(AlbumPage.class, data);
+                    } else {
+                        data.putString(AlbumSetPage.KEY_MEDIA_PATH, setPath.toString());
+                        getStateManager().startState(AlbumSetPage.class, data);
+                    }
+                } else {
+                    startDefaultPage();
+                }
+            } else {
+                Path itemPath = dm.findPathByUri(uri, contentType);
+                Path albumPath = dm.getDefaultSetOf(itemPath);
+
+                data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString());
+
+                // TODO: Make the parameter "SingleItemOnly" public so other
+                //       activities can reference it.
+                boolean singleItemOnly = (albumPath == null)
+                        || intent.getBooleanExtra("SingleItemOnly", false);
+                if (!singleItemOnly) {
+                    data.putString(PhotoPage.KEY_MEDIA_SET_PATH, albumPath.toString());
+                    // when FLAG_ACTIVITY_NEW_TASK is set, (e.g. when intent is fired
+                    // from notification), back button should behave the same as up button
+                    // rather than taking users back to the home screen
+                    if (intent.getBooleanExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, false)
+                            || ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0)) {
+                        data.putBoolean(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+                    }
+                }
+
+                getStateManager().startState(SinglePhotoPage.class, data);
+            }
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        Utils.assertTrue(getStateManager().getStateCount() > 0);
+        super.onResume();
+        if (mVersionCheckDialog != null) {
+            mVersionCheckDialog.show();
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if (mVersionCheckDialog != null) {
+            mVersionCheckDialog.dismiss();
+        }
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        if (dialog == mVersionCheckDialog) {
+            mVersionCheckDialog = null;
+        }
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        final boolean isTouchPad = (event.getSource()
+                & InputDevice.SOURCE_CLASS_POSITION) != 0;
+        if (isTouchPad) {
+            float maxX = event.getDevice().getMotionRange(MotionEvent.AXIS_X).getMax();
+            float maxY = event.getDevice().getMotionRange(MotionEvent.AXIS_Y).getMax();
+            View decor = getWindow().getDecorView();
+            float scaleX = decor.getWidth() / maxX;
+            float scaleY = decor.getHeight() / maxY;
+            float x = event.getX() * scaleX;
+            //x = decor.getWidth() - x; // invert x
+            float y = event.getY() * scaleY;
+            //y = decor.getHeight() - y; // invert y
+            MotionEvent touchEvent = MotionEvent.obtain(event.getDownTime(),
+                    event.getEventTime(), event.getAction(), x, y, event.getMetaState());
+            return dispatchTouchEvent(touchEvent);
+        }
+        return super.onGenericMotionEvent(event);
+    }
+}
diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java
new file mode 100644
index 0000000..588f584
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.app;
+
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.ActionBar.OnNavigationListener;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ShareActionProvider;
+import android.widget.TextView;
+import android.widget.TwoLineListItem;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+
+public class GalleryActionBar implements OnNavigationListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "GalleryActionBar";
+
+    private ClusterRunner mClusterRunner;
+    private CharSequence[] mTitles;
+    private ArrayList<Integer> mActions;
+    private Context mContext;
+    private LayoutInflater mInflater;
+    private AbstractGalleryActivity mActivity;
+    private ActionBar mActionBar;
+    private int mCurrentIndex;
+    private ClusterAdapter mAdapter = new ClusterAdapter();
+
+    private AlbumModeAdapter mAlbumModeAdapter;
+    private OnAlbumModeSelectedListener mAlbumModeListener;
+    private int mLastAlbumModeSelected;
+    private CharSequence [] mAlbumModes;
+    public static final int ALBUM_FILMSTRIP_MODE_SELECTED = 0;
+    public static final int ALBUM_GRID_MODE_SELECTED = 1;
+
+    public interface ClusterRunner {
+        public void doCluster(int id);
+    }
+
+    public interface OnAlbumModeSelectedListener {
+        public void onAlbumModeSelected(int mode);
+    }
+
+    private static class ActionItem {
+        public int action;
+        public boolean enabled;
+        public boolean visible;
+        public int spinnerTitle;
+        public int dialogTitle;
+        public int clusterBy;
+
+        public ActionItem(int action, boolean applied, boolean enabled, int title,
+                int clusterBy) {
+            this(action, applied, enabled, title, title, clusterBy);
+        }
+
+        public ActionItem(int action, boolean applied, boolean enabled, int spinnerTitle,
+                int dialogTitle, int clusterBy) {
+            this.action = action;
+            this.enabled = enabled;
+            this.spinnerTitle = spinnerTitle;
+            this.dialogTitle = dialogTitle;
+            this.clusterBy = clusterBy;
+            this.visible = true;
+        }
+    }
+
+    private static final ActionItem[] sClusterItems = new ActionItem[] {
+        new ActionItem(FilterUtils.CLUSTER_BY_ALBUM, true, false, R.string.albums,
+                R.string.group_by_album),
+        new ActionItem(FilterUtils.CLUSTER_BY_LOCATION, true, false,
+                R.string.locations, R.string.location, R.string.group_by_location),
+        new ActionItem(FilterUtils.CLUSTER_BY_TIME, true, false, R.string.times,
+                R.string.time, R.string.group_by_time),
+        new ActionItem(FilterUtils.CLUSTER_BY_FACE, true, false, R.string.people,
+                R.string.group_by_faces),
+        new ActionItem(FilterUtils.CLUSTER_BY_TAG, true, false, R.string.tags,
+                R.string.group_by_tags)
+    };
+
+    private class ClusterAdapter extends BaseAdapter {
+
+        @Override
+        public int getCount() {
+            return sClusterItems.length;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return sClusterItems[position];
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return sClusterItems[position].action;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.action_bar_text,
+                        parent, false);
+            }
+            TextView view = (TextView) convertView;
+            view.setText(sClusterItems[position].spinnerTitle);
+            return convertView;
+        }
+    }
+
+    private class AlbumModeAdapter extends BaseAdapter {
+        @Override
+        public int getCount() {
+            return mAlbumModes.length;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mAlbumModes[position];
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.action_bar_two_line_text,
+                        parent, false);
+            }
+            TwoLineListItem view = (TwoLineListItem) convertView;
+            view.getText1().setText(mActionBar.getTitle());
+            view.getText2().setText((CharSequence) getItem(position));
+            return convertView;
+        }
+
+        @Override
+        public View getDropDownView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.action_bar_text,
+                        parent, false);
+            }
+            TextView view = (TextView) convertView;
+            view.setText((CharSequence) getItem(position));
+            return convertView;
+        }
+    }
+
+    public static String getClusterByTypeString(Context context, int type) {
+        for (ActionItem item : sClusterItems) {
+            if (item.action == type) {
+                return context.getString(item.clusterBy);
+            }
+        }
+        return null;
+    }
+
+    public GalleryActionBar(AbstractGalleryActivity activity) {
+        mActionBar = activity.getActionBar();
+        mContext = activity.getAndroidContext();
+        mActivity = activity;
+        mInflater = ((Activity) mActivity).getLayoutInflater();
+        mCurrentIndex = 0;
+    }
+
+    private void createDialogData() {
+        ArrayList<CharSequence> titles = new ArrayList<CharSequence>();
+        mActions = new ArrayList<Integer>();
+        for (ActionItem item : sClusterItems) {
+            if (item.enabled && item.visible) {
+                titles.add(mContext.getString(item.dialogTitle));
+                mActions.add(item.action);
+            }
+        }
+        mTitles = new CharSequence[titles.size()];
+        titles.toArray(mTitles);
+    }
+
+    public int getHeight() {
+        return mActionBar != null ? mActionBar.getHeight() : 0;
+    }
+
+    public void setClusterItemEnabled(int id, boolean enabled) {
+        for (ActionItem item : sClusterItems) {
+            if (item.action == id) {
+                item.enabled = enabled;
+                return;
+            }
+        }
+    }
+
+    public void setClusterItemVisibility(int id, boolean visible) {
+        for (ActionItem item : sClusterItems) {
+            if (item.action == id) {
+                item.visible = visible;
+                return;
+            }
+        }
+    }
+
+    public int getClusterTypeAction() {
+        return sClusterItems[mCurrentIndex].action;
+    }
+
+    public void enableClusterMenu(int action, ClusterRunner runner) {
+        if (mActionBar != null) {
+            // Don't set cluster runner until action bar is ready.
+            mClusterRunner = null;
+            mActionBar.setListNavigationCallbacks(mAdapter, this);
+            mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+            setSelectedAction(action);
+            mClusterRunner = runner;
+        }
+    }
+
+    // The only use case not to hideMenu in this method is to ensure
+    // all elements disappear at the same time when exiting gallery.
+    // hideMenu should always be true in all other cases.
+    public void disableClusterMenu(boolean hideMenu) {
+        if (mActionBar != null) {
+            mClusterRunner = null;
+            if (hideMenu) {
+                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+            }
+        }
+    }
+
+    public void onConfigurationChanged() {
+        if (mActionBar != null && mAlbumModeListener != null) {
+            OnAlbumModeSelectedListener listener = mAlbumModeListener;
+            enableAlbumModeMenu(mLastAlbumModeSelected, listener);
+        }
+    }
+
+    public void enableAlbumModeMenu(int selected, OnAlbumModeSelectedListener listener) {
+        if (mActionBar != null) {
+            if (mAlbumModeAdapter == null) {
+                // Initialize the album mode options if they haven't been already
+                Resources res = mActivity.getResources();
+                mAlbumModes = new CharSequence[] {
+                        res.getString(R.string.switch_photo_filmstrip),
+                        res.getString(R.string.switch_photo_grid)};
+                mAlbumModeAdapter = new AlbumModeAdapter();
+            }
+            mAlbumModeListener = null;
+            mLastAlbumModeSelected = selected;
+            mActionBar.setListNavigationCallbacks(mAlbumModeAdapter, this);
+            mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+            mActionBar.setSelectedNavigationItem(selected);
+            mAlbumModeListener = listener;
+        }
+    }
+
+    public void disableAlbumModeMenu(boolean hideMenu) {
+        if (mActionBar != null) {
+            mAlbumModeListener = null;
+            if (hideMenu) {
+                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+            }
+        }
+    }
+
+    public void showClusterDialog(final ClusterRunner clusterRunner) {
+        createDialogData();
+        final ArrayList<Integer> actions = mActions;
+        new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems(
+                mTitles, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                // Need to lock rendering when operations invoked by system UI (main thread) are
+                // modifying slot data used in GL thread for rendering.
+                mActivity.getGLRoot().lockRenderThread();
+                try {
+                    clusterRunner.doCluster(actions.get(which).intValue());
+                } finally {
+                    mActivity.getGLRoot().unlockRenderThread();
+                }
+            }
+        }).create().show();
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void setHomeButtonEnabled(boolean enabled) {
+        if (mActionBar != null) mActionBar.setHomeButtonEnabled(enabled);
+    }
+
+    public void setDisplayOptions(boolean displayHomeAsUp, boolean showTitle) {
+        if (mActionBar == null) return;
+        int options = 0;
+        if (displayHomeAsUp) options |= ActionBar.DISPLAY_HOME_AS_UP;
+        if (showTitle) options |= ActionBar.DISPLAY_SHOW_TITLE;
+
+        mActionBar.setDisplayOptions(options,
+                ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
+        mActionBar.setHomeButtonEnabled(displayHomeAsUp);
+    }
+
+    public void setTitle(String title) {
+        if (mActionBar != null) mActionBar.setTitle(title);
+    }
+
+    public void setTitle(int titleId) {
+        if (mActionBar != null) {
+            mActionBar.setTitle(mContext.getString(titleId));
+        }
+    }
+
+    public void setSubtitle(String title) {
+        if (mActionBar != null) mActionBar.setSubtitle(title);
+    }
+
+    public void show() {
+        if (mActionBar != null) mActionBar.show();
+    }
+
+    public void hide() {
+        if (mActionBar != null) mActionBar.hide();
+    }
+
+    public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        if (mActionBar != null) mActionBar.addOnMenuVisibilityListener(listener);
+    }
+
+    public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        if (mActionBar != null) mActionBar.removeOnMenuVisibilityListener(listener);
+    }
+
+    public boolean setSelectedAction(int type) {
+        if (mActionBar == null) return false;
+
+        for (int i = 0, n = sClusterItems.length; i < n; i++) {
+            ActionItem item = sClusterItems[i];
+            if (item.action == type) {
+                mActionBar.setSelectedNavigationItem(i);
+                mCurrentIndex = i;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+        if (itemPosition != mCurrentIndex && mClusterRunner != null
+                || mAlbumModeListener != null) {
+            // Need to lock rendering when operations invoked by system UI (main thread) are
+            // modifying slot data used in GL thread for rendering.
+            mActivity.getGLRoot().lockRenderThread();
+            try {
+                if (mAlbumModeListener != null) {
+                    mAlbumModeListener.onAlbumModeSelected(itemPosition);
+                } else {
+                    mClusterRunner.doCluster(sClusterItems[itemPosition].action);
+                }
+            } finally {
+                mActivity.getGLRoot().unlockRenderThread();
+            }
+        }
+        return false;
+    }
+
+    private Menu mActionBarMenu;
+    private ShareActionProvider mSharePanoramaActionProvider;
+    private ShareActionProvider mShareActionProvider;
+    private Intent mSharePanoramaIntent;
+    private Intent mShareIntent;
+
+    public void createActionBarMenu(int menuRes, Menu menu) {
+        mActivity.getMenuInflater().inflate(menuRes, menu);
+        mActionBarMenu = menu;
+
+        MenuItem item = menu.findItem(R.id.action_share_panorama);
+        if (item != null) {
+            mSharePanoramaActionProvider = (ShareActionProvider)
+                item.getActionProvider();
+            mSharePanoramaActionProvider
+                .setShareHistoryFileName("panorama_share_history.xml");
+            mSharePanoramaActionProvider.setShareIntent(mSharePanoramaIntent);
+        }
+
+        item = menu.findItem(R.id.action_share);
+        if (item != null) {
+            mShareActionProvider = (ShareActionProvider)
+                item.getActionProvider();
+            mShareActionProvider
+                .setShareHistoryFileName("share_history.xml");
+            mShareActionProvider.setShareIntent(mShareIntent);
+        }
+    }
+
+    public Menu getMenu() {
+        return mActionBarMenu;
+    }
+
+    public void setShareIntents(Intent sharePanoramaIntent, Intent shareIntent,
+        ShareActionProvider.OnShareTargetSelectedListener onShareListener) {
+        mSharePanoramaIntent = sharePanoramaIntent;
+        if (mSharePanoramaActionProvider != null) {
+            mSharePanoramaActionProvider.setShareIntent(sharePanoramaIntent);
+        }
+        mShareIntent = shareIntent;
+        if (mShareActionProvider != null) {
+            mShareActionProvider.setShareIntent(shareIntent);
+            mShareActionProvider.setOnShareTargetSelectedListener(
+                onShareListener);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java
new file mode 100644
index 0000000..b56b8a8
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryApp.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
+public interface GalleryApp {
+    public DataManager getDataManager();
+
+    public StitchingProgressManager getStitchingProgressManager();
+    public ImageCacheService getImageCacheService();
+    public DownloadCache getDownloadCache();
+    public ThreadPool getThreadPool();
+
+    public Context getAndroidContext();
+    public Looper getMainLooper();
+    public ContentResolver getContentResolver();
+    public Resources getResources();
+}
diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java
new file mode 100644
index 0000000..2abdaa0
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.Application;
+import android.content.Context;
+import android.os.AsyncTask;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.gadget.WidgetUtils;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.UsageStatistics;
+import com.android.photos.data.MediaCache;
+
+import java.io.File;
+
+public class GalleryAppImpl extends Application implements GalleryApp {
+
+    private static final String DOWNLOAD_FOLDER = "download";
+    private static final long DOWNLOAD_CAPACITY = 64 * 1024 * 1024; // 64M
+
+    private ImageCacheService mImageCacheService;
+    private Object mLock = new Object();
+    private DataManager mDataManager;
+    private ThreadPool mThreadPool;
+    private DownloadCache mDownloadCache;
+    private StitchingProgressManager mStitchingProgressManager;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        com.android.camera.Util.initialize(this);
+        initializeAsyncTask();
+        GalleryUtils.initialize(this);
+        WidgetUtils.initialize(this);
+        PicasaSource.initialize(this);
+        UsageStatistics.initialize(this);
+        MediaCache.initialize(this);
+
+        mStitchingProgressManager = LightCycleHelper.createStitchingManagerInstance(this);
+        if (mStitchingProgressManager != null) {
+            mStitchingProgressManager.addChangeListener(getDataManager());
+        }
+    }
+
+    @Override
+    public Context getAndroidContext() {
+        return this;
+    }
+
+    @Override
+    public synchronized DataManager getDataManager() {
+        if (mDataManager == null) {
+            mDataManager = new DataManager(this);
+            mDataManager.initializeSourceMap();
+        }
+        return mDataManager;
+    }
+
+    @Override
+    public StitchingProgressManager getStitchingProgressManager() {
+        return mStitchingProgressManager;
+    }
+
+    @Override
+    public ImageCacheService getImageCacheService() {
+        // This method may block on file I/O so a dedicated lock is needed here.
+        synchronized (mLock) {
+            if (mImageCacheService == null) {
+                mImageCacheService = new ImageCacheService(getAndroidContext());
+            }
+            return mImageCacheService;
+        }
+    }
+
+    @Override
+    public synchronized ThreadPool getThreadPool() {
+        if (mThreadPool == null) {
+            mThreadPool = new ThreadPool();
+        }
+        return mThreadPool;
+    }
+
+    @Override
+    public synchronized DownloadCache getDownloadCache() {
+        if (mDownloadCache == null) {
+            File cacheDir = new File(getExternalCacheDir(), DOWNLOAD_FOLDER);
+
+            if (!cacheDir.isDirectory()) cacheDir.mkdirs();
+
+            if (!cacheDir.isDirectory()) {
+                throw new RuntimeException(
+                        "fail to create: " + cacheDir.getAbsolutePath());
+            }
+            mDownloadCache = new DownloadCache(this, cacheDir, DOWNLOAD_CAPACITY);
+        }
+        return mDownloadCache;
+    }
+
+    private void initializeAsyncTask() {
+        // AsyncTask class needs to be loaded in UI thread.
+        // So we load it here to comply the rule.
+        try {
+            Class.forName(AsyncTask.class.getName());
+        } catch (ClassNotFoundException e) {
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java
new file mode 100644
index 0000000..06f4fe4
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryContext.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.util.ThreadPool;
+
+public interface GalleryContext {
+    public DataManager getDataManager();
+
+    public Context getAndroidContext();
+
+    public Looper getMainLooper();
+    public Resources getResources();
+    public ThreadPool getThreadPool();
+}
diff --git a/src/com/android/gallery3d/app/LoadingListener.java b/src/com/android/gallery3d/app/LoadingListener.java
new file mode 100644
index 0000000..e94df93
--- /dev/null
+++ b/src/com/android/gallery3d/app/LoadingListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+public interface LoadingListener {
+    public void onLoadingStarted();
+    /**
+     * Called when loading is complete or no further progress can be made.
+     *
+     * @param loadingFailed true if data source cannot provide requested data
+     */
+    public void onLoadingFinished(boolean loadingFailed);
+}
diff --git a/src/com/android/gallery3d/app/Log.java b/src/com/android/gallery3d/app/Log.java
new file mode 100644
index 0000000..07a8ea5
--- /dev/null
+++ b/src/com/android/gallery3d/app/Log.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+public class Log {
+    public static int v(String tag, String msg) {
+        return android.util.Log.v(tag, msg);
+    }
+    public static int v(String tag, String msg, Throwable tr) {
+        return android.util.Log.v(tag, msg, tr);
+    }
+    public static int d(String tag, String msg) {
+        return android.util.Log.d(tag, msg);
+    }
+    public static int d(String tag, String msg, Throwable tr) {
+        return android.util.Log.d(tag, msg, tr);
+    }
+    public static int i(String tag, String msg) {
+        return android.util.Log.i(tag, msg);
+    }
+    public static int i(String tag, String msg, Throwable tr) {
+        return android.util.Log.i(tag, msg, tr);
+    }
+    public static int w(String tag, String msg) {
+        return android.util.Log.w(tag, msg);
+    }
+    public static int w(String tag, String msg, Throwable tr) {
+        return android.util.Log.w(tag, msg, tr);
+    }
+    public static int w(String tag, Throwable tr) {
+        return android.util.Log.w(tag, tr);
+    }
+    public static int e(String tag, String msg) {
+        return android.util.Log.e(tag, msg);
+    }
+    public static int e(String tag, String msg, Throwable tr) {
+        return android.util.Log.e(tag, msg, tr);
+    }
+}
diff --git a/src/com/android/gallery3d/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java
new file mode 100644
index 0000000..4f5c358
--- /dev/null
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.CacheStorageUsageInfo;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ManageCacheDrawer;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+
+public class ManageCachePage extends ActivityState implements
+        SelectionManager.SelectionListener, MenuExecutor.ProgressListener,
+        EyePosition.EyePositionListener, OnClickListener {
+    public static final String KEY_MEDIA_PATH = "media-path";
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "ManageCachePage";
+
+    private static final int DATA_CACHE_SIZE = 256;
+    private static final int MSG_REFRESH_STORAGE_INFO = 1;
+    private static final int MSG_REQUEST_LAYOUT = 2;
+    private static final int PROGRESS_BAR_MAX = 10000;
+
+    private SlotView mSlotView;
+    private MediaSet mMediaSet;
+
+    protected SelectionManager mSelectionManager;
+    protected ManageCacheDrawer mSelectionDrawer;
+    private AlbumSetDataLoader mAlbumSetDataAdapter;
+
+    private EyePosition mEyePosition;
+
+    // The eyes' position of the user, the origin is at the center of the
+    // device and the unit is in pixels.
+    private float mX;
+    private float mY;
+    private float mZ;
+
+    private int mAlbumCountToMakeAvailableOffline;
+    private View mFooterContent;
+    private CacheStorageUsageInfo mCacheStorageInfo;
+    private Future<Void> mUpdateStorageInfo;
+    private Handler mHandler;
+    private boolean mLayoutReady = false;
+
+    @Override
+    protected int getBackgroundColorId() {
+        return R.color.cache_background;
+    }
+
+    private GLView mRootPane = new GLView() {
+        private float mMatrix[] = new float[16];
+
+        @Override
+        protected void renderBackground(GLCanvas view) {
+            view.clearBuffer(getBackgroundColor());
+        }
+
+        @Override
+        protected void onLayout(
+                boolean changed, int left, int top, int right, int bottom) {
+            // Hack: our layout depends on other components on the screen.
+            // We assume the other components will complete before we get a change
+            // to run a message in main thread.
+            if (!mLayoutReady) {
+                mHandler.sendEmptyMessage(MSG_REQUEST_LAYOUT);
+                return;
+            }
+            mLayoutReady = false;
+
+            mEyePosition.resetPosition();
+            int slotViewTop = mActivity.getGalleryActionBar().getHeight();
+            int slotViewBottom = bottom - top;
+
+            View footer = mActivity.findViewById(R.id.footer);
+            if (footer != null) {
+                int location[] = {0, 0};
+                footer.getLocationOnScreen(location);
+                slotViewBottom = location[1];
+            }
+
+            mSlotView.layout(0, slotViewTop, right - left, slotViewBottom);
+        }
+
+        @Override
+        protected void render(GLCanvas canvas) {
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            GalleryUtils.setViewPointMatrix(mMatrix,
+                        getWidth() / 2 + mX, getHeight() / 2 + mY, mZ);
+            canvas.multiplyMatrix(mMatrix, 0);
+            super.render(canvas);
+            canvas.restore();
+        }
+    };
+
+    @Override
+    public void onEyePositionChanged(float x, float y, float z) {
+        mRootPane.lockRendering();
+        mX = x;
+        mY = y;
+        mZ = z;
+        mRootPane.unlockRendering();
+        mRootPane.invalidate();
+    }
+
+    private void onDown(int index) {
+        mSelectionDrawer.setPressedIndex(index);
+    }
+
+    private void onUp() {
+        mSelectionDrawer.setPressedIndex(-1);
+    }
+
+    public void onSingleTapUp(int slotIndex) {
+        MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+        if (targetSet == null) return; // Content is dirty, we shall reload soon
+
+        // ignore selection action if the target set does not support cache
+        // operation (like a local album).
+        if ((targetSet.getSupportedOperations()
+                & MediaSet.SUPPORT_CACHE) == 0) {
+            showToastForLocalAlbum();
+            return;
+        }
+
+        Path path = targetSet.getPath();
+        boolean isFullyCached =
+                (targetSet.getCacheFlag() == MediaObject.CACHE_FLAG_FULL);
+        boolean isSelected = mSelectionManager.isItemSelected(path);
+
+        if (!isFullyCached) {
+            // We only count the media sets that will be made available offline
+            // in this session.
+            if (isSelected) {
+                --mAlbumCountToMakeAvailableOffline;
+            } else {
+                ++mAlbumCountToMakeAvailableOffline;
+            }
+        }
+
+        long sizeOfTarget = targetSet.getCacheSize();
+        mCacheStorageInfo.increaseTargetCacheSize(
+                (isFullyCached ^ isSelected) ? -sizeOfTarget : sizeOfTarget);
+        refreshCacheStorageInfo();
+
+        mSelectionManager.toggle(path);
+        mSlotView.invalidate();
+    }
+
+    @Override
+    public void onCreate(Bundle data, Bundle restoreState) {
+        super.onCreate(data, restoreState);
+        mCacheStorageInfo = new CacheStorageUsageInfo(mActivity);
+        initializeViews();
+        initializeData(data);
+        mEyePosition = new EyePosition(mActivity.getAndroidContext(), this);
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_REFRESH_STORAGE_INFO:
+                        refreshCacheStorageInfo();
+                        break;
+                    case MSG_REQUEST_LAYOUT: {
+                        mLayoutReady = true;
+                        removeMessages(MSG_REQUEST_LAYOUT);
+                        mRootPane.requestLayout();
+                        break;
+                    }
+                }
+            }
+        };
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        // We use different layout resources for different configs
+        initializeFooterViews();
+        FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+        if (layout.getVisibility() == View.VISIBLE) {
+            layout.removeAllViews();
+            layout.addView(mFooterContent);
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mAlbumSetDataAdapter.pause();
+        mSelectionDrawer.pause();
+        mEyePosition.pause();
+
+        if (mUpdateStorageInfo != null) {
+            mUpdateStorageInfo.cancel();
+            mUpdateStorageInfo = null;
+        }
+        mHandler.removeMessages(MSG_REFRESH_STORAGE_INFO);
+
+        FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+        layout.removeAllViews();
+        layout.setVisibility(View.INVISIBLE);
+    }
+
+    private Job<Void> mUpdateStorageInfoJob = new Job<Void>() {
+        @Override
+        public Void run(JobContext jc) {
+            mCacheStorageInfo.loadStorageInfo(jc);
+            if (!jc.isCancelled()) {
+                mHandler.sendEmptyMessage(MSG_REFRESH_STORAGE_INFO);
+            }
+            return null;
+        }
+    };
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        setContentPane(mRootPane);
+        mAlbumSetDataAdapter.resume();
+        mSelectionDrawer.resume();
+        mEyePosition.resume();
+        mUpdateStorageInfo = mActivity.getThreadPool().submit(mUpdateStorageInfoJob);
+        FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+        layout.addView(mFooterContent);
+        layout.setVisibility(View.VISIBLE);
+    }
+
+    private void initializeData(Bundle data) {
+        String mediaPath = data.getString(ManageCachePage.KEY_MEDIA_PATH);
+        mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+        mSelectionManager.setSourceMediaSet(mMediaSet);
+
+        // We will always be in selection mode in this page.
+        mSelectionManager.setAutoLeaveSelectionMode(false);
+        mSelectionManager.enterSelectionMode();
+
+        mAlbumSetDataAdapter = new AlbumSetDataLoader(
+                mActivity, mMediaSet, DATA_CACHE_SIZE);
+        mSelectionDrawer.setModel(mAlbumSetDataAdapter);
+    }
+
+    private void initializeViews() {
+        Activity activity = mActivity;
+
+        mSelectionManager = new SelectionManager(mActivity, true);
+        mSelectionManager.setSelectionListener(this);
+
+        Config.ManageCachePage config = Config.ManageCachePage.get(activity);
+        mSlotView = new SlotView(mActivity, config.slotViewSpec);
+        mSelectionDrawer = new ManageCacheDrawer(mActivity, mSelectionManager, mSlotView,
+                config.labelSpec, config.cachePinSize, config.cachePinMargin);
+        mSlotView.setSlotRenderer(mSelectionDrawer);
+        mSlotView.setListener(new SlotView.SimpleListener() {
+            @Override
+            public void onDown(int index) {
+                ManageCachePage.this.onDown(index);
+            }
+
+            @Override
+            public void onUp(boolean followedByLongPress) {
+                ManageCachePage.this.onUp();
+            }
+
+            @Override
+            public void onSingleTapUp(int slotIndex) {
+                ManageCachePage.this.onSingleTapUp(slotIndex);
+            }
+        });
+        mRootPane.addComponent(mSlotView);
+        initializeFooterViews();
+    }
+
+    private void initializeFooterViews() {
+        Activity activity = mActivity;
+
+        LayoutInflater inflater = activity.getLayoutInflater();
+        mFooterContent = inflater.inflate(R.layout.manage_offline_bar, null);
+
+        mFooterContent.findViewById(R.id.done).setOnClickListener(this);
+        refreshCacheStorageInfo();
+    }
+
+    @Override
+    public void onClick(View view) {
+        Utils.assertTrue(view.getId() == R.id.done);
+        GLRoot root = mActivity.getGLRoot();
+        root.lockRenderThread();
+        try {
+            ArrayList<Path> ids = mSelectionManager.getSelected(false);
+            if (ids.size() == 0) {
+                onBackPressed();
+                return;
+            }
+            showToast();
+
+            MenuExecutor menuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+            menuExecutor.startAction(R.id.action_toggle_full_caching,
+                    R.string.process_caching_requests, this);
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    private void showToast() {
+        if (mAlbumCountToMakeAvailableOffline > 0) {
+            Activity activity = mActivity;
+            Toast.makeText(activity, activity.getResources().getQuantityString(
+                    R.plurals.make_albums_available_offline,
+                    mAlbumCountToMakeAvailableOffline),
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void showToastForLocalAlbum() {
+        Activity activity = mActivity;
+        Toast.makeText(activity, activity.getResources().getString(
+            R.string.try_to_set_local_album_available_offline),
+            Toast.LENGTH_SHORT).show();
+    }
+
+    private void refreshCacheStorageInfo() {
+        ProgressBar progressBar = (ProgressBar) mFooterContent.findViewById(R.id.progress);
+        TextView status = (TextView) mFooterContent.findViewById(R.id.status);
+        progressBar.setMax(PROGRESS_BAR_MAX);
+        long totalBytes = mCacheStorageInfo.getTotalBytes();
+        long usedBytes = mCacheStorageInfo.getUsedBytes();
+        long expectedBytes = mCacheStorageInfo.getExpectedUsedBytes();
+        long freeBytes = mCacheStorageInfo.getFreeBytes();
+
+        Activity activity = mActivity;
+        if (totalBytes == 0) {
+            progressBar.setProgress(0);
+            progressBar.setSecondaryProgress(0);
+
+            // TODO: get the string translated
+            String label = activity.getString(R.string.free_space_format, "-");
+            status.setText(label);
+        } else {
+            progressBar.setProgress((int) (usedBytes * PROGRESS_BAR_MAX / totalBytes));
+            progressBar.setSecondaryProgress(
+                    (int) (expectedBytes * PROGRESS_BAR_MAX / totalBytes));
+            String label = activity.getString(R.string.free_space_format,
+                    Formatter.formatFileSize(activity, freeBytes));
+            status.setText(label);
+        }
+    }
+
+    @Override
+    public void onProgressComplete(int result) {
+        onBackPressed();
+    }
+
+    @Override
+    public void onProgressUpdate(int index) {
+    }
+
+    @Override
+    public void onSelectionModeChange(int mode) {
+    }
+
+    @Override
+    public void onSelectionChange(Path path, boolean selected) {
+    }
+
+    @Override
+    public void onConfirmDialogDismissed(boolean confirmed) {
+    }
+
+    @Override
+    public void onConfirmDialogShown() {
+    }
+
+    @Override
+    public void onProgressStart() {
+    }
+}
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
new file mode 100644
index 0000000..40edbbe
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieActivity.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.gallery3d.app;
+
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ShareActionProvider;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+
+/**
+ * This activity plays a video from a specified URI.
+ *
+ * The client of this activity can pass a logo bitmap in the intent (KEY_LOGO_BITMAP)
+ * to set the action bar logo so the playback process looks more seamlessly integrated with
+ * the original activity.
+ */
+public class MovieActivity extends Activity {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MovieActivity";
+    public static final String KEY_LOGO_BITMAP = "logo-bitmap";
+    public static final String KEY_TREAT_UP_AS_BACK = "treat-up-as-back";
+
+    private MoviePlayer mPlayer;
+    private boolean mFinishOnCompletion;
+    private Uri mUri;
+    private boolean mTreatUpAsBack;
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private void setSystemUiVisibility(View rootView) {
+        if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
+            rootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        requestWindowFeature(Window.FEATURE_ACTION_BAR);
+        requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+        setContentView(R.layout.movie_view);
+        View rootView = findViewById(R.id.movie_view_root);
+
+        setSystemUiVisibility(rootView);
+
+        Intent intent = getIntent();
+        initializeActionBar(intent);
+        mFinishOnCompletion = intent.getBooleanExtra(
+                MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+        mTreatUpAsBack = intent.getBooleanExtra(KEY_TREAT_UP_AS_BACK, false);
+        mPlayer = new MoviePlayer(rootView, this, intent.getData(), savedInstanceState,
+                !mFinishOnCompletion) {
+            @Override
+            public void onCompletion() {
+                if (mFinishOnCompletion) {
+                    finish();
+                }
+            }
+        };
+        if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) {
+            int orientation = intent.getIntExtra(
+                    MediaStore.EXTRA_SCREEN_ORIENTATION,
+                    ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+            if (orientation != getRequestedOrientation()) {
+                setRequestedOrientation(orientation);
+            }
+        }
+        Window win = getWindow();
+        WindowManager.LayoutParams winParams = win.getAttributes();
+        winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
+        winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
+        win.setAttributes(winParams);
+
+        // We set the background in the theme to have the launching animation.
+        // But for the performance (and battery), we remove the background here.
+        win.setBackgroundDrawable(null);
+    }
+
+    private void setActionBarLogoFromIntent(Intent intent) {
+        Bitmap logo = intent.getParcelableExtra(KEY_LOGO_BITMAP);
+        if (logo != null) {
+            getActionBar().setLogo(
+                    new BitmapDrawable(getResources(), logo));
+        }
+    }
+
+    private void initializeActionBar(Intent intent) {
+        mUri = intent.getData();
+        final ActionBar actionBar = getActionBar();
+        if (actionBar == null) {
+            return;
+        }
+        setActionBarLogoFromIntent(intent);
+        actionBar.setDisplayOptions(
+                ActionBar.DISPLAY_HOME_AS_UP,
+                ActionBar.DISPLAY_HOME_AS_UP);
+
+        String title = intent.getStringExtra(Intent.EXTRA_TITLE);
+        if (title != null) {
+            actionBar.setTitle(title);
+        } else {
+            // Displays the filename as title, reading the filename from the
+            // interface: {@link android.provider.OpenableColumns#DISPLAY_NAME}.
+            AsyncQueryHandler queryHandler =
+                    new AsyncQueryHandler(getContentResolver()) {
+                @Override
+                protected void onQueryComplete(int token, Object cookie,
+                        Cursor cursor) {
+                    try {
+                        if ((cursor != null) && cursor.moveToFirst()) {
+                            String displayName = cursor.getString(0);
+
+                            // Just show empty title if other apps don't set
+                            // DISPLAY_NAME
+                            actionBar.setTitle((displayName == null) ? "" :
+                                    displayName);
+                        }
+                    } finally {
+                        Utils.closeSilently(cursor);
+                    }
+                }
+            };
+            queryHandler.startQuery(0, null, mUri,
+                    new String[] {OpenableColumns.DISPLAY_NAME}, null, null,
+                    null);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.movie, menu);
+
+        // Document says EXTRA_STREAM should be a content: Uri
+        // So, we only share the video if it's "content:".
+        MenuItem shareItem = menu.findItem(R.id.action_share);
+        if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme())) {
+            shareItem.setVisible(true);
+            ((ShareActionProvider) shareItem.getActionProvider())
+                    .setShareIntent(createShareIntent());
+        } else {
+            shareItem.setVisible(false);
+        }
+        return true;
+    }
+
+    private Intent createShareIntent() {
+        Intent intent = new Intent(Intent.ACTION_SEND);
+        intent.setType("video/*");
+        intent.putExtra(Intent.EXTRA_STREAM, mUri);
+        return intent;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        int id = item.getItemId();
+        if (id == android.R.id.home) {
+            if (mTreatUpAsBack) {
+                finish();
+            } else {
+                startActivity(new Intent(this, Gallery.class));
+                finish();
+            }
+            return true;
+        } else if (id == R.id.action_share) {
+            startActivity(Intent.createChooser(createShareIntent(),
+                    getString(R.string.share)));
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onStart() {
+        ((AudioManager) getSystemService(AUDIO_SERVICE))
+                .requestAudioFocus(null, AudioManager.STREAM_MUSIC,
+                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        super.onStart();
+    }
+
+    @Override
+    protected void onStop() {
+        ((AudioManager) getSystemService(AUDIO_SERVICE))
+                .abandonAudioFocus(null);
+        super.onStop();
+    }
+
+    @Override
+    public void onPause() {
+        mPlayer.onPause();
+        super.onPause();
+    }
+
+    @Override
+    public void onResume() {
+        mPlayer.onResume();
+        super.onResume();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        mPlayer.onSaveInstanceState(outState);
+    }
+
+    @Override
+    public void onDestroy() {
+        mPlayer.onDestroy();
+        super.onDestroy();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return mPlayer.onKeyDown(keyCode, event)
+                || super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return mPlayer.onKeyUp(keyCode, event)
+                || super.onKeyUp(keyCode, event);
+    }
+}
diff --git a/src/com/android/gallery3d/app/MovieControllerOverlay.java b/src/com/android/gallery3d/app/MovieControllerOverlay.java
new file mode 100644
index 0000000..f01e619
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieControllerOverlay.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.app;
+
+import android.content.Context;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import com.android.gallery3d.R;
+
+/**
+ * The playback controller for the Movie Player.
+ */
+public class MovieControllerOverlay extends CommonControllerOverlay implements
+        AnimationListener {
+
+    private boolean hidden;
+
+    private final Handler handler;
+    private final Runnable startHidingRunnable;
+    private final Animation hideAnimation;
+
+    public MovieControllerOverlay(Context context) {
+        super(context);
+
+        handler = new Handler();
+        startHidingRunnable = new Runnable() {
+                @Override
+            public void run() {
+                startHiding();
+            }
+        };
+
+        hideAnimation = AnimationUtils.loadAnimation(context, R.anim.player_out);
+        hideAnimation.setAnimationListener(this);
+
+        hide();
+    }
+
+    @Override
+    protected void createTimeBar(Context context) {
+        mTimeBar = new TimeBar(context, this);
+    }
+
+    @Override
+    public void hide() {
+        boolean wasHidden = hidden;
+        hidden = true;
+        super.hide();
+        if (mListener != null && wasHidden != hidden) {
+            mListener.onHidden();
+        }
+    }
+
+
+    @Override
+    public void show() {
+        boolean wasHidden = hidden;
+        hidden = false;
+        super.show();
+        if (mListener != null && wasHidden != hidden) {
+            mListener.onShown();
+        }
+        maybeStartHiding();
+    }
+
+    private void maybeStartHiding() {
+        cancelHiding();
+        if (mState == State.PLAYING) {
+            handler.postDelayed(startHidingRunnable, 2500);
+        }
+    }
+
+    private void startHiding() {
+        startHideAnimation(mBackground);
+        startHideAnimation(mTimeBar);
+        startHideAnimation(mPlayPauseReplayView);
+    }
+
+    private void startHideAnimation(View view) {
+        if (view.getVisibility() == View.VISIBLE) {
+            view.startAnimation(hideAnimation);
+        }
+    }
+
+    private void cancelHiding() {
+        handler.removeCallbacks(startHidingRunnable);
+        mBackground.setAnimation(null);
+        mTimeBar.setAnimation(null);
+        mPlayPauseReplayView.setAnimation(null);
+    }
+
+    @Override
+    public void onAnimationStart(Animation animation) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onAnimationRepeat(Animation animation) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onAnimationEnd(Animation animation) {
+        hide();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (hidden) {
+            show();
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (super.onTouchEvent(event)) {
+            return true;
+        }
+
+        if (hidden) {
+            show();
+            return true;
+        }
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                cancelHiding();
+                if (mState == State.PLAYING || mState == State.PAUSED) {
+                    mListener.onPlayPause();
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                maybeStartHiding();
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    protected void updateViews() {
+        if (hidden) {
+            return;
+        }
+        super.updateViews();
+    }
+
+    // TimeBar listener
+
+    @Override
+    public void onScrubbingStart() {
+        cancelHiding();
+        super.onScrubbingStart();
+    }
+
+    @Override
+    public void onScrubbingMove(int time) {
+        cancelHiding();
+        super.onScrubbingMove(time);
+    }
+
+    @Override
+    public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) {
+        maybeStartHiding();
+        super.onScrubbingEnd(time, trimStartTime, trimEndTime);
+    }
+}
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
new file mode 100644
index 0000000..ce91834
--- /dev/null
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -0,0 +1,525 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.VideoView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+
+public class MoviePlayer implements
+        MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
+        ControllerOverlay.Listener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MoviePlayer";
+
+    private static final String KEY_VIDEO_POSITION = "video-position";
+    private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
+
+    // These are constants in KeyEvent, appearing on API level 11.
+    private static final int KEYCODE_MEDIA_PLAY = 126;
+    private static final int KEYCODE_MEDIA_PAUSE = 127;
+
+    // Copied from MediaPlaybackService in the Music Player app.
+    private static final String SERVICECMD = "com.android.music.musicservicecommand";
+    private static final String CMDNAME = "command";
+    private static final String CMDPAUSE = "pause";
+
+    private static final long BLACK_TIMEOUT = 500;
+
+    // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
+    // Otherwise, we pause the player.
+    private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
+
+    private Context mContext;
+    private final VideoView mVideoView;
+    private final View mRootView;
+    private final Bookmarker mBookmarker;
+    private final Uri mUri;
+    private final Handler mHandler = new Handler();
+    private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
+    private final MovieControllerOverlay mController;
+
+    private long mResumeableTime = Long.MAX_VALUE;
+    private int mVideoPosition = 0;
+    private boolean mHasPaused = false;
+    private int mLastSystemUiVis = 0;
+
+    // If the time bar is being dragged.
+    private boolean mDragging;
+
+    // If the time bar is visible.
+    private boolean mShowing;
+
+    private final Runnable mPlayingChecker = new Runnable() {
+        @Override
+        public void run() {
+            if (mVideoView.isPlaying()) {
+                mController.showPlaying();
+            } else {
+                mHandler.postDelayed(mPlayingChecker, 250);
+            }
+        }
+    };
+
+    private final Runnable mProgressChecker = new Runnable() {
+        @Override
+        public void run() {
+            int pos = setProgress();
+            mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000));
+        }
+    };
+
+    public MoviePlayer(View rootView, final MovieActivity movieActivity,
+            Uri videoUri, Bundle savedInstance, boolean canReplay) {
+        mContext = movieActivity.getApplicationContext();
+        mRootView = rootView;
+        mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+        mBookmarker = new Bookmarker(movieActivity);
+        mUri = videoUri;
+
+        mController = new MovieControllerOverlay(mContext);
+        ((ViewGroup)rootView).addView(mController.getView());
+        mController.setListener(this);
+        mController.setCanReplay(canReplay);
+
+        mVideoView.setOnErrorListener(this);
+        mVideoView.setOnCompletionListener(this);
+        mVideoView.setVideoURI(mUri);
+        mVideoView.setOnTouchListener(new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                mController.show();
+                return true;
+            }
+        });
+        mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+            @Override
+            public void onPrepared(MediaPlayer player) {
+                if (!mVideoView.canSeekForward() || !mVideoView.canSeekBackward()) {
+                    mController.setSeekable(false);
+                } else {
+                    mController.setSeekable(true);
+                }
+                setProgress();
+            }
+        });
+
+        // The SurfaceView is transparent before drawing the first frame.
+        // This makes the UI flashing when open a video. (black -> old screen
+        // -> video) However, we have no way to know the timing of the first
+        // frame. So, we hide the VideoView for a while to make sure the
+        // video has been drawn on it.
+        mVideoView.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                mVideoView.setVisibility(View.VISIBLE);
+            }
+        }, BLACK_TIMEOUT);
+
+        setOnSystemUiVisibilityChangeListener();
+        // Hide system UI by default
+        showSystemUi(false);
+
+        mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
+        mAudioBecomingNoisyReceiver.register();
+
+        Intent i = new Intent(SERVICECMD);
+        i.putExtra(CMDNAME, CMDPAUSE);
+        movieActivity.sendBroadcast(i);
+
+        if (savedInstance != null) { // this is a resumed activity
+            mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
+            mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
+            mVideoView.start();
+            mVideoView.suspend();
+            mHasPaused = true;
+        } else {
+            final Integer bookmark = mBookmarker.getBookmark(mUri);
+            if (bookmark != null) {
+                showResumeDialog(movieActivity, bookmark);
+            } else {
+                startVideo();
+            }
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private void setOnSystemUiVisibilityChangeListener() {
+        if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION) return;
+
+        // When the user touches the screen or uses some hard key, the framework
+        // will change system ui visibility from invisible to visible. We show
+        // the media control and enable system UI (e.g. ActionBar) to be visible at this point
+        mVideoView.setOnSystemUiVisibilityChangeListener(
+                new View.OnSystemUiVisibilityChangeListener() {
+            @Override
+            public void onSystemUiVisibilityChange(int visibility) {
+                int diff = mLastSystemUiVis ^ visibility;
+                mLastSystemUiVis = visibility;
+                if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+                        && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+                    mController.show();
+                }
+            }
+        });
+    }
+
+    @SuppressWarnings("deprecation")
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private void showSystemUi(boolean visible) {
+        if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) return;
+
+        int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+        if (!visible) {
+            // We used the deprecated "STATUS_BAR_HIDDEN" for unbundling
+            flag |= View.STATUS_BAR_HIDDEN | View.SYSTEM_UI_FLAG_FULLSCREEN
+                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+        }
+        mVideoView.setSystemUiVisibility(flag);
+    }
+
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+        outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
+    }
+
+    private void showResumeDialog(Context context, final int bookmark) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setTitle(R.string.resume_playing_title);
+        builder.setMessage(String.format(
+                context.getString(R.string.resume_playing_message),
+                GalleryUtils.formatDuration(context, bookmark / 1000)));
+        builder.setOnCancelListener(new OnCancelListener() {
+            @Override
+            public void onCancel(DialogInterface dialog) {
+                onCompletion();
+            }
+        });
+        builder.setPositiveButton(
+                R.string.resume_playing_resume, new OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                mVideoView.seekTo(bookmark);
+                startVideo();
+            }
+        });
+        builder.setNegativeButton(
+                R.string.resume_playing_restart, new OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                startVideo();
+            }
+        });
+        builder.show();
+    }
+
+    public void onPause() {
+        mHasPaused = true;
+        mHandler.removeCallbacksAndMessages(null);
+        mVideoPosition = mVideoView.getCurrentPosition();
+        mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
+        mVideoView.suspend();
+        mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
+    }
+
+    public void onResume() {
+        if (mHasPaused) {
+            mVideoView.seekTo(mVideoPosition);
+            mVideoView.resume();
+
+            // If we have slept for too long, pause the play
+            if (System.currentTimeMillis() > mResumeableTime) {
+                pauseVideo();
+            }
+        }
+        mHandler.post(mProgressChecker);
+    }
+
+    public void onDestroy() {
+        mVideoView.stopPlayback();
+        mAudioBecomingNoisyReceiver.unregister();
+    }
+
+    // This updates the time bar display (if necessary). It is called every
+    // second by mProgressChecker and also from places where the time bar needs
+    // to be updated immediately.
+    private int setProgress() {
+        if (mDragging || !mShowing) {
+            return 0;
+        }
+        int position = mVideoView.getCurrentPosition();
+        int duration = mVideoView.getDuration();
+        mController.setTimes(position, duration, 0, 0);
+        return position;
+    }
+
+    private void startVideo() {
+        // For streams that we expect to be slow to start up, show a
+        // progress spinner until playback starts.
+        String scheme = mUri.getScheme();
+        if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
+            mController.showLoading();
+            mHandler.removeCallbacks(mPlayingChecker);
+            mHandler.postDelayed(mPlayingChecker, 250);
+        } else {
+            mController.showPlaying();
+            mController.hide();
+        }
+
+        mVideoView.start();
+        setProgress();
+    }
+
+    private void playVideo() {
+        mVideoView.start();
+        mController.showPlaying();
+        setProgress();
+    }
+
+    private void pauseVideo() {
+        mVideoView.pause();
+        mController.showPaused();
+    }
+
+    // Below are notifications from VideoView
+    @Override
+    public boolean onError(MediaPlayer player, int arg1, int arg2) {
+        mHandler.removeCallbacksAndMessages(null);
+        // VideoView will show an error dialog if we return false, so no need
+        // to show more message.
+        mController.showErrorMessage("");
+        return false;
+    }
+
+    @Override
+    public void onCompletion(MediaPlayer mp) {
+        mController.showEnded();
+        onCompletion();
+    }
+
+    public void onCompletion() {
+    }
+
+    // Below are notifications from ControllerOverlay
+    @Override
+    public void onPlayPause() {
+        if (mVideoView.isPlaying()) {
+            pauseVideo();
+        } else {
+            playVideo();
+        }
+    }
+
+    @Override
+    public void onSeekStart() {
+        mDragging = true;
+    }
+
+    @Override
+    public void onSeekMove(int time) {
+        mVideoView.seekTo(time);
+    }
+
+    @Override
+    public void onSeekEnd(int time, int start, int end) {
+        mDragging = false;
+        mVideoView.seekTo(time);
+        setProgress();
+    }
+
+    @Override
+    public void onShown() {
+        mShowing = true;
+        setProgress();
+        showSystemUi(true);
+    }
+
+    @Override
+    public void onHidden() {
+        mShowing = false;
+        showSystemUi(false);
+    }
+
+    @Override
+    public void onReplay() {
+        startVideo();
+    }
+
+    // Below are key events passed from MovieActivity.
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+
+        // Some headsets will fire off 7-10 events on a single click
+        if (event.getRepeatCount() > 0) {
+            return isMediaKey(keyCode);
+        }
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+                if (mVideoView.isPlaying()) {
+                    pauseVideo();
+                } else {
+                    playVideo();
+                }
+                return true;
+            case KEYCODE_MEDIA_PAUSE:
+                if (mVideoView.isPlaying()) {
+                    pauseVideo();
+                }
+                return true;
+            case KEYCODE_MEDIA_PLAY:
+                if (!mVideoView.isPlaying()) {
+                    playVideo();
+                }
+                return true;
+            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+            case KeyEvent.KEYCODE_MEDIA_NEXT:
+                // TODO: Handle next / previous accordingly, for now we're
+                // just consuming the events.
+                return true;
+        }
+        return false;
+    }
+
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return isMediaKey(keyCode);
+    }
+
+    private static boolean isMediaKey(int keyCode) {
+        return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS
+                || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE;
+    }
+
+    // We want to pause when the headset is unplugged.
+    private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
+
+        public void register() {
+            mContext.registerReceiver(this,
+                    new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+        }
+
+        public void unregister() {
+            mContext.unregisterReceiver(this);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mVideoView.isPlaying()) pauseVideo();
+        }
+    }
+}
+
+class Bookmarker {
+    private static final String TAG = "Bookmarker";
+
+    private static final String BOOKMARK_CACHE_FILE = "bookmark";
+    private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
+    private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
+    private static final int BOOKMARK_CACHE_VERSION = 1;
+
+    private static final int HALF_MINUTE = 30 * 1000;
+    private static final int TWO_MINUTES = 4 * HALF_MINUTE;
+
+    private final Context mContext;
+
+    public Bookmarker(Context context) {
+        mContext = context;
+    }
+
+    public void setBookmark(Uri uri, int bookmark, int duration) {
+        try {
+            BlobCache cache = CacheManager.getCache(mContext,
+                    BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+                    BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            DataOutputStream dos = new DataOutputStream(bos);
+            dos.writeUTF(uri.toString());
+            dos.writeInt(bookmark);
+            dos.writeInt(duration);
+            dos.flush();
+            cache.insert(uri.hashCode(), bos.toByteArray());
+        } catch (Throwable t) {
+            Log.w(TAG, "setBookmark failed", t);
+        }
+    }
+
+    public Integer getBookmark(Uri uri) {
+        try {
+            BlobCache cache = CacheManager.getCache(mContext,
+                    BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+                    BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+            byte[] data = cache.lookup(uri.hashCode());
+            if (data == null) return null;
+
+            DataInputStream dis = new DataInputStream(
+                    new ByteArrayInputStream(data));
+
+            String uriString = DataInputStream.readUTF(dis);
+            int bookmark = dis.readInt();
+            int duration = dis.readInt();
+
+            if (!uriString.equals(uri.toString())) {
+                return null;
+            }
+
+            if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
+                    || (bookmark > (duration - HALF_MINUTE))) {
+                return null;
+            }
+            return Integer.valueOf(bookmark);
+        } catch (Throwable t) {
+            Log.w(TAG, "getBookmark failed", t);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/gallery3d/app/MuteVideo.java b/src/com/android/gallery3d/app/MuteVideo.java
new file mode 100644
index 0000000..d3f3aa5
--- /dev/null
+++ b/src/com/android/gallery3d/app/MuteVideo.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.android.gallery3d.util.SaveVideoFileUtils;
+
+import java.io.IOException;
+
+public class MuteVideo {
+
+    private ProgressDialog mMuteProgress;
+
+    private String mFilePath = null;
+    private Uri mUri = null;
+    private SaveVideoFileInfo mDstFileInfo = null;
+    private Activity mActivity = null;
+    private final Handler mHandler = new Handler();
+
+    final String TIME_STAMP_NAME = "'MUTE'_yyyyMMdd_HHmmss";
+
+    public MuteVideo(String filePath, Uri uri, Activity activity) {
+        mUri = uri;
+        mFilePath = filePath;
+        mActivity = activity;
+    }
+
+    public void muteInBackground() {
+        mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME,
+                mActivity.getContentResolver(), mUri,
+                mActivity.getString(R.string.folder_download));
+
+        showProgressDialog();
+        new Thread(new Runnable() {
+                @Override
+            public void run() {
+                try {
+                    VideoUtils.startMute(mFilePath, mDstFileInfo);
+                    SaveVideoFileUtils.insertContent(
+                            mDstFileInfo, mActivity.getContentResolver(), mUri);
+                } catch (IOException e) {
+                    Toast.makeText(mActivity, mActivity.getString(R.string.video_mute_err),
+                            Toast.LENGTH_SHORT).show();
+                }
+                // After muting is done, trigger the UI changed.
+                mHandler.post(new Runnable() {
+                        @Override
+                    public void run() {
+                        Toast.makeText(mActivity.getApplicationContext(),
+                                mActivity.getString(R.string.save_into,
+                                        mDstFileInfo.mFolderName),
+                                Toast.LENGTH_SHORT)
+                                .show();
+
+                        if (mMuteProgress != null) {
+                            mMuteProgress.dismiss();
+                            mMuteProgress = null;
+
+                            // Show the result only when the activity not
+                            // stopped.
+                            Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
+                            intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*");
+                            intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
+                            mActivity.startActivity(intent);
+                        }
+                    }
+                });
+            }
+        }).start();
+    }
+
+    private void showProgressDialog() {
+        mMuteProgress = new ProgressDialog(mActivity);
+        mMuteProgress.setTitle(mActivity.getString(R.string.muting));
+        mMuteProgress.setMessage(mActivity.getString(R.string.please_wait));
+        mMuteProgress.setCancelable(false);
+        mMuteProgress.setCanceledOnTouchOutside(false);
+        mMuteProgress.show();
+    }
+}
diff --git a/src/com/android/gallery3d/app/NotificationIds.java b/src/com/android/gallery3d/app/NotificationIds.java
new file mode 100644
index 0000000..d697d85
--- /dev/null
+++ b/src/com/android/gallery3d/app/NotificationIds.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.app;
+
+public class NotificationIds {
+    public static final int INGEST_NOTIFICATION_SCANNING = 10;
+    public static final int INGEST_NOTIFICATION_IMPORTING = 11;
+}
diff --git a/src/com/android/gallery3d/app/OrientationManager.java b/src/com/android/gallery3d/app/OrientationManager.java
new file mode 100644
index 0000000..f2f632c
--- /dev/null
+++ b/src/com/android/gallery3d/app/OrientationManager.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.provider.Settings;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.ui.OrientationSource;
+
+public class OrientationManager implements OrientationSource {
+    private static final String TAG = "OrientationManager";
+
+    // Orientation hysteresis amount used in rounding, in degrees
+    private static final int ORIENTATION_HYSTERESIS = 5;
+
+    private Activity mActivity;
+    private MyOrientationEventListener mOrientationListener;
+    // If the framework orientation is locked.
+    private boolean mOrientationLocked = false;
+
+    // This is true if "Settings -> Display -> Rotation Lock" is checked. We
+    // don't allow the orientation to be unlocked if the value is true.
+    private boolean mRotationLockedSetting = false;
+
+    public OrientationManager(Activity activity) {
+        mActivity = activity;
+        mOrientationListener = new MyOrientationEventListener(activity);
+    }
+
+    public void resume() {
+        ContentResolver resolver = mActivity.getContentResolver();
+        mRotationLockedSetting = Settings.System.getInt(
+                resolver, Settings.System.ACCELEROMETER_ROTATION, 0) != 1;
+        mOrientationListener.enable();
+    }
+
+    public void pause() {
+        mOrientationListener.disable();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Orientation handling
+    //
+    //  We can choose to lock the framework orientation or not. If we lock the
+    //  framework orientation, we calculate a a compensation value according to
+    //  current device orientation and send it to listeners. If we don't lock
+    //  the framework orientation, we always set the compensation value to 0.
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Lock the framework orientation to the current device orientation
+    public void lockOrientation() {
+        if (mOrientationLocked) return;
+        mOrientationLocked = true;
+        if (ApiHelper.HAS_ORIENTATION_LOCK) {
+            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
+        } else {
+            mActivity.setRequestedOrientation(calculateCurrentScreenOrientation());
+        }
+    }
+
+    // Unlock the framework orientation, so it can change when the device
+    // rotates.
+    public void unlockOrientation() {
+        if (!mOrientationLocked) return;
+        mOrientationLocked = false;
+        Log.d(TAG, "unlock orientation");
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
+    }
+
+    private int calculateCurrentScreenOrientation() {
+        int displayRotation = getDisplayRotation();
+        // Display rotation >= 180 means we need to use the REVERSE landscape/portrait
+        boolean standard = displayRotation < 180;
+        if (mActivity.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE) {
+            return standard
+                    ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+                    : ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+        } else {
+            if (displayRotation == 90 || displayRotation == 270) {
+                // If displayRotation = 90 or 270 then we are on a landscape
+                // device. On landscape devices, portrait is a 90 degree
+                // clockwise rotation from landscape, so we need
+                // to flip which portrait we pick as display rotation is counter clockwise
+                standard = !standard;
+            }
+            return standard
+                    ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+                    : ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+        }
+    }
+
+    // This listens to the device orientation, so we can update the compensation.
+    private class MyOrientationEventListener extends OrientationEventListener {
+        public MyOrientationEventListener(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onOrientationChanged(int orientation) {
+            // We keep the last known orientation. So if the user first orient
+            // the camera then point the camera to floor or sky, we still have
+            // the correct orientation.
+            if (orientation == ORIENTATION_UNKNOWN) return;
+            orientation = roundOrientation(orientation, 0);
+        }
+    }
+
+    @Override
+    public int getDisplayRotation() {
+        return getDisplayRotation(mActivity);
+    }
+
+    @Override
+    public int getCompensation() {
+        return 0;
+    }
+
+    private static int roundOrientation(int orientation, int orientationHistory) {
+        boolean changeOrientation = false;
+        if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) {
+            changeOrientation = true;
+        } else {
+            int dist = Math.abs(orientation - orientationHistory);
+            dist = Math.min(dist, 360 - dist);
+            changeOrientation = (dist >= 45 + ORIENTATION_HYSTERESIS);
+        }
+        if (changeOrientation) {
+            return ((orientation + 45) / 90 * 90) % 360;
+        }
+        return orientationHistory;
+    }
+
+    private static int getDisplayRotation(Activity activity) {
+        int rotation = activity.getWindowManager().getDefaultDisplay()
+                .getRotation();
+        switch (rotation) {
+            case Surface.ROTATION_0: return 0;
+            case Surface.ROTATION_90: return 90;
+            case Surface.ROTATION_180: return 180;
+            case Surface.ROTATION_270: return 270;
+        }
+        return 0;
+    }
+}
diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java
new file mode 100644
index 0000000..9b2412f
--- /dev/null
+++ b/src/com/android/gallery3d/app/PackagesMonitor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.IntentService;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.LightCycleHelper;
+
+public class PackagesMonitor extends BroadcastReceiver {
+    public static final String KEY_PACKAGES_VERSION  = "packages-version";
+
+    public synchronized static int getPackagesVersion(Context context) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        return prefs.getInt(KEY_PACKAGES_VERSION, 1);
+    }
+
+    @Override
+    public void onReceive(final Context context, final Intent intent) {
+        intent.setClass(context, AsyncService.class);
+        context.startService(intent);
+    }
+
+    public static class AsyncService extends IntentService {
+        public AsyncService() {
+            super("GalleryPackagesMonitorAsync");
+        }
+
+        @Override
+        protected void onHandleIntent(Intent intent) {
+            onReceiveAsync(this, intent);
+        }
+    }
+
+    // Runs in a background thread.
+    private static void onReceiveAsync(Context context, Intent intent) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+        int version = prefs.getInt(KEY_PACKAGES_VERSION, 1);
+        prefs.edit().putInt(KEY_PACKAGES_VERSION, version + 1).commit();
+
+        String action = intent.getAction();
+        String packageName = intent.getData().getSchemeSpecificPart();
+        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+            PicasaSource.onPackageAdded(context, packageName);
+        } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+            PicasaSource.onPackageRemoved(context, packageName);
+        } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+            PicasaSource.onPackageChanged(context, packageName);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/PanoramaMetadataSupport.java b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
new file mode 100644
index 0000000..ba0c9e7
--- /dev/null
+++ b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.PanoramaMetadataJob;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+
+import java.util.ArrayList;
+
+/**
+ * This class breaks out the off-thread panorama support checks so that the
+ * complexity can be shared between UriImage and LocalImage, which need to
+ * support panoramas.
+ */
+public class PanoramaMetadataSupport implements FutureListener<PanoramaMetadata> {
+    private Object mLock = new Object();
+    private Future<PanoramaMetadata> mGetPanoMetadataTask;
+    private PanoramaMetadata mPanoramaMetadata;
+    private ArrayList<PanoramaSupportCallback> mCallbacksWaiting;
+    private MediaObject mMediaObject;
+
+    public PanoramaMetadataSupport(MediaObject mediaObject) {
+        mMediaObject = mediaObject;
+    }
+
+    public void getPanoramaSupport(GalleryApp app, PanoramaSupportCallback callback) {
+        synchronized (mLock) {
+            if (mPanoramaMetadata != null) {
+                callback.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer,
+                        mPanoramaMetadata.mIsPanorama360);
+            } else {
+                if (mCallbacksWaiting == null) {
+                    mCallbacksWaiting = new ArrayList<PanoramaSupportCallback>();
+                    mGetPanoMetadataTask = app.getThreadPool().submit(
+                            new PanoramaMetadataJob(app.getAndroidContext(),
+                                    mMediaObject.getContentUri()), this);
+
+                }
+                mCallbacksWaiting.add(callback);
+            }
+        }
+    }
+
+    public void clearCachedValues() {
+        synchronized (mLock) {
+            if (mPanoramaMetadata != null) {
+                mPanoramaMetadata = null;
+            } else if (mGetPanoMetadataTask != null) {
+                mGetPanoMetadataTask.cancel();
+                for (PanoramaSupportCallback cb : mCallbacksWaiting) {
+                    cb.panoramaInfoAvailable(mMediaObject, false, false);
+                }
+                mGetPanoMetadataTask = null;
+                mCallbacksWaiting = null;
+            }
+        }
+    }
+
+    @Override
+    public void onFutureDone(Future<PanoramaMetadata> future) {
+        synchronized (mLock) {
+            mPanoramaMetadata = future.get();
+            if (mPanoramaMetadata == null) {
+                // Error getting panorama data from file. Treat as not panorama.
+                mPanoramaMetadata = LightCycleHelper.NOT_PANORAMA;
+            }
+            for (PanoramaSupportCallback cb : mCallbacksWaiting) {
+                cb.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer,
+                        mPanoramaMetadata.mIsPanorama360);
+            }
+            mGetPanoMetadataTask = null;
+            mCallbacksWaiting = null;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
new file mode 100644
index 0000000..fd3a7cf
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -0,0 +1,1133 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.LocalMediaItem;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.ui.TiledScreenNail;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class PhotoDataAdapter implements PhotoPage.Model {
+    @SuppressWarnings("unused")
+    private static final String TAG = "PhotoDataAdapter";
+
+    private static final int MSG_LOAD_START = 1;
+    private static final int MSG_LOAD_FINISH = 2;
+    private static final int MSG_RUN_OBJECT = 3;
+    private static final int MSG_UPDATE_IMAGE_REQUESTS = 4;
+
+    private static final int MIN_LOAD_COUNT = 16;
+    private static final int DATA_CACHE_SIZE = 256;
+    private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
+    private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
+
+    private static final int BIT_SCREEN_NAIL = 1;
+    private static final int BIT_FULL_IMAGE = 2;
+
+    // sImageFetchSeq is the fetching sequence for images.
+    // We want to fetch the current screennail first (offset = 0), the next
+    // screennail (offset = +1), then the previous screennail (offset = -1) etc.
+    // After all the screennail are fetched, we fetch the full images (only some
+    // of them because of we don't want to use too much memory).
+    private static ImageFetch[] sImageFetchSeq;
+
+    private static class ImageFetch {
+        int indexOffset;
+        int imageBit;
+        public ImageFetch(int offset, int bit) {
+            indexOffset = offset;
+            imageBit = bit;
+        }
+    }
+
+    static {
+        int k = 0;
+        sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3];
+        sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL);
+
+        for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+            sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL);
+            sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL);
+        }
+
+        sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE);
+        sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE);
+        sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE);
+    }
+
+    private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter();
+
+    // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image).
+    //
+    // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE
+    // entries. The valid index range are [mContentStart, mContentEnd). We keep
+    // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use
+    // (i % DATA_CACHE_SIZE) as index to the array.
+    //
+    // The valid MediaItem window size (mContentEnd - mContentStart) may be
+    // smaller than DATA_CACHE_SIZE because we only update the window and reload
+    // the MediaItems when there are significant changes to the window position
+    // (>= MIN_LOAD_COUNT).
+    private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE];
+    private int mContentStart = 0;
+    private int mContentEnd = 0;
+
+    // The ImageCache is a Path-to-ImageEntry map. It only holds the
+    // ImageEntries in the range of [mActiveStart, mActiveEnd).  We also keep
+    // mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.  Besides, the
+    // [mActiveStart, mActiveEnd) range must be contained within
+    // the [mContentStart, mContentEnd) range.
+    private HashMap<Path, ImageEntry> mImageCache =
+            new HashMap<Path, ImageEntry>();
+    private int mActiveStart = 0;
+    private int mActiveEnd = 0;
+
+    // mCurrentIndex is the "center" image the user is viewing. The change of
+    // mCurrentIndex triggers the data loading and image loading.
+    private int mCurrentIndex;
+
+    // mChanges keeps the version number (of MediaItem) about the images. If any
+    // of the version number changes, we notify the view. This is used after a
+    // database reload or mCurrentIndex changes.
+    private final long mChanges[] = new long[IMAGE_CACHE_SIZE];
+    // mPaths keeps the corresponding Path (of MediaItem) for the images. This
+    // is used to determine the item movement.
+    private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE];
+
+    private final Handler mMainHandler;
+    private final ThreadPool mThreadPool;
+
+    private final PhotoView mPhotoView;
+    private final MediaSet mSource;
+    private ReloadTask mReloadTask;
+
+    private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+    private int mSize = 0;
+    private Path mItemPath;
+    private int mCameraIndex;
+    private boolean mIsPanorama;
+    private boolean mIsStaticCamera;
+    private boolean mIsActive;
+    private boolean mNeedFullImage;
+    private int mFocusHintDirection = FOCUS_HINT_NEXT;
+    private Path mFocusHintPath = null;
+
+    public interface DataListener extends LoadingListener {
+        public void onPhotoChanged(int index, Path item);
+    }
+
+    private DataListener mDataListener;
+
+    private final SourceListener mSourceListener = new SourceListener();
+    private final TiledTexture.Uploader mUploader;
+
+    // The path of the current viewing item will be stored in mItemPath.
+    // If mItemPath is not null, mCurrentIndex is only a hint for where we
+    // can find the item. If mItemPath is null, then we use the mCurrentIndex to
+    // find the image being viewed. cameraIndex is the index of the camera
+    // preview. If cameraIndex < 0, there is no camera preview.
+    public PhotoDataAdapter(AbstractGalleryActivity activity, PhotoView view,
+            MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex,
+            boolean isPanorama, boolean isStaticCamera) {
+        mSource = Utils.checkNotNull(mediaSet);
+        mPhotoView = Utils.checkNotNull(view);
+        mItemPath = Utils.checkNotNull(itemPath);
+        mCurrentIndex = indexHint;
+        mCameraIndex = cameraIndex;
+        mIsPanorama = isPanorama;
+        mIsStaticCamera = isStaticCamera;
+        mThreadPool = activity.getThreadPool();
+        mNeedFullImage = true;
+
+        Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
+
+        mUploader = new TiledTexture.Uploader(activity.getGLRoot());
+
+        mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+            @SuppressWarnings("unchecked")
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_RUN_OBJECT:
+                        ((Runnable) message.obj).run();
+                        return;
+                    case MSG_LOAD_START: {
+                        if (mDataListener != null) {
+                            mDataListener.onLoadingStarted();
+                        }
+                        return;
+                    }
+                    case MSG_LOAD_FINISH: {
+                        if (mDataListener != null) {
+                            mDataListener.onLoadingFinished(false);
+                        }
+                        return;
+                    }
+                    case MSG_UPDATE_IMAGE_REQUESTS: {
+                        updateImageRequests();
+                        return;
+                    }
+                    default: throw new AssertionError();
+                }
+            }
+        };
+
+        updateSlidingWindow();
+    }
+
+    private MediaItem getItemInternal(int index) {
+        if (index < 0 || index >= mSize) return null;
+        if (index >= mContentStart && index < mContentEnd) {
+            return mData[index % DATA_CACHE_SIZE];
+        }
+        return null;
+    }
+
+    private long getVersion(int index) {
+        MediaItem item = getItemInternal(index);
+        if (item == null) return MediaObject.INVALID_DATA_VERSION;
+        return item.getDataVersion();
+    }
+
+    private Path getPath(int index) {
+        MediaItem item = getItemInternal(index);
+        if (item == null) return null;
+        return item.getPath();
+    }
+
+    private void fireDataChange() {
+        // First check if data actually changed.
+        boolean changed = false;
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            long newVersion = getVersion(mCurrentIndex + i);
+            if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) {
+                mChanges[i + SCREEN_NAIL_MAX] = newVersion;
+                changed = true;
+            }
+        }
+
+        if (!changed) return;
+
+        // Now calculate the fromIndex array. fromIndex represents the item
+        // movement. It records the index where the picture come from. The
+        // special value Integer.MAX_VALUE means it's a new picture.
+        final int N = IMAGE_CACHE_SIZE;
+        int fromIndex[] = new int[N];
+
+        // Remember the old path array.
+        Path oldPaths[] = new Path[N];
+        System.arraycopy(mPaths, 0, oldPaths, 0, N);
+
+        // Update the mPaths array.
+        for (int i = 0; i < N; ++i) {
+            mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX);
+        }
+
+        // Calculate the fromIndex array.
+        for (int i = 0; i < N; i++) {
+            Path p = mPaths[i];
+            if (p == null) {
+                fromIndex[i] = Integer.MAX_VALUE;
+                continue;
+            }
+
+            // Try to find the same path in the old array
+            int j;
+            for (j = 0; j < N; j++) {
+                if (oldPaths[j] == p) {
+                    break;
+                }
+            }
+            fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
+        }
+
+        mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex,
+                mSize - 1 - mCurrentIndex);
+    }
+
+    public void setDataListener(DataListener listener) {
+        mDataListener = listener;
+    }
+
+    private void updateScreenNail(Path path, Future<ScreenNail> future) {
+        ImageEntry entry = mImageCache.get(path);
+        ScreenNail screenNail = future.get();
+
+        if (entry == null || entry.screenNailTask != future) {
+            if (screenNail != null) screenNail.recycle();
+            return;
+        }
+
+        entry.screenNailTask = null;
+
+        // Combine the ScreenNails if we already have a BitmapScreenNail
+        if (entry.screenNail instanceof TiledScreenNail) {
+            TiledScreenNail original = (TiledScreenNail) entry.screenNail;
+            screenNail = original.combine(screenNail);
+        }
+
+        if (screenNail == null) {
+            entry.failToLoad = true;
+        } else {
+            entry.failToLoad = false;
+            entry.screenNail = screenNail;
+        }
+
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            if (path == getPath(mCurrentIndex + i)) {
+                if (i == 0) updateTileProvider(entry);
+                mPhotoView.notifyImageChange(i);
+                break;
+            }
+        }
+        updateImageRequests();
+        updateScreenNailUploadQueue();
+    }
+
+    private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
+        ImageEntry entry = mImageCache.get(path);
+        if (entry == null || entry.fullImageTask != future) {
+            BitmapRegionDecoder fullImage = future.get();
+            if (fullImage != null) fullImage.recycle();
+            return;
+        }
+
+        entry.fullImageTask = null;
+        entry.fullImage = future.get();
+        if (entry.fullImage != null) {
+            if (path == getPath(mCurrentIndex)) {
+                updateTileProvider(entry);
+                mPhotoView.notifyImageChange(0);
+            }
+        }
+        updateImageRequests();
+    }
+
+    @Override
+    public void resume() {
+        mIsActive = true;
+        TiledTexture.prepareResources();
+
+        mSource.addContentListener(mSourceListener);
+        updateImageCache();
+        updateImageRequests();
+
+        mReloadTask = new ReloadTask();
+        mReloadTask.start();
+
+        fireDataChange();
+    }
+
+    @Override
+    public void pause() {
+        mIsActive = false;
+
+        mReloadTask.terminate();
+        mReloadTask = null;
+
+        mSource.removeContentListener(mSourceListener);
+
+        for (ImageEntry entry : mImageCache.values()) {
+            if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+            if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+            if (entry.screenNail != null) entry.screenNail.recycle();
+        }
+        mImageCache.clear();
+        mTileProvider.clear();
+
+        mUploader.clear();
+        TiledTexture.freeResources();
+    }
+
+    private MediaItem getItem(int index) {
+        if (index < 0 || index >= mSize || !mIsActive) return null;
+        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+        if (index >= mContentStart && index < mContentEnd) {
+            return mData[index % DATA_CACHE_SIZE];
+        }
+        return null;
+    }
+
+    private void updateCurrentIndex(int index) {
+        if (mCurrentIndex == index) return;
+        mCurrentIndex = index;
+        updateSlidingWindow();
+
+        MediaItem item = mData[index % DATA_CACHE_SIZE];
+        mItemPath = item == null ? null : item.getPath();
+
+        updateImageCache();
+        updateImageRequests();
+        updateTileProvider();
+
+        if (mDataListener != null) {
+            mDataListener.onPhotoChanged(index, mItemPath);
+        }
+
+        fireDataChange();
+    }
+
+    private void uploadScreenNail(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index < mActiveStart || index >= mActiveEnd) return;
+
+        MediaItem item = getItem(index);
+        if (item == null) return;
+
+        ImageEntry e = mImageCache.get(item.getPath());
+        if (e == null) return;
+
+        ScreenNail s = e.screenNail;
+        if (s instanceof TiledScreenNail) {
+            TiledTexture t = ((TiledScreenNail) s).getTexture();
+            if (t != null && !t.isReady()) mUploader.addTexture(t);
+        }
+    }
+
+    private void updateScreenNailUploadQueue() {
+        mUploader.clear();
+        uploadScreenNail(0);
+        for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+            uploadScreenNail(i);
+            uploadScreenNail(-i);
+        }
+    }
+
+    @Override
+    public void moveTo(int index) {
+        updateCurrentIndex(index);
+    }
+
+    @Override
+    public ScreenNail getScreenNail(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index < 0 || index >= mSize || !mIsActive) return null;
+        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+        MediaItem item = getItem(index);
+        if (item == null) return null;
+
+        ImageEntry entry = mImageCache.get(item.getPath());
+        if (entry == null) return null;
+
+        // Create a default ScreenNail if the real one is not available yet,
+        // except for camera that a black screen is better than a gray tile.
+        if (entry.screenNail == null && !isCamera(offset)) {
+            entry.screenNail = newPlaceholderScreenNail(item);
+            if (offset == 0) updateTileProvider(entry);
+        }
+
+        return entry.screenNail;
+    }
+
+    @Override
+    public void getImageSize(int offset, PhotoView.Size size) {
+        MediaItem item = getItem(mCurrentIndex + offset);
+        if (item == null) {
+            size.width = 0;
+            size.height = 0;
+        } else {
+            size.width = item.getWidth();
+            size.height = item.getHeight();
+        }
+    }
+
+    @Override
+    public int getImageRotation(int offset) {
+        MediaItem item = getItem(mCurrentIndex + offset);
+        return (item == null) ? 0 : item.getFullImageRotation();
+    }
+
+    @Override
+    public void setNeedFullImage(boolean enabled) {
+        mNeedFullImage = enabled;
+        mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS);
+    }
+
+    @Override
+    public boolean isCamera(int offset) {
+        return mCurrentIndex + offset == mCameraIndex;
+    }
+
+    @Override
+    public boolean isPanorama(int offset) {
+        return isCamera(offset) && mIsPanorama;
+    }
+
+    @Override
+    public boolean isStaticCamera(int offset) {
+        return isCamera(offset) && mIsStaticCamera;
+    }
+
+    @Override
+    public boolean isVideo(int offset) {
+        MediaItem item = getItem(mCurrentIndex + offset);
+        return (item == null)
+                ? false
+                : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+    }
+
+    @Override
+    public boolean isDeletable(int offset) {
+        MediaItem item = getItem(mCurrentIndex + offset);
+        return (item == null)
+                ? false
+                : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+    }
+
+    @Override
+    public int getLoadingState(int offset) {
+        ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset));
+        if (entry == null) return LOADING_INIT;
+        if (entry.failToLoad) return LOADING_FAIL;
+        if (entry.screenNail != null) return LOADING_COMPLETE;
+        return LOADING_INIT;
+    }
+
+    @Override
+    public ScreenNail getScreenNail() {
+        return getScreenNail(0);
+    }
+
+    @Override
+    public int getImageHeight() {
+        return mTileProvider.getImageHeight();
+    }
+
+    @Override
+    public int getImageWidth() {
+        return mTileProvider.getImageWidth();
+    }
+
+    @Override
+    public int getLevelCount() {
+        return mTileProvider.getLevelCount();
+    }
+
+    @Override
+    public Bitmap getTile(int level, int x, int y, int tileSize) {
+        return mTileProvider.getTile(level, x, y, tileSize);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return mSize == 0;
+    }
+
+    @Override
+    public int getCurrentIndex() {
+        return mCurrentIndex;
+    }
+
+    @Override
+    public MediaItem getMediaItem(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index >= mContentStart && index < mContentEnd) {
+            return mData[index % DATA_CACHE_SIZE];
+        }
+        return null;
+    }
+
+    @Override
+    public void setCurrentPhoto(Path path, int indexHint) {
+        if (mItemPath == path) return;
+        mItemPath = path;
+        mCurrentIndex = indexHint;
+        updateSlidingWindow();
+        updateImageCache();
+        fireDataChange();
+
+        // We need to reload content if the path doesn't match.
+        MediaItem item = getMediaItem(0);
+        if (item != null && item.getPath() != path) {
+            if (mReloadTask != null) mReloadTask.notifyDirty();
+        }
+    }
+
+    @Override
+    public void setFocusHintDirection(int direction) {
+        mFocusHintDirection = direction;
+    }
+
+    @Override
+    public void setFocusHintPath(Path path) {
+        mFocusHintPath = path;
+    }
+
+    private void updateTileProvider() {
+        ImageEntry entry = mImageCache.get(getPath(mCurrentIndex));
+        if (entry == null) { // in loading
+            mTileProvider.clear();
+        } else {
+            updateTileProvider(entry);
+        }
+    }
+
+    private void updateTileProvider(ImageEntry entry) {
+        ScreenNail screenNail = entry.screenNail;
+        BitmapRegionDecoder fullImage = entry.fullImage;
+        if (screenNail != null) {
+            if (fullImage != null) {
+                mTileProvider.setScreenNail(screenNail,
+                        fullImage.getWidth(), fullImage.getHeight());
+                mTileProvider.setRegionDecoder(fullImage);
+            } else {
+                int width = screenNail.getWidth();
+                int height = screenNail.getHeight();
+                mTileProvider.setScreenNail(screenNail, width, height);
+            }
+        } else {
+            mTileProvider.clear();
+        }
+    }
+
+    private void updateSlidingWindow() {
+        // 1. Update the image window
+        int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2,
+                0, Math.max(0, mSize - IMAGE_CACHE_SIZE));
+        int end = Math.min(mSize, start + IMAGE_CACHE_SIZE);
+
+        if (mActiveStart == start && mActiveEnd == end) return;
+
+        mActiveStart = start;
+        mActiveEnd = end;
+
+        // 2. Update the data window
+        start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2,
+                0, Math.max(0, mSize - DATA_CACHE_SIZE));
+        end = Math.min(mSize, start + DATA_CACHE_SIZE);
+        if (mContentStart > mActiveStart || mContentEnd < mActiveEnd
+                || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) {
+            for (int i = mContentStart; i < mContentEnd; ++i) {
+                if (i < start || i >= end) {
+                    mData[i % DATA_CACHE_SIZE] = null;
+                }
+            }
+            mContentStart = start;
+            mContentEnd = end;
+            if (mReloadTask != null) mReloadTask.notifyDirty();
+        }
+    }
+
+    private void updateImageRequests() {
+        if (!mIsActive) return;
+
+        int currentIndex = mCurrentIndex;
+        MediaItem item = mData[currentIndex % DATA_CACHE_SIZE];
+        if (item == null || item.getPath() != mItemPath) {
+            // current item mismatch - don't request image
+            return;
+        }
+
+        // 1. Find the most wanted request and start it (if not already started).
+        Future<?> task = null;
+        for (int i = 0; i < sImageFetchSeq.length; i++) {
+            int offset = sImageFetchSeq[i].indexOffset;
+            int bit = sImageFetchSeq[i].imageBit;
+            if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue;
+            task = startTaskIfNeeded(currentIndex + offset, bit);
+            if (task != null) break;
+        }
+
+        // 2. Cancel everything else.
+        for (ImageEntry entry : mImageCache.values()) {
+            if (entry.screenNailTask != null && entry.screenNailTask != task) {
+                entry.screenNailTask.cancel();
+                entry.screenNailTask = null;
+                entry.requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+            }
+            if (entry.fullImageTask != null && entry.fullImageTask != task) {
+                entry.fullImageTask.cancel();
+                entry.fullImageTask = null;
+                entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+            }
+        }
+    }
+
+    private class ScreenNailJob implements Job<ScreenNail> {
+        private MediaItem mItem;
+
+        public ScreenNailJob(MediaItem item) {
+            mItem = item;
+        }
+
+        @Override
+        public ScreenNail run(JobContext jc) {
+            // We try to get a ScreenNail first, if it fails, we fallback to get
+            // a Bitmap and then wrap it in a BitmapScreenNail instead.
+            ScreenNail s = mItem.getScreenNail();
+            if (s != null) return s;
+
+            // If this is a temporary item, don't try to get its bitmap because
+            // it won't be available. We will get its bitmap after a data reload.
+            if (isTemporaryItem(mItem)) {
+                return newPlaceholderScreenNail(mItem);
+            }
+
+            Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
+            if (jc.isCancelled()) return null;
+            if (bitmap != null) {
+                bitmap = BitmapUtils.rotateBitmap(bitmap,
+                    mItem.getRotation() - mItem.getFullImageRotation(), true);
+            }
+            return bitmap == null ? null : new TiledScreenNail(bitmap);
+        }
+    }
+
+    private class FullImageJob implements Job<BitmapRegionDecoder> {
+        private MediaItem mItem;
+
+        public FullImageJob(MediaItem item) {
+            mItem = item;
+        }
+
+        @Override
+        public BitmapRegionDecoder run(JobContext jc) {
+            if (isTemporaryItem(mItem)) {
+                return null;
+            }
+            return mItem.requestLargeImage().run(jc);
+        }
+    }
+
+    // Returns true if we think this is a temporary item created by Camera. A
+    // temporary item is an image or a video whose data is still being
+    // processed, but an incomplete entry is created first in MediaProvider, so
+    // we can display them (in grey tile) even if they are not saved to disk
+    // yet. When the image or video data is actually saved, we will get
+    // notification from MediaProvider, reload data, and show the actual image
+    // or video data.
+    private boolean isTemporaryItem(MediaItem mediaItem) {
+        // Must have camera to create a temporary item.
+        if (mCameraIndex < 0) return false;
+        // Must be an item in camera roll.
+        if (!(mediaItem instanceof LocalMediaItem)) return false;
+        LocalMediaItem item = (LocalMediaItem) mediaItem;
+        if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false;
+        // Must have no size, but must have width and height information
+        if (item.getSize() != 0) return false;
+        if (item.getWidth() == 0) return false;
+        if (item.getHeight() == 0) return false;
+        // Must be created in the last 10 seconds.
+        if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false;
+        return true;
+    }
+
+    // Create a default ScreenNail when a ScreenNail is needed, but we don't yet
+    // have one available (because the image data is still being saved, or the
+    // Bitmap is still being loaded.
+    private ScreenNail newPlaceholderScreenNail(MediaItem item) {
+        int width = item.getWidth();
+        int height = item.getHeight();
+        return new TiledScreenNail(width, height);
+    }
+
+    // Returns the task if we started the task or the task is already started.
+    private Future<?> startTaskIfNeeded(int index, int which) {
+        if (index < mActiveStart || index >= mActiveEnd) return null;
+
+        ImageEntry entry = mImageCache.get(getPath(index));
+        if (entry == null) return null;
+        MediaItem item = mData[index % DATA_CACHE_SIZE];
+        Utils.assertTrue(item != null);
+        long version = item.getDataVersion();
+
+        if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null
+                && entry.requestedScreenNail == version) {
+            return entry.screenNailTask;
+        } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null
+                && entry.requestedFullImage == version) {
+            return entry.fullImageTask;
+        }
+
+        if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) {
+            entry.requestedScreenNail = version;
+            entry.screenNailTask = mThreadPool.submit(
+                    new ScreenNailJob(item),
+                    new ScreenNailListener(item));
+            // request screen nail
+            return entry.screenNailTask;
+        }
+        if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version
+                && (item.getSupportedOperations()
+                & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
+            entry.requestedFullImage = version;
+            entry.fullImageTask = mThreadPool.submit(
+                    new FullImageJob(item),
+                    new FullImageListener(item));
+            // request full image
+            return entry.fullImageTask;
+        }
+        return null;
+    }
+
+    private void updateImageCache() {
+        HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet());
+        for (int i = mActiveStart; i < mActiveEnd; ++i) {
+            MediaItem item = mData[i % DATA_CACHE_SIZE];
+            if (item == null) continue;
+            Path path = item.getPath();
+            ImageEntry entry = mImageCache.get(path);
+            toBeRemoved.remove(path);
+            if (entry != null) {
+                if (Math.abs(i - mCurrentIndex) > 1) {
+                    if (entry.fullImageTask != null) {
+                        entry.fullImageTask.cancel();
+                        entry.fullImageTask = null;
+                    }
+                    entry.fullImage = null;
+                    entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+                }
+                if (entry.requestedScreenNail != item.getDataVersion()) {
+                    // This ScreenNail is outdated, we want to update it if it's
+                    // still a placeholder.
+                    if (entry.screenNail instanceof TiledScreenNail) {
+                        TiledScreenNail s = (TiledScreenNail) entry.screenNail;
+                        s.updatePlaceholderSize(
+                                item.getWidth(), item.getHeight());
+                    }
+                }
+            } else {
+                entry = new ImageEntry();
+                mImageCache.put(path, entry);
+            }
+        }
+
+        // Clear the data and requests for ImageEntries outside the new window.
+        for (Path path : toBeRemoved) {
+            ImageEntry entry = mImageCache.remove(path);
+            if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+            if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+            if (entry.screenNail != null) entry.screenNail.recycle();
+        }
+
+        updateScreenNailUploadQueue();
+    }
+
+    private class FullImageListener
+            implements Runnable, FutureListener<BitmapRegionDecoder> {
+        private final Path mPath;
+        private Future<BitmapRegionDecoder> mFuture;
+
+        public FullImageListener(MediaItem item) {
+            mPath = item.getPath();
+        }
+
+        @Override
+        public void onFutureDone(Future<BitmapRegionDecoder> future) {
+            mFuture = future;
+            mMainHandler.sendMessage(
+                    mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+        }
+
+        @Override
+        public void run() {
+            updateFullImage(mPath, mFuture);
+        }
+    }
+
+    private class ScreenNailListener
+            implements Runnable, FutureListener<ScreenNail> {
+        private final Path mPath;
+        private Future<ScreenNail> mFuture;
+
+        public ScreenNailListener(MediaItem item) {
+            mPath = item.getPath();
+        }
+
+        @Override
+        public void onFutureDone(Future<ScreenNail> future) {
+            mFuture = future;
+            mMainHandler.sendMessage(
+                    mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+        }
+
+        @Override
+        public void run() {
+            updateScreenNail(mPath, mFuture);
+        }
+    }
+
+    private static class ImageEntry {
+        public BitmapRegionDecoder fullImage;
+        public ScreenNail screenNail;
+        public Future<ScreenNail> screenNailTask;
+        public Future<BitmapRegionDecoder> fullImageTask;
+        public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+        public long requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+        public boolean failToLoad = false;
+    }
+
+    private class SourceListener implements ContentListener {
+        @Override
+        public void onContentDirty() {
+            if (mReloadTask != null) mReloadTask.notifyDirty();
+        }
+    }
+
+    private <T> T executeAndWait(Callable<T> callable) {
+        FutureTask<T> task = new FutureTask<T>(callable);
+        mMainHandler.sendMessage(
+                mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+        try {
+            return task.get();
+        } catch (InterruptedException e) {
+            return null;
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class UpdateInfo {
+        public long version;
+        public boolean reloadContent;
+        public Path target;
+        public int indexHint;
+        public int contentStart;
+        public int contentEnd;
+
+        public int size;
+        public ArrayList<MediaItem> items;
+    }
+
+    private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+        private boolean needContentReload() {
+            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+                if (mData[i % DATA_CACHE_SIZE] == null) return true;
+            }
+            MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+            return current == null || current.getPath() != mItemPath;
+        }
+
+        @Override
+        public UpdateInfo call() throws Exception {
+            // TODO: Try to load some data in first update
+            UpdateInfo info = new UpdateInfo();
+            info.version = mSourceVersion;
+            info.reloadContent = needContentReload();
+            info.target = mItemPath;
+            info.indexHint = mCurrentIndex;
+            info.contentStart = mContentStart;
+            info.contentEnd = mContentEnd;
+            info.size = mSize;
+            return info;
+        }
+    }
+
+    private class UpdateContent implements Callable<Void> {
+        UpdateInfo mUpdateInfo;
+
+        public UpdateContent(UpdateInfo updateInfo) {
+            mUpdateInfo = updateInfo;
+        }
+
+        @Override
+        public Void call() throws Exception {
+            UpdateInfo info = mUpdateInfo;
+            mSourceVersion = info.version;
+
+            if (info.size != mSize) {
+                mSize = info.size;
+                if (mContentEnd > mSize) mContentEnd = mSize;
+                if (mActiveEnd > mSize) mActiveEnd = mSize;
+            }
+
+            mCurrentIndex = info.indexHint;
+            updateSlidingWindow();
+
+            if (info.items != null) {
+                int start = Math.max(info.contentStart, mContentStart);
+                int end = Math.min(info.contentStart + info.items.size(), mContentEnd);
+                int dataIndex = start % DATA_CACHE_SIZE;
+                for (int i = start; i < end; ++i) {
+                    mData[dataIndex] = info.items.get(i - info.contentStart);
+                    if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0;
+                }
+            }
+
+            // update mItemPath
+            MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+            mItemPath = current == null ? null : current.getPath();
+
+            updateImageCache();
+            updateTileProvider();
+            updateImageRequests();
+
+            if (mDataListener != null) {
+                mDataListener.onPhotoChanged(mCurrentIndex, mItemPath);
+            }
+
+            fireDataChange();
+            return null;
+        }
+    }
+
+    private class ReloadTask extends Thread {
+        private volatile boolean mActive = true;
+        private volatile boolean mDirty = true;
+
+        private boolean mIsLoading = false;
+
+        private void updateLoading(boolean loading) {
+            if (mIsLoading == loading) return;
+            mIsLoading = loading;
+            mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+        }
+
+        @Override
+        public void run() {
+            while (mActive) {
+                synchronized (this) {
+                    if (!mDirty && mActive) {
+                        updateLoading(false);
+                        Utils.waitWithoutInterrupt(this);
+                        continue;
+                    }
+                }
+                mDirty = false;
+                UpdateInfo info = executeAndWait(new GetUpdateInfo());
+                updateLoading(true);
+                long version = mSource.reload();
+                if (info.version != version) {
+                    info.reloadContent = true;
+                    info.size = mSource.getMediaItemCount();
+                }
+                if (!info.reloadContent) continue;
+                info.items = mSource.getMediaItem(
+                        info.contentStart, info.contentEnd);
+
+                int index = MediaSet.INDEX_NOT_FOUND;
+
+                // First try to focus on the given hint path if there is one.
+                if (mFocusHintPath != null) {
+                    index = findIndexOfPathInCache(info, mFocusHintPath);
+                    mFocusHintPath = null;
+                }
+
+                // Otherwise try to see if the currently focused item can be found.
+                if (index == MediaSet.INDEX_NOT_FOUND) {
+                    MediaItem item = findCurrentMediaItem(info);
+                    if (item != null && item.getPath() == info.target) {
+                        index = info.indexHint;
+                    } else {
+                        index = findIndexOfTarget(info);
+                    }
+                }
+
+                // The image has been deleted. Focus on the next image (keep
+                // mCurrentIndex unchanged) or the previous image (decrease
+                // mCurrentIndex by 1). In page mode we want to see the next
+                // image, so we focus on the next one. In film mode we want the
+                // later images to shift left to fill the empty space, so we
+                // focus on the previous image (so it will not move). In any
+                // case the index needs to be limited to [0, mSize).
+                if (index == MediaSet.INDEX_NOT_FOUND) {
+                    index = info.indexHint;
+                    int focusHintDirection = mFocusHintDirection;
+                    if (index == (mCameraIndex + 1)) {
+                        focusHintDirection = FOCUS_HINT_NEXT;
+                    }
+                    if (focusHintDirection == FOCUS_HINT_PREVIOUS
+                            && index > 0) {
+                        index--;
+                    }
+                }
+
+                // Don't change index if mSize == 0
+                if (mSize > 0) {
+                    if (index >= mSize) index = mSize - 1;
+                }
+
+                info.indexHint = index;
+
+                executeAndWait(new UpdateContent(info));
+            }
+        }
+
+        public synchronized void notifyDirty() {
+            mDirty = true;
+            notifyAll();
+        }
+
+        public synchronized void terminate() {
+            mActive = false;
+            notifyAll();
+        }
+
+        private MediaItem findCurrentMediaItem(UpdateInfo info) {
+            ArrayList<MediaItem> items = info.items;
+            int index = info.indexHint - info.contentStart;
+            return index < 0 || index >= items.size() ? null : items.get(index);
+        }
+
+        private int findIndexOfTarget(UpdateInfo info) {
+            if (info.target == null) return info.indexHint;
+            ArrayList<MediaItem> items = info.items;
+
+            // First, try to find the item in the data just loaded
+            if (items != null) {
+                int i = findIndexOfPathInCache(info, info.target);
+                if (i != MediaSet.INDEX_NOT_FOUND) return i;
+            }
+
+            // Not found, find it in mSource.
+            return mSource.getIndexOfItem(info.target, info.indexHint);
+        }
+
+        private int findIndexOfPathInCache(UpdateInfo info, Path path) {
+            ArrayList<MediaItem> items = info.items;
+            for (int i = 0, n = items.size(); i < n; ++i) {
+                MediaItem item = items.get(i);
+                if (item != null && item.getPath() == path) {
+                    return i + info.contentStart;
+                }
+            }
+            return MediaSet.INDEX_NOT_FOUND;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
new file mode 100644
index 0000000..7a71e91
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -0,0 +1,1571 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.annotation.TargetApi;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateBeamUrisCallback;
+import android.nfc.NfcEvent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.RelativeLayout;
+import android.widget.ShareActionProvider;
+import android.widget.Toast;
+
+import com.android.camera.CameraActivity;
+import com.android.camera.ProxyLauncher;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.ComboAlbum;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.FilterDeleteSet;
+import com.android.gallery3d.data.FilterSource;
+import com.android.gallery3d.data.LocalImage;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.data.SecureAlbum;
+import com.android.gallery3d.data.SecureSource;
+import com.android.gallery3d.data.SnailAlbum;
+import com.android.gallery3d.data.SnailItem;
+import com.android.gallery3d.data.SnailSource;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.UsageStatistics;
+
+public abstract class PhotoPage extends ActivityState implements
+        PhotoView.Listener, AppBridge.Server, ShareActionProvider.OnShareTargetSelectedListener,
+        PhotoPageBottomControls.Delegate, GalleryActionBar.OnAlbumModeSelectedListener {
+    private static final String TAG = "PhotoPage";
+
+    private static final int MSG_HIDE_BARS = 1;
+    private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
+    private static final int MSG_UPDATE_ACTION_BAR = 5;
+    private static final int MSG_UNFREEZE_GLROOT = 6;
+    private static final int MSG_WANT_BARS = 7;
+    private static final int MSG_REFRESH_BOTTOM_CONTROLS = 8;
+    private static final int MSG_ON_CAMERA_CENTER = 9;
+    private static final int MSG_ON_PICTURE_CENTER = 10;
+    private static final int MSG_REFRESH_IMAGE = 11;
+    private static final int MSG_UPDATE_PHOTO_UI = 12;
+    private static final int MSG_UPDATE_PROGRESS = 13;
+    private static final int MSG_UPDATE_DEFERRED = 14;
+    private static final int MSG_UPDATE_SHARE_URI = 15;
+    private static final int MSG_UPDATE_PANORAMA_UI = 16;
+
+    private static final int HIDE_BARS_TIMEOUT = 3500;
+    private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
+
+    private static final int REQUEST_SLIDESHOW = 1;
+    private static final int REQUEST_CROP = 2;
+    private static final int REQUEST_CROP_PICASA = 3;
+    private static final int REQUEST_EDIT = 4;
+    private static final int REQUEST_PLAY_VIDEO = 5;
+    private static final int REQUEST_TRIM = 6;
+
+    public static final String KEY_MEDIA_SET_PATH = "media-set-path";
+    public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
+    public static final String KEY_INDEX_HINT = "index-hint";
+    public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
+    public static final String KEY_APP_BRIDGE = "app-bridge";
+    public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
+    public static final String KEY_START_IN_FILMSTRIP = "start-in-filmstrip";
+    public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
+    public static final String KEY_SHOW_WHEN_LOCKED = "show_when_locked";
+    public static final String KEY_IN_CAMERA_ROLL = "in_camera_roll";
+
+    public static final String KEY_ALBUMPAGE_TRANSITION = "albumpage-transition";
+    public static final int MSG_ALBUMPAGE_NONE = 0;
+    public static final int MSG_ALBUMPAGE_STARTED = 1;
+    public static final int MSG_ALBUMPAGE_RESUMED = 2;
+    public static final int MSG_ALBUMPAGE_PICKED = 4;
+
+    public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit";
+    public static final String ACTION_SIMPLE_EDIT = "action_simple_edit";
+
+    private GalleryApp mApplication;
+    private SelectionManager mSelectionManager;
+
+    private PhotoView mPhotoView;
+    private PhotoPage.Model mModel;
+    private DetailsHelper mDetailsHelper;
+    private boolean mShowDetails;
+
+    // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
+    // E.g., viewing a photo in gmail attachment
+    private FilterDeleteSet mMediaSet;
+
+    // The mediaset used by camera launched from secure lock screen.
+    private SecureAlbum mSecureAlbum;
+
+    private int mCurrentIndex = 0;
+    private Handler mHandler;
+    private boolean mShowBars = true;
+    private volatile boolean mActionBarAllowed = true;
+    private GalleryActionBar mActionBar;
+    private boolean mIsMenuVisible;
+    private boolean mHaveImageEditor;
+    private PhotoPageBottomControls mBottomControls;
+    private PhotoPageProgressBar mProgressBar;
+    private MediaItem mCurrentPhoto = null;
+    private MenuExecutor mMenuExecutor;
+    private boolean mIsActive;
+    private boolean mShowSpinner;
+    private String mSetPathString;
+    // This is the original mSetPathString before adding the camera preview item.
+    private String mOriginalSetPathString;
+    private AppBridge mAppBridge;
+    private SnailItem mScreenNailItem;
+    private SnailAlbum mScreenNailSet;
+    private OrientationManager mOrientationManager;
+    private boolean mTreatBackAsUp;
+    private boolean mStartInFilmstrip;
+    private boolean mHasCameraScreennailOrPlaceholder = false;
+    private boolean mRecenterCameraOnResume = true;
+
+    // These are only valid after the panorama callback
+    private boolean mIsPanorama;
+    private boolean mIsPanorama360;
+
+    private long mCameraSwitchCutoff = 0;
+    private boolean mSkipUpdateCurrentPhoto = false;
+    private static final long CAMERA_SWITCH_CUTOFF_THRESHOLD_MS = 300;
+
+    private static final long DEFERRED_UPDATE_MS = 250;
+    private boolean mDeferredUpdateWaiting = false;
+    private long mDeferUpdateUntil = Long.MAX_VALUE;
+
+    // The item that is deleted (but it can still be undeleted before commiting)
+    private Path mDeletePath;
+    private boolean mDeleteIsFocus;  // whether the deleted item was in focus
+
+    private Uri[] mNfcPushUris = new Uri[1];
+
+    private final MyMenuVisibilityListener mMenuVisibilityListener =
+            new MyMenuVisibilityListener();
+    private UpdateProgressListener mProgressListener;
+
+    private final PanoramaSupportCallback mUpdatePanoramaMenuItemsCallback = new PanoramaSupportCallback() {
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            if (mediaObject == mCurrentPhoto) {
+                mHandler.obtainMessage(MSG_UPDATE_PANORAMA_UI, isPanorama360 ? 1 : 0, 0,
+                        mediaObject).sendToTarget();
+            }
+        }
+    };
+
+    private final PanoramaSupportCallback mRefreshBottomControlsCallback = new PanoramaSupportCallback() {
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            if (mediaObject == mCurrentPhoto) {
+                mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, isPanorama ? 1 : 0, isPanorama360 ? 1 : 0,
+                        mediaObject).sendToTarget();
+            }
+        }
+    };
+
+    private final PanoramaSupportCallback mUpdateShareURICallback = new PanoramaSupportCallback() {
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            if (mediaObject == mCurrentPhoto) {
+                mHandler.obtainMessage(MSG_UPDATE_SHARE_URI, isPanorama360 ? 1 : 0, 0, mediaObject)
+                        .sendToTarget();
+            }
+        }
+    };
+
+    public static interface Model extends PhotoView.Model {
+        public void resume();
+        public void pause();
+        public boolean isEmpty();
+        public void setCurrentPhoto(Path path, int indexHint);
+    }
+
+    private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
+        @Override
+        public void onMenuVisibilityChanged(boolean isVisible) {
+            mIsMenuVisible = isVisible;
+            refreshHidingMessage();
+        }
+    }
+
+    private class UpdateProgressListener implements StitchingChangeListener {
+
+        @Override
+        public void onStitchingResult(Uri uri) {
+            sendUpdate(uri, MSG_REFRESH_IMAGE);
+        }
+
+        @Override
+        public void onStitchingQueued(Uri uri) {
+            sendUpdate(uri, MSG_UPDATE_PROGRESS);
+        }
+
+        @Override
+        public void onStitchingProgress(Uri uri, final int progress) {
+            sendUpdate(uri, MSG_UPDATE_PROGRESS);
+        }
+
+        private void sendUpdate(Uri uri, int message) {
+            MediaObject currentPhoto = mCurrentPhoto;
+            boolean isCurrentPhoto = currentPhoto instanceof LocalImage
+                    && currentPhoto.getContentUri().equals(uri);
+            if (isCurrentPhoto) {
+                mHandler.sendEmptyMessage(message);
+            }
+        }
+    };
+
+    @Override
+    protected int getBackgroundColorId() {
+        return R.color.photo_background;
+    }
+
+    private final GLView mRootPane = new GLView() {
+        @Override
+        protected void onLayout(
+                boolean changed, int left, int top, int right, int bottom) {
+            mPhotoView.layout(0, 0, right - left, bottom - top);
+            if (mShowDetails) {
+                mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
+            }
+        }
+    };
+
+    @Override
+    public void onCreate(Bundle data, Bundle restoreState) {
+        super.onCreate(data, restoreState);
+        mActionBar = mActivity.getGalleryActionBar();
+        mSelectionManager = new SelectionManager(mActivity, false);
+        mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+
+        mPhotoView = new PhotoView(mActivity);
+        mPhotoView.setListener(this);
+        mRootPane.addComponent(mPhotoView);
+        mApplication = (GalleryApp) ((Activity) mActivity).getApplication();
+        mOrientationManager = mActivity.getOrientationManager();
+        mActivity.getGLRoot().setOrientationSource(mOrientationManager);
+
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_HIDE_BARS: {
+                        hideBars();
+                        break;
+                    }
+                    case MSG_REFRESH_BOTTOM_CONTROLS: {
+                        if (mCurrentPhoto == message.obj && mBottomControls != null) {
+                            mIsPanorama = message.arg1 == 1;
+                            mIsPanorama360 = message.arg2 == 1;
+                            mBottomControls.refresh();
+                        }
+                        break;
+                    }
+                    case MSG_ON_FULL_SCREEN_CHANGED: {
+                        if (mAppBridge != null) {
+                            mAppBridge.onFullScreenChanged(message.arg1 == 1);
+                        }
+                        break;
+                    }
+                    case MSG_UPDATE_ACTION_BAR: {
+                        updateBars();
+                        break;
+                    }
+                    case MSG_WANT_BARS: {
+                        wantBars();
+                        break;
+                    }
+                    case MSG_UNFREEZE_GLROOT: {
+                        mActivity.getGLRoot().unfreeze();
+                        break;
+                    }
+                    case MSG_UPDATE_DEFERRED: {
+                        long nextUpdate = mDeferUpdateUntil - SystemClock.uptimeMillis();
+                        if (nextUpdate <= 0) {
+                            mDeferredUpdateWaiting = false;
+                            updateUIForCurrentPhoto();
+                        } else {
+                            mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, nextUpdate);
+                        }
+                        break;
+                    }
+                    case MSG_ON_CAMERA_CENTER: {
+                        mSkipUpdateCurrentPhoto = false;
+                        boolean stayedOnCamera = false;
+                        if (!mPhotoView.getFilmMode()) {
+                            stayedOnCamera = true;
+                        } else if (SystemClock.uptimeMillis() < mCameraSwitchCutoff &&
+                                mMediaSet.getMediaItemCount() > 1) {
+                            mPhotoView.switchToImage(1);
+                        } else {
+                            if (mAppBridge != null) mPhotoView.setFilmMode(false);
+                            stayedOnCamera = true;
+                        }
+
+                        if (stayedOnCamera) {
+                            if (mAppBridge == null && mMediaSet.getTotalMediaItemCount() > 1) {
+                                launchCamera();
+                                /* We got here by swiping from photo 1 to the
+                                   placeholder, so make it be the thing that
+                                   is in focus when the user presses back from
+                                   the camera app */
+                                mPhotoView.switchToImage(1);
+                            } else {
+                                updateBars();
+                                updateCurrentPhoto(mModel.getMediaItem(0));
+                            }
+                        }
+                        break;
+                    }
+                    case MSG_ON_PICTURE_CENTER: {
+                        if (!mPhotoView.getFilmMode() && mCurrentPhoto != null
+                                && (mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0) {
+                            mPhotoView.setFilmMode(true);
+                        }
+                        break;
+                    }
+                    case MSG_REFRESH_IMAGE: {
+                        final MediaItem photo = mCurrentPhoto;
+                        mCurrentPhoto = null;
+                        updateCurrentPhoto(photo);
+                        break;
+                    }
+                    case MSG_UPDATE_PHOTO_UI: {
+                        updateUIForCurrentPhoto();
+                        break;
+                    }
+                    case MSG_UPDATE_PROGRESS: {
+                        updateProgressBar();
+                        break;
+                    }
+                    case MSG_UPDATE_SHARE_URI: {
+                        if (mCurrentPhoto == message.obj) {
+                            boolean isPanorama360 = message.arg1 != 0;
+                            Uri contentUri = mCurrentPhoto.getContentUri();
+                            Intent panoramaIntent = null;
+                            if (isPanorama360) {
+                                panoramaIntent = createSharePanoramaIntent(contentUri);
+                            }
+                            Intent shareIntent = createShareIntent(mCurrentPhoto);
+
+                            mActionBar.setShareIntents(panoramaIntent, shareIntent, PhotoPage.this);
+                            setNfcBeamPushUri(contentUri);
+                        }
+                        break;
+                    }
+                    case MSG_UPDATE_PANORAMA_UI: {
+                        if (mCurrentPhoto == message.obj) {
+                            boolean isPanorama360 = message.arg1 != 0;
+                            updatePanoramaUI(isPanorama360);
+                        }
+                        break;
+                    }
+                    default: throw new AssertionError(message.what);
+                }
+            }
+        };
+
+        mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
+        mOriginalSetPathString = mSetPathString;
+        setupNfcBeamPush();
+        String itemPathString = data.getString(KEY_MEDIA_ITEM_PATH);
+        Path itemPath = itemPathString != null ?
+                Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)) :
+                    null;
+        mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
+        mStartInFilmstrip = data.getBoolean(KEY_START_IN_FILMSTRIP, false);
+        boolean inCameraRoll = data.getBoolean(KEY_IN_CAMERA_ROLL, false);
+        mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
+        if (mSetPathString != null) {
+            mShowSpinner = true;
+            mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE);
+            if (mAppBridge != null) {
+                mShowBars = false;
+                mHasCameraScreennailOrPlaceholder = true;
+                mAppBridge.setServer(this);
+
+                // Get the ScreenNail from AppBridge and register it.
+                int id = SnailSource.newId();
+                Path screenNailSetPath = SnailSource.getSetPath(id);
+                Path screenNailItemPath = SnailSource.getItemPath(id);
+                mScreenNailSet = (SnailAlbum) mActivity.getDataManager()
+                        .getMediaObject(screenNailSetPath);
+                mScreenNailItem = (SnailItem) mActivity.getDataManager()
+                        .getMediaObject(screenNailItemPath);
+                mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+
+                if (data.getBoolean(KEY_SHOW_WHEN_LOCKED, false)) {
+                    // Set the flag to be on top of the lock screen.
+                    mFlags |= FLAG_SHOW_WHEN_LOCKED;
+                }
+
+                // Don't display "empty album" action item for capture intents.
+                if (!mSetPathString.equals("/local/all/0")) {
+                    // Check if the path is a secure album.
+                    if (SecureSource.isSecurePath(mSetPathString)) {
+                        mSecureAlbum = (SecureAlbum) mActivity.getDataManager()
+                                .getMediaSet(mSetPathString);
+                        mShowSpinner = false;
+                    }
+                    mSetPathString = "/filter/empty/{"+mSetPathString+"}";
+                }
+
+                // Combine the original MediaSet with the one for ScreenNail
+                // from AppBridge.
+                mSetPathString = "/combo/item/{" + screenNailSetPath +
+                        "," + mSetPathString + "}";
+
+                // Start from the screen nail.
+                itemPath = screenNailItemPath;
+            } else if (inCameraRoll && GalleryUtils.isCameraAvailable(mActivity)) {
+                mSetPathString = "/combo/item/{" + FilterSource.FILTER_CAMERA_SHORTCUT +
+                        "," + mSetPathString + "}";
+                mCurrentIndex++;
+                mHasCameraScreennailOrPlaceholder = true;
+            }
+
+            MediaSet originalSet = mActivity.getDataManager()
+                    .getMediaSet(mSetPathString);
+            if (mHasCameraScreennailOrPlaceholder && originalSet instanceof ComboAlbum) {
+                // Use the name of the camera album rather than the default
+                // ComboAlbum behavior
+                ((ComboAlbum) originalSet).useNameOfChild(1);
+            }
+            mSelectionManager.setSourceMediaSet(originalSet);
+            mSetPathString = "/filter/delete/{" + mSetPathString + "}";
+            mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
+                    .getMediaSet(mSetPathString);
+            if (mMediaSet == null) {
+                Log.w(TAG, "failed to restore " + mSetPathString);
+            }
+            if (itemPath == null) {
+                int mediaItemCount = mMediaSet.getMediaItemCount();
+                if (mediaItemCount > 0) {
+                    if (mCurrentIndex >= mediaItemCount) mCurrentIndex = 0;
+                    itemPath = mMediaSet.getMediaItem(mCurrentIndex, 1)
+                        .get(0).getPath();
+                } else {
+                    // Bail out, PhotoPage can't load on an empty album
+                    return;
+                }
+            }
+            PhotoDataAdapter pda = new PhotoDataAdapter(
+                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
+                    mAppBridge == null ? -1 : 0,
+                    mAppBridge == null ? false : mAppBridge.isPanorama(),
+                    mAppBridge == null ? false : mAppBridge.isStaticCamera());
+            mModel = pda;
+            mPhotoView.setModel(mModel);
+
+            pda.setDataListener(new PhotoDataAdapter.DataListener() {
+
+                @Override
+                public void onPhotoChanged(int index, Path item) {
+                    int oldIndex = mCurrentIndex;
+                    mCurrentIndex = index;
+
+                    if (mHasCameraScreennailOrPlaceholder) {
+                        if (mCurrentIndex > 0) {
+                            mSkipUpdateCurrentPhoto = false;
+                        }
+
+                        if (oldIndex == 0 && mCurrentIndex > 0
+                                && !mPhotoView.getFilmMode()) {
+                            mPhotoView.setFilmMode(true);
+                            if (mAppBridge != null) {
+                                UsageStatistics.onEvent("CameraToFilmstrip",
+                                        UsageStatistics.TRANSITION_SWIPE, null);
+                            }
+                        } else if (oldIndex == 2 && mCurrentIndex == 1) {
+                            mCameraSwitchCutoff = SystemClock.uptimeMillis() +
+                                    CAMERA_SWITCH_CUTOFF_THRESHOLD_MS;
+                            mPhotoView.stopScrolling();
+                        } else if (oldIndex >= 1 && mCurrentIndex == 0) {
+                            mPhotoView.setWantPictureCenterCallbacks(true);
+                            mSkipUpdateCurrentPhoto = true;
+                        }
+                    }
+                    if (!mSkipUpdateCurrentPhoto) {
+                        if (item != null) {
+                            MediaItem photo = mModel.getMediaItem(0);
+                            if (photo != null) updateCurrentPhoto(photo);
+                        }
+                        updateBars();
+                    }
+                    // Reset the timeout for the bars after a swipe
+                    refreshHidingMessage();
+                }
+
+                @Override
+                public void onLoadingFinished(boolean loadingFailed) {
+                    if (!mModel.isEmpty()) {
+                        MediaItem photo = mModel.getMediaItem(0);
+                        if (photo != null) updateCurrentPhoto(photo);
+                    } else if (mIsActive) {
+                        // We only want to finish the PhotoPage if there is no
+                        // deletion that the user can undo.
+                        if (mMediaSet.getNumberOfDeletions() == 0) {
+                            mActivity.getStateManager().finishState(
+                                    PhotoPage.this);
+                        }
+                    }
+                }
+
+                @Override
+                public void onLoadingStarted() {
+                }
+            });
+        } else {
+            // Get default media set by the URI
+            MediaItem mediaItem = (MediaItem)
+                    mActivity.getDataManager().getMediaObject(itemPath);
+            mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
+            mPhotoView.setModel(mModel);
+            updateCurrentPhoto(mediaItem);
+            mShowSpinner = false;
+        }
+
+        mPhotoView.setFilmMode(mStartInFilmstrip && mMediaSet.getMediaItemCount() > 1);
+        RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+                .findViewById(mAppBridge != null ? R.id.content : R.id.gallery_root);
+        if (galleryRoot != null) {
+            if (mSecureAlbum == null) {
+                mBottomControls = new PhotoPageBottomControls(this, mActivity, galleryRoot);
+            }
+            StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+            if (progressManager != null) {
+                mProgressBar = new PhotoPageProgressBar(mActivity, galleryRoot);
+                mProgressListener = new UpdateProgressListener();
+                progressManager.addChangeListener(mProgressListener);
+                if (mSecureAlbum != null) {
+                    progressManager.addChangeListener(mSecureAlbum);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onPictureCenter(boolean isCamera) {
+        isCamera = isCamera || (mHasCameraScreennailOrPlaceholder && mAppBridge == null);
+        mPhotoView.setWantPictureCenterCallbacks(false);
+        mHandler.removeMessages(MSG_ON_CAMERA_CENTER);
+        mHandler.removeMessages(MSG_ON_PICTURE_CENTER);
+        mHandler.sendEmptyMessage(isCamera ? MSG_ON_CAMERA_CENTER : MSG_ON_PICTURE_CENTER);
+    }
+
+    @Override
+    public boolean canDisplayBottomControls() {
+        return mIsActive && !mPhotoView.canUndo();
+    }
+
+    @Override
+    public boolean canDisplayBottomControl(int control) {
+        if (mCurrentPhoto == null) {
+            return false;
+        }
+        switch(control) {
+            case R.id.photopage_bottom_control_edit:
+                return mHaveImageEditor && mShowBars
+                        && !mPhotoView.getFilmMode()
+                        && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_EDIT) != 0
+                        && mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE;
+            case R.id.photopage_bottom_control_panorama:
+                return mIsPanorama;
+            case R.id.photopage_bottom_control_tiny_planet:
+                return mHaveImageEditor && mShowBars
+                        && mIsPanorama360 && !mPhotoView.getFilmMode();
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public void onBottomControlClicked(int control) {
+        switch(control) {
+            case R.id.photopage_bottom_control_edit:
+                launchPhotoEditor();
+                return;
+            case R.id.photopage_bottom_control_panorama:
+                mActivity.getPanoramaViewHelper()
+                        .showPanorama(mCurrentPhoto.getContentUri());
+                return;
+            case R.id.photopage_bottom_control_tiny_planet:
+                launchTinyPlanet();
+                return;
+            default:
+                return;
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private void setupNfcBeamPush() {
+        if (!ApiHelper.HAS_SET_BEAM_PUSH_URIS) return;
+
+        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mActivity);
+        if (adapter != null) {
+            adapter.setBeamPushUris(null, mActivity);
+            adapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() {
+                @Override
+                public Uri[] createBeamUris(NfcEvent event) {
+                    return mNfcPushUris;
+                }
+            }, mActivity);
+        }
+    }
+
+    private void setNfcBeamPushUri(Uri uri) {
+        mNfcPushUris[0] = uri;
+    }
+
+    private static Intent createShareIntent(MediaObject mediaObject) {
+        int type = mediaObject.getMediaType();
+        return new Intent(Intent.ACTION_SEND)
+                .setType(MenuExecutor.getMimeType(type))
+                .putExtra(Intent.EXTRA_STREAM, mediaObject.getContentUri())
+                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+    }
+
+    private static Intent createSharePanoramaIntent(Uri contentUri) {
+        return new Intent(Intent.ACTION_SEND)
+                .setType(GalleryUtils.MIME_TYPE_PANORAMA360)
+                .putExtra(Intent.EXTRA_STREAM, contentUri)
+                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+    }
+
+    private void overrideTransitionToEditor() {
+        ((Activity) mActivity).overridePendingTransition(android.R.anim.fade_in,
+                android.R.anim.fade_out);
+    }
+
+    private void launchTinyPlanet() {
+        // Deep link into tiny planet
+        MediaItem current = mModel.getMediaItem(0);
+        Intent intent = new Intent(FilterShowActivity.TINY_PLANET_ACTION);
+        intent.setClass(mActivity, FilterShowActivity.class);
+        intent.setDataAndType(current.getContentUri(), current.getMimeType())
+            .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+                mActivity.isFullscreen());
+        mActivity.startActivityForResult(intent, REQUEST_EDIT);
+        overrideTransitionToEditor();
+    }
+
+    private void launchCamera() {
+        Intent intent = new Intent(mActivity, CameraActivity.class)
+            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mRecenterCameraOnResume = false;
+        mActivity.startActivity(intent);
+    }
+
+    private void launchPhotoEditor() {
+        MediaItem current = mModel.getMediaItem(0);
+        if (current == null || (current.getSupportedOperations()
+                & MediaObject.SUPPORT_EDIT) == 0) {
+            return;
+        }
+
+        Intent intent = new Intent(ACTION_NEXTGEN_EDIT);
+
+        intent.setDataAndType(current.getContentUri(), current.getMimeType())
+                .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        if (mActivity.getPackageManager()
+                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
+            intent.setAction(Intent.ACTION_EDIT);
+        }
+        intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+                mActivity.isFullscreen());
+        ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
+                REQUEST_EDIT);
+        overrideTransitionToEditor();
+    }
+
+    private void launchSimpleEditor() {
+        MediaItem current = mModel.getMediaItem(0);
+        if (current == null || (current.getSupportedOperations()
+                & MediaObject.SUPPORT_EDIT) == 0) {
+            return;
+        }
+
+        Intent intent = new Intent(ACTION_SIMPLE_EDIT);
+
+        intent.setDataAndType(current.getContentUri(), current.getMimeType())
+                .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        if (mActivity.getPackageManager()
+                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
+            intent.setAction(Intent.ACTION_EDIT);
+        }
+        intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+                mActivity.isFullscreen());
+        ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
+                REQUEST_EDIT);
+        overrideTransitionToEditor();
+    }
+
+    private void requestDeferredUpdate() {
+        mDeferUpdateUntil = SystemClock.uptimeMillis() + DEFERRED_UPDATE_MS;
+        if (!mDeferredUpdateWaiting) {
+            mDeferredUpdateWaiting = true;
+            mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, DEFERRED_UPDATE_MS);
+        }
+    }
+
+    private void updateUIForCurrentPhoto() {
+        if (mCurrentPhoto == null) return;
+
+        // If by swiping or deletion the user ends up on an action item
+        // and zoomed in, zoom out so that the context of the action is
+        // more clear
+        if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0
+                && !mPhotoView.getFilmMode()) {
+            mPhotoView.setWantPictureCenterCallbacks(true);
+        }
+
+        updateMenuOperations();
+        refreshBottomControlsWhenReady();
+        if (mShowDetails) {
+            mDetailsHelper.reloadDetails();
+        }
+        if ((mSecureAlbum == null)
+                && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
+            mCurrentPhoto.getPanoramaSupport(mUpdateShareURICallback);
+        }
+        updateProgressBar();
+    }
+
+    private void updateCurrentPhoto(MediaItem photo) {
+        if (mCurrentPhoto == photo) return;
+        mCurrentPhoto = photo;
+        if (mPhotoView.getFilmMode()) {
+            requestDeferredUpdate();
+        } else {
+            updateUIForCurrentPhoto();
+        }
+    }
+
+    private void updateProgressBar() {
+        if (mProgressBar != null) {
+            mProgressBar.hideProgress();
+            StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+            if (progressManager != null && mCurrentPhoto instanceof LocalImage) {
+                Integer progress = progressManager.getProgress(mCurrentPhoto.getContentUri());
+                if (progress != null) {
+                    mProgressBar.setProgress(progress);
+                }
+            }
+        }
+    }
+
+    private void updateMenuOperations() {
+        Menu menu = mActionBar.getMenu();
+
+        // it could be null if onCreateActionBar has not been called yet
+        if (menu == null) return;
+
+        MenuItem item = menu.findItem(R.id.action_slideshow);
+        if (item != null) {
+            item.setVisible((mSecureAlbum == null) && canDoSlideShow());
+        }
+        if (mCurrentPhoto == null) return;
+
+        int supportedOperations = mCurrentPhoto.getSupportedOperations();
+        if (mSecureAlbum != null) {
+            supportedOperations &= MediaObject.SUPPORT_DELETE;
+        } else {
+            mCurrentPhoto.getPanoramaSupport(mUpdatePanoramaMenuItemsCallback);
+            if (!mHaveImageEditor) {
+                supportedOperations &= ~MediaObject.SUPPORT_EDIT;
+            }
+        }
+        MenuExecutor.updateMenuOperation(menu, supportedOperations);
+    }
+
+    private boolean canDoSlideShow() {
+        if (mMediaSet == null || mCurrentPhoto == null) {
+            return false;
+        }
+        if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
+            return false;
+        }
+        return true;
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  Action Bar show/hide management
+    //////////////////////////////////////////////////////////////////////////
+
+    private void showBars() {
+        if (mShowBars) return;
+        mShowBars = true;
+        mOrientationManager.unlockOrientation();
+        mActionBar.show();
+        mActivity.getGLRoot().setLightsOutMode(false);
+        refreshHidingMessage();
+        refreshBottomControlsWhenReady();
+    }
+
+    private void hideBars() {
+        if (!mShowBars) return;
+        mShowBars = false;
+        mActionBar.hide();
+        mActivity.getGLRoot().setLightsOutMode(true);
+        mHandler.removeMessages(MSG_HIDE_BARS);
+        refreshBottomControlsWhenReady();
+    }
+
+    private void refreshHidingMessage() {
+        mHandler.removeMessages(MSG_HIDE_BARS);
+        if (!mIsMenuVisible && !mPhotoView.getFilmMode()) {
+            mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
+        }
+    }
+
+    private boolean canShowBars() {
+        // No bars if we are showing camera preview.
+        if (mAppBridge != null && mCurrentIndex == 0
+                && !mPhotoView.getFilmMode()) return false;
+
+        // No bars if it's not allowed.
+        if (!mActionBarAllowed) return false;
+
+        Configuration config = mActivity.getResources().getConfiguration();
+        if (config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private void wantBars() {
+        if (canShowBars()) showBars();
+    }
+
+    private void toggleBars() {
+        if (mShowBars) {
+            hideBars();
+        } else {
+            if (canShowBars()) showBars();
+        }
+    }
+
+    private void updateBars() {
+        if (!canShowBars()) {
+            hideBars();
+        }
+    }
+
+    @Override
+    protected void onBackPressed() {
+        if (mShowDetails) {
+            hideDetails();
+        } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
+            // We are leaving this page. Set the result now.
+            setResult();
+            if (mStartInFilmstrip && !mPhotoView.getFilmMode()) {
+                mPhotoView.setFilmMode(true);
+            } else if (mTreatBackAsUp) {
+                onUpPressed();
+            } else {
+                super.onBackPressed();
+            }
+        }
+    }
+
+    private void onUpPressed() {
+        if ((mStartInFilmstrip || mAppBridge != null)
+                && !mPhotoView.getFilmMode()) {
+            mPhotoView.setFilmMode(true);
+            return;
+        }
+
+        if (mActivity.getStateManager().getStateCount() > 1) {
+            setResult();
+            super.onBackPressed();
+            return;
+        }
+
+        if (mOriginalSetPathString == null) return;
+
+        if (mAppBridge == null) {
+            // We're in view mode so set up the stacks on our own.
+            Bundle data = new Bundle(getData());
+            data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
+            data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+                    mActivity.getDataManager().getTopSetPath(
+                            DataManager.INCLUDE_ALL));
+            mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+        } else {
+            GalleryUtils.startGalleryActivity(mActivity);
+        }
+    }
+
+    private void setResult() {
+        Intent result = null;
+        result = new Intent();
+        result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
+        setStateResult(Activity.RESULT_OK, result);
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  AppBridge.Server interface
+    //////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void setCameraRelativeFrame(Rect frame) {
+        mPhotoView.setCameraRelativeFrame(frame);
+    }
+
+    @Override
+    public boolean switchWithCaptureAnimation(int offset) {
+        return mPhotoView.switchWithCaptureAnimation(offset);
+    }
+
+    @Override
+    public void setSwipingEnabled(boolean enabled) {
+        mPhotoView.setSwipingEnabled(enabled);
+    }
+
+    @Override
+    public void notifyScreenNailChanged() {
+        mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+        mScreenNailSet.notifyChange();
+    }
+
+    @Override
+    public void addSecureAlbumItem(boolean isVideo, int id) {
+        mSecureAlbum.addMediaItem(isVideo, id);
+    }
+
+    @Override
+    protected boolean onCreateActionBar(Menu menu) {
+        mActionBar.createActionBarMenu(R.menu.photo, menu);
+        mHaveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
+        updateMenuOperations();
+        mActionBar.setTitle(mMediaSet != null ? mMediaSet.getName() : "");
+        return true;
+    }
+
+    private MenuExecutor.ProgressListener mConfirmDialogListener =
+            new MenuExecutor.ProgressListener() {
+        @Override
+        public void onProgressUpdate(int index) {}
+
+        @Override
+        public void onProgressComplete(int result) {}
+
+        @Override
+        public void onConfirmDialogShown() {
+            mHandler.removeMessages(MSG_HIDE_BARS);
+        }
+
+        @Override
+        public void onConfirmDialogDismissed(boolean confirmed) {
+            refreshHidingMessage();
+        }
+
+        @Override
+        public void onProgressStart() {}
+    };
+
+    private void switchToGrid() {
+        if (mActivity.getStateManager().hasStateClass(AlbumPage.class)) {
+            onUpPressed();
+        } else {
+            if (mOriginalSetPathString == null) return;
+            if (mProgressBar != null) {
+                updateCurrentPhoto(null);
+                mProgressBar.hideProgress();
+            }
+            Bundle data = new Bundle(getData());
+            data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
+            data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+                    mActivity.getDataManager().getTopSetPath(
+                            DataManager.INCLUDE_ALL));
+
+            // We only show cluster menu in the first AlbumPage in stack
+            // TODO: Enable this when running from the camera app
+            boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+            data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum
+                    && mAppBridge == null);
+
+            data.putBoolean(PhotoPage.KEY_APP_BRIDGE, mAppBridge != null);
+
+            // Account for live preview being first item
+            mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT,
+                    mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex);
+
+            if (mHasCameraScreennailOrPlaceholder && mAppBridge != null) {
+                mActivity.getStateManager().startState(AlbumPage.class, data);
+            } else {
+                mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+            }
+        }
+    }
+
+    @Override
+    protected boolean onItemSelected(MenuItem item) {
+        if (mModel == null) return true;
+        refreshHidingMessage();
+        MediaItem current = mModel.getMediaItem(0);
+
+        // This is a shield for monkey when it clicks the action bar
+        // menu when transitioning from filmstrip to camera
+        if (current instanceof SnailItem) return true;
+        // TODO: We should check the current photo against the MediaItem
+        // that the menu was initially created for. We need to fix this
+        // after PhotoPage being refactored.
+        if (current == null) {
+            // item is not ready, ignore
+            return true;
+        }
+        int currentIndex = mModel.getCurrentIndex();
+        Path path = current.getPath();
+
+        DataManager manager = mActivity.getDataManager();
+        int action = item.getItemId();
+        String confirmMsg = null;
+        switch (action) {
+            case android.R.id.home: {
+                onUpPressed();
+                return true;
+            }
+            case R.id.action_slideshow: {
+                Bundle data = new Bundle();
+                data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
+                data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
+                data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
+                data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+                mActivity.getStateManager().startStateForResult(
+                        SlideshowPage.class, REQUEST_SLIDESHOW, data);
+                return true;
+            }
+            case R.id.action_crop: {
+                Activity activity = mActivity;
+                Intent intent = new Intent(CropActivity.CROP_ACTION);
+                intent.setClass(activity, CropActivity.class);
+                intent.setDataAndType(manager.getContentUri(path), current.getMimeType())
+                    .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
+                        ? REQUEST_CROP_PICASA
+                        : REQUEST_CROP);
+                return true;
+            }
+            case R.id.action_trim: {
+                Intent intent = new Intent(mActivity, TrimVideo.class);
+                intent.setData(manager.getContentUri(path));
+                // We need the file path to wrap this into a RandomAccessFile.
+                intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath());
+                mActivity.startActivityForResult(intent, REQUEST_TRIM);
+                return true;
+            }
+            case R.id.action_mute: {
+                MuteVideo muteVideo = new MuteVideo(current.getFilePath(),
+                        manager.getContentUri(path), mActivity);
+                muteVideo.muteInBackground();
+                return true;
+            }
+            case R.id.action_edit: {
+                launchPhotoEditor();
+                return true;
+            }
+            case R.id.action_simple_edit: {
+                launchSimpleEditor();
+                return true;
+            }
+            case R.id.action_details: {
+                if (mShowDetails) {
+                    hideDetails();
+                } else {
+                    showDetails();
+                }
+                return true;
+            }
+            case R.id.action_delete:
+                confirmMsg = mActivity.getResources().getQuantityString(
+                        R.plurals.delete_selection, 1);
+            case R.id.action_setas:
+            case R.id.action_rotate_ccw:
+            case R.id.action_rotate_cw:
+            case R.id.action_show_on_map:
+                mSelectionManager.deSelectAll();
+                mSelectionManager.toggle(path);
+                mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
+                return true;
+            default :
+                return false;
+        }
+    }
+
+    private void hideDetails() {
+        mShowDetails = false;
+        mDetailsHelper.hide();
+    }
+
+    private void showDetails() {
+        mShowDetails = true;
+        if (mDetailsHelper == null) {
+            mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
+            mDetailsHelper.setCloseListener(new CloseListener() {
+                @Override
+                public void onClose() {
+                    hideDetails();
+                }
+            });
+        }
+        mDetailsHelper.show();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Callbacks from PhotoView
+    ////////////////////////////////////////////////////////////////////////////
+    @Override
+    public void onSingleTapUp(int x, int y) {
+        if (mAppBridge != null) {
+            if (mAppBridge.onSingleTapUp(x, y)) return;
+        }
+
+        MediaItem item = mModel.getMediaItem(0);
+        if (item == null || item == mScreenNailItem) {
+            // item is not ready or it is camera preview, ignore
+            return;
+        }
+
+        int supported = item.getSupportedOperations();
+        boolean playVideo = ((supported & MediaItem.SUPPORT_PLAY) != 0);
+        boolean unlock = ((supported & MediaItem.SUPPORT_UNLOCK) != 0);
+        boolean goBack = ((supported & MediaItem.SUPPORT_BACK) != 0);
+        boolean launchCamera = ((supported & MediaItem.SUPPORT_CAMERA_SHORTCUT) != 0);
+
+        if (playVideo) {
+            // determine if the point is at center (1/6) of the photo view.
+            // (The position of the "play" icon is at center (1/6) of the photo)
+            int w = mPhotoView.getWidth();
+            int h = mPhotoView.getHeight();
+            playVideo = (Math.abs(x - w / 2) * 12 <= w)
+                && (Math.abs(y - h / 2) * 12 <= h);
+        }
+
+        if (playVideo) {
+            if (mSecureAlbum == null) {
+                playVideo(mActivity, item.getPlayUri(), item.getName());
+            } else {
+                mActivity.getStateManager().finishState(this);
+            }
+        } else if (goBack) {
+            onBackPressed();
+        } else if (unlock) {
+            Intent intent = new Intent(mActivity, Gallery.class);
+            intent.putExtra(Gallery.KEY_DISMISS_KEYGUARD, true);
+            mActivity.startActivity(intent);
+        } else if (launchCamera) {
+            launchCamera();
+        } else {
+            toggleBars();
+        }
+    }
+
+    @Override
+    public void onActionBarAllowed(boolean allowed) {
+        mActionBarAllowed = allowed;
+        mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
+    }
+
+    @Override
+    public void onActionBarWanted() {
+        mHandler.sendEmptyMessage(MSG_WANT_BARS);
+    }
+
+    @Override
+    public void onFullScreenChanged(boolean full) {
+        Message m = mHandler.obtainMessage(
+                MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
+        m.sendToTarget();
+    }
+
+    // How we do delete/undo:
+    //
+    // When the user choose to delete a media item, we just tell the
+    // FilterDeleteSet to hide that item. If the user choose to undo it, we
+    // again tell FilterDeleteSet not to hide it. If the user choose to commit
+    // the deletion, we then actually delete the media item.
+    @Override
+    public void onDeleteImage(Path path, int offset) {
+        onCommitDeleteImage();  // commit the previous deletion
+        mDeletePath = path;
+        mDeleteIsFocus = (offset == 0);
+        mMediaSet.addDeletion(path, mCurrentIndex + offset);
+    }
+
+    @Override
+    public void onUndoDeleteImage() {
+        if (mDeletePath == null) return;
+        // If the deletion was done on the focused item, we want the model to
+        // focus on it when it is undeleted.
+        if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
+        mMediaSet.removeDeletion(mDeletePath);
+        mDeletePath = null;
+    }
+
+    @Override
+    public void onCommitDeleteImage() {
+        if (mDeletePath == null) return;
+        mMenuExecutor.startSingleItemAction(R.id.action_delete, mDeletePath);
+        mDeletePath = null;
+    }
+
+    public void playVideo(Activity activity, Uri uri, String title) {
+        try {
+            Intent intent = new Intent(Intent.ACTION_VIEW)
+                    .setDataAndType(uri, "video/*")
+                    .putExtra(Intent.EXTRA_TITLE, title)
+                    .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
+            activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(activity, activity.getString(R.string.video_err),
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void setCurrentPhotoByIntent(Intent intent) {
+        if (intent == null) return;
+        Path path = mApplication.getDataManager()
+                .findPathByUri(intent.getData(), intent.getType());
+        if (path != null) {
+            Path albumPath = mApplication.getDataManager().getDefaultSetOf(path);
+            if (!albumPath.equalsIgnoreCase(mOriginalSetPathString)) {
+                // If the edited image is stored in a different album, we need
+                // to start a new activity state to show the new image
+                Bundle data = new Bundle(getData());
+                data.putString(KEY_MEDIA_SET_PATH, albumPath.toString());
+                data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path.toString());
+                mActivity.getStateManager().startState(SinglePhotoPage.class, data);
+                return;
+            }
+            mModel.setCurrentPhoto(path, mCurrentIndex);
+        }
+    }
+
+    @Override
+    protected void onStateResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode == Activity.RESULT_CANCELED) {
+            // This is a reset, not a canceled
+            return;
+        }
+        if (resultCode == ProxyLauncher.RESULT_USER_CANCELED) {
+            // Unmap reset vs. canceled
+            resultCode = Activity.RESULT_CANCELED;
+        }
+        mRecenterCameraOnResume = false;
+        switch (requestCode) {
+            case REQUEST_EDIT:
+                setCurrentPhotoByIntent(data);
+                break;
+            case REQUEST_CROP:
+                if (resultCode == Activity.RESULT_OK) {
+                    setCurrentPhotoByIntent(data);
+                }
+                break;
+            case REQUEST_CROP_PICASA: {
+                if (resultCode == Activity.RESULT_OK) {
+                    Context context = mActivity.getAndroidContext();
+                    String message = context.getString(R.string.crop_saved,
+                            context.getString(R.string.folder_edited_online_photos));
+                    Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+                }
+                break;
+            }
+            case REQUEST_SLIDESHOW: {
+                if (data == null) break;
+                String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
+                int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
+                if (path != null) {
+                    mModel.setCurrentPhoto(Path.fromString(path), index);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mIsActive = false;
+
+        mActivity.getGLRoot().unfreeze();
+        mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
+
+        DetailsHelper.pause();
+        // Hide the detail dialog on exit
+        if (mShowDetails) hideDetails();
+        if (mModel != null) {
+            mModel.pause();
+        }
+        mPhotoView.pause();
+        mHandler.removeMessages(MSG_HIDE_BARS);
+        mHandler.removeMessages(MSG_REFRESH_BOTTOM_CONTROLS);
+        refreshBottomControlsWhenReady();
+        mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+        if (mShowSpinner) {
+            mActionBar.disableAlbumModeMenu(true);
+        }
+        onCommitDeleteImage();
+        mMenuExecutor.pause();
+        if (mMediaSet != null) mMediaSet.clearDeletion();
+    }
+
+    @Override
+    public void onCurrentImageUpdated() {
+        mActivity.getGLRoot().unfreeze();
+    }
+
+    @Override
+    public void onFilmModeChanged(boolean enabled) {
+        refreshBottomControlsWhenReady();
+        if (mShowSpinner) {
+            if (enabled) {
+                mActionBar.enableAlbumModeMenu(
+                        GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this);
+            } else {
+                mActionBar.disableAlbumModeMenu(true);
+            }
+        }
+        if (enabled) {
+            mHandler.removeMessages(MSG_HIDE_BARS);
+            UsageStatistics.onContentViewChanged(
+                    UsageStatistics.COMPONENT_GALLERY, "FilmstripPage");
+        } else {
+            refreshHidingMessage();
+            if (mAppBridge == null || mCurrentIndex > 0) {
+                UsageStatistics.onContentViewChanged(
+                        UsageStatistics.COMPONENT_GALLERY, "SinglePhotoPage");
+            } else {
+                UsageStatistics.onContentViewChanged(
+                        UsageStatistics.COMPONENT_CAMERA, "Unknown"); // TODO
+            }
+        }
+    }
+
+    private void transitionFromAlbumPageIfNeeded() {
+        TransitionStore transitions = mActivity.getTransitionStore();
+
+        int albumPageTransition = transitions.get(
+                KEY_ALBUMPAGE_TRANSITION, MSG_ALBUMPAGE_NONE);
+
+        if (albumPageTransition == MSG_ALBUMPAGE_NONE && mAppBridge != null
+                && mRecenterCameraOnResume) {
+            // Generally, resuming the PhotoPage when in Camera should
+            // reset to the capture mode to allow quick photo taking
+            mCurrentIndex = 0;
+            mPhotoView.resetToFirstPicture();
+        } else {
+            int resumeIndex = transitions.get(KEY_INDEX_HINT, -1);
+            if (resumeIndex >= 0) {
+                if (mHasCameraScreennailOrPlaceholder) {
+                    // Account for preview/placeholder being the first item
+                    resumeIndex++;
+                }
+                if (resumeIndex < mMediaSet.getMediaItemCount()) {
+                    mCurrentIndex = resumeIndex;
+                    mModel.moveTo(mCurrentIndex);
+                }
+            }
+        }
+
+        if (albumPageTransition == MSG_ALBUMPAGE_RESUMED) {
+            mPhotoView.setFilmMode(mStartInFilmstrip || mAppBridge != null);
+        } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) {
+            mPhotoView.setFilmMode(false);
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        if (mModel == null) {
+            mActivity.getStateManager().finishState(this);
+            return;
+        }
+        transitionFromAlbumPageIfNeeded();
+
+        mActivity.getGLRoot().freeze();
+        mIsActive = true;
+        setContentPane(mRootPane);
+
+        mModel.resume();
+        mPhotoView.resume();
+        mActionBar.setDisplayOptions(
+                ((mSecureAlbum == null) && (mSetPathString != null)), false);
+        mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
+        refreshBottomControlsWhenReady();
+        if (mShowSpinner && mPhotoView.getFilmMode()) {
+            mActionBar.enableAlbumModeMenu(
+                    GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this);
+        }
+        if (!mShowBars) {
+            mActionBar.hide();
+            mActivity.getGLRoot().setLightsOutMode(true);
+        }
+        boolean haveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
+        if (haveImageEditor != mHaveImageEditor) {
+            mHaveImageEditor = haveImageEditor;
+            updateMenuOperations();
+        }
+
+        mRecenterCameraOnResume = true;
+        mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mAppBridge != null) {
+            mAppBridge.setServer(null);
+            mScreenNailItem.setScreenNail(null);
+            mAppBridge.detachScreenNail();
+            mAppBridge = null;
+            mScreenNailSet = null;
+            mScreenNailItem = null;
+        }
+        mActivity.getGLRoot().setOrientationSource(null);
+        if (mBottomControls != null) mBottomControls.cleanup();
+
+        // Remove all pending messages.
+        mHandler.removeCallbacksAndMessages(null);
+        super.onDestroy();
+    }
+
+    private class MyDetailsSource implements DetailsSource {
+
+        @Override
+        public MediaDetails getDetails() {
+            return mModel.getMediaItem(0).getDetails();
+        }
+
+        @Override
+        public int size() {
+            return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
+        }
+
+        @Override
+        public int setIndex() {
+            return mModel.getCurrentIndex();
+        }
+    }
+
+    @Override
+    public void onAlbumModeSelected(int mode) {
+        if (mode == GalleryActionBar.ALBUM_GRID_MODE_SELECTED) {
+            switchToGrid();
+        }
+    }
+
+    @Override
+    public void refreshBottomControlsWhenReady() {
+        if (mBottomControls == null) {
+            return;
+        }
+        MediaObject currentPhoto = mCurrentPhoto;
+        if (currentPhoto == null) {
+            mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, 0, 0, currentPhoto).sendToTarget();
+        } else {
+            currentPhoto.getPanoramaSupport(mRefreshBottomControlsCallback);
+        }
+    }
+
+    private void updatePanoramaUI(boolean isPanorama360) {
+        Menu menu = mActionBar.getMenu();
+
+        // it could be null if onCreateActionBar has not been called yet
+        if (menu == null) {
+            return;
+        }
+
+        MenuExecutor.updateMenuForPanorama(menu, isPanorama360, isPanorama360);
+
+        if (isPanorama360) {
+            MenuItem item = menu.findItem(R.id.action_share);
+            if (item != null) {
+                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+                item.setTitle(mActivity.getResources().getString(R.string.share_as_photo));
+            }
+        } else if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_SHARE) != 0) {
+            MenuItem item = menu.findItem(R.id.action_share);
+            if (item != null) {
+                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+                item.setTitle(mActivity.getResources().getString(R.string.share));
+            }
+        }
+    }
+
+    @Override
+    public void onUndoBarVisibilityChanged(boolean visible) {
+        refreshBottomControlsWhenReady();
+    }
+
+    @Override
+    public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
+        final long timestampMillis = mCurrentPhoto.getDateInMs();
+        final String mediaType = getMediaTypeString(mCurrentPhoto);
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_GALLERY,
+                UsageStatistics.ACTION_SHARE,
+                mediaType,
+                        timestampMillis > 0
+                        ? System.currentTimeMillis() - timestampMillis
+                        : -1);
+        return false;
+    }
+
+    private static String getMediaTypeString(MediaItem item) {
+        if (item.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO) {
+            return "Video";
+        } else if (item.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE) {
+            return "Photo";
+        } else {
+            return "Unknown:" + item.getMediaType();
+        }
+    }
+
+}
diff --git a/src/com/android/gallery3d/app/PhotoPageBottomControls.java b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
new file mode 100644
index 0000000..24b8ceb
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PhotoPageBottomControls implements OnClickListener {
+    public interface Delegate {
+        public boolean canDisplayBottomControls();
+        public boolean canDisplayBottomControl(int control);
+        public void onBottomControlClicked(int control);
+        public void refreshBottomControlsWhenReady();
+    }
+
+    private Delegate mDelegate;
+    private ViewGroup mParentLayout;
+    private ViewGroup mContainer;
+
+    private boolean mContainerVisible = false;
+    private Map<View, Boolean> mControlsVisible = new HashMap<View, Boolean>();
+
+    private Animation mContainerAnimIn = new AlphaAnimation(0f, 1f);
+    private Animation mContainerAnimOut = new AlphaAnimation(1f, 0f);
+    private static final int CONTAINER_ANIM_DURATION_MS = 200;
+
+    private static final int CONTROL_ANIM_DURATION_MS = 150;
+    private static Animation getControlAnimForVisibility(boolean visible) {
+        Animation anim = visible ? new AlphaAnimation(0f, 1f)
+                : new AlphaAnimation(1f, 0f);
+        anim.setDuration(CONTROL_ANIM_DURATION_MS);
+        return anim;
+    }
+
+    public PhotoPageBottomControls(Delegate delegate, Context context, RelativeLayout layout) {
+        mDelegate = delegate;
+        mParentLayout = layout;
+
+        LayoutInflater inflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mContainer = (ViewGroup) inflater
+                .inflate(R.layout.photopage_bottom_controls, mParentLayout, false);
+        mParentLayout.addView(mContainer);
+
+        for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
+            View child = mContainer.getChildAt(i);
+            child.setOnClickListener(this);
+            mControlsVisible.put(child, false);
+        }
+
+        mContainerAnimIn.setDuration(CONTAINER_ANIM_DURATION_MS);
+        mContainerAnimOut.setDuration(CONTAINER_ANIM_DURATION_MS);
+
+        mDelegate.refreshBottomControlsWhenReady();
+    }
+
+    private void hide() {
+        mContainer.clearAnimation();
+        mContainerAnimOut.reset();
+        mContainer.startAnimation(mContainerAnimOut);
+        mContainer.setVisibility(View.INVISIBLE);
+    }
+
+    private void show() {
+        mContainer.clearAnimation();
+        mContainerAnimIn.reset();
+        mContainer.startAnimation(mContainerAnimIn);
+        mContainer.setVisibility(View.VISIBLE);
+    }
+
+    public void refresh() {
+        boolean visible = mDelegate.canDisplayBottomControls();
+        boolean containerVisibilityChanged = (visible != mContainerVisible);
+        if (containerVisibilityChanged) {
+            if (visible) {
+                show();
+            } else {
+                hide();
+            }
+            mContainerVisible = visible;
+        }
+        if (!mContainerVisible) {
+            return;
+        }
+        for (View control : mControlsVisible.keySet()) {
+            Boolean prevVisibility = mControlsVisible.get(control);
+            boolean curVisibility = mDelegate.canDisplayBottomControl(control.getId());
+            if (prevVisibility.booleanValue() != curVisibility) {
+                if (!containerVisibilityChanged) {
+                    control.clearAnimation();
+                    control.startAnimation(getControlAnimForVisibility(curVisibility));
+                }
+                control.setVisibility(curVisibility ? View.VISIBLE : View.INVISIBLE);
+                mControlsVisible.put(control, curVisibility);
+            }
+        }
+        // Force a layout change
+        mContainer.requestLayout(); // Kick framework to draw the control.
+    }
+
+    public void cleanup() {
+        mParentLayout.removeView(mContainer);
+        mControlsVisible.clear();
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (mContainerVisible && mControlsVisible.get(view).booleanValue()) {
+            mDelegate.onBottomControlClicked(view.getId());
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPageProgressBar.java b/src/com/android/gallery3d/app/PhotoPageProgressBar.java
new file mode 100644
index 0000000..141fea6
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPageProgressBar.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+public class PhotoPageProgressBar {
+    private ViewGroup mContainer;
+    private View mProgress;
+
+    public PhotoPageProgressBar(Context context, RelativeLayout parentLayout) {
+        LayoutInflater inflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mContainer = (ViewGroup) inflater.inflate(R.layout.photopage_progress_bar, parentLayout,
+                false);
+        parentLayout.addView(mContainer);
+        mProgress = mContainer.findViewById(R.id.photopage_progress_foreground);
+    }
+
+    public void setProgress(int progressPercent) {
+        mContainer.setVisibility(View.VISIBLE);
+        LayoutParams layoutParams = mProgress.getLayoutParams();
+        layoutParams.width = mContainer.getWidth() * progressPercent / 100;
+        mProgress.setLayoutParams(layoutParams);
+    }
+
+    public void hideProgress() {
+        mContainer.setVisibility(View.INVISIBLE);
+    }
+}
diff --git a/src/com/android/gallery3d/app/PickerActivity.java b/src/com/android/gallery3d/app/PickerActivity.java
new file mode 100644
index 0000000..d5bb218
--- /dev/null
+++ b/src/com/android/gallery3d/app/PickerActivity.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.app;
+
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.Window;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.GLRootView;
+
+public class PickerActivity extends AbstractGalleryActivity
+        implements OnClickListener {
+
+    public static final String KEY_ALBUM_PATH = "album-path";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // We show the picker in two ways. One smaller screen we use a full
+        // screen window with an action bar. On larger screen we use a dialog.
+        boolean isDialog = getResources().getBoolean(R.bool.picker_is_dialog);
+
+        if (!isDialog) {
+            requestWindowFeature(Window.FEATURE_ACTION_BAR);
+            requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+        }
+
+        setContentView(R.layout.dialog_picker);
+
+        if (isDialog) {
+            // In dialog mode, we don't have the action bar to show the
+            // "cancel" action, so we show an additional "cancel" button.
+            View view = findViewById(R.id.cancel);
+            view.setOnClickListener(this);
+            view.setVisibility(View.VISIBLE);
+
+            // We need this, otherwise the view will be dimmed because it
+            // is "behind" the dialog.
+            ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.pickup, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.action_cancel) {
+            finish();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v.getId() == R.id.cancel) finish();
+    }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
new file mode 100644
index 0000000..00f2fe7
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.BitmapScreenNail;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
+public class SinglePhotoDataAdapter extends TileImageViewAdapter
+        implements PhotoPage.Model {
+
+    private static final String TAG = "SinglePhotoDataAdapter";
+    private static final int SIZE_BACKUP = 1024;
+    private static final int MSG_UPDATE_IMAGE = 1;
+
+    private MediaItem mItem;
+    private boolean mHasFullImage;
+    private Future<?> mTask;
+    private Handler mHandler;
+
+    private PhotoView mPhotoView;
+    private ThreadPool mThreadPool;
+    private int mLoadingState = LOADING_INIT;
+    private BitmapScreenNail mBitmapScreenNail;
+
+    public SinglePhotoDataAdapter(
+            AbstractGalleryActivity activity, PhotoView view, MediaItem item) {
+        mItem = Utils.checkNotNull(item);
+        mHasFullImage = (item.getSupportedOperations() &
+                MediaItem.SUPPORT_FULL_IMAGE) != 0;
+        mPhotoView = Utils.checkNotNull(view);
+        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+            @Override
+            @SuppressWarnings("unchecked")
+            public void handleMessage(Message message) {
+                Utils.assertTrue(message.what == MSG_UPDATE_IMAGE);
+                if (mHasFullImage) {
+                    onDecodeLargeComplete((ImageBundle) message.obj);
+                } else {
+                    onDecodeThumbComplete((Future<Bitmap>) message.obj);
+                }
+            }
+        };
+        mThreadPool = activity.getThreadPool();
+    }
+
+    private static class ImageBundle {
+        public final BitmapRegionDecoder decoder;
+        public final Bitmap backupImage;
+
+        public ImageBundle(BitmapRegionDecoder decoder, Bitmap backupImage) {
+            this.decoder = decoder;
+            this.backupImage = backupImage;
+        }
+    }
+
+    private FutureListener<BitmapRegionDecoder> mLargeListener =
+            new FutureListener<BitmapRegionDecoder>() {
+        @Override
+        public void onFutureDone(Future<BitmapRegionDecoder> future) {
+            BitmapRegionDecoder decoder = future.get();
+            if (decoder == null) return;
+            int width = decoder.getWidth();
+            int height = decoder.getHeight();
+            BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inSampleSize = BitmapUtils.computeSampleSize(
+                    (float) SIZE_BACKUP / Math.max(width, height));
+            Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, width, height), options);
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    MSG_UPDATE_IMAGE, new ImageBundle(decoder, bitmap)));
+        }
+    };
+
+    private FutureListener<Bitmap> mThumbListener =
+            new FutureListener<Bitmap>() {
+        @Override
+        public void onFutureDone(Future<Bitmap> future) {
+            mHandler.sendMessage(
+                    mHandler.obtainMessage(MSG_UPDATE_IMAGE, future));
+        }
+    };
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    private void setScreenNail(Bitmap bitmap, int width, int height) {
+        mBitmapScreenNail = new BitmapScreenNail(bitmap);
+        setScreenNail(mBitmapScreenNail, width, height);
+    }
+
+    private void onDecodeLargeComplete(ImageBundle bundle) {
+        try {
+            setScreenNail(bundle.backupImage,
+                    bundle.decoder.getWidth(), bundle.decoder.getHeight());
+            setRegionDecoder(bundle.decoder);
+            mPhotoView.notifyImageChange(0);
+        } catch (Throwable t) {
+            Log.w(TAG, "fail to decode large", t);
+        }
+    }
+
+    private void onDecodeThumbComplete(Future<Bitmap> future) {
+        try {
+            Bitmap backup = future.get();
+            if (backup == null) {
+                mLoadingState = LOADING_FAIL;
+                return;
+            } else {
+                mLoadingState = LOADING_COMPLETE;
+            }
+            setScreenNail(backup, backup.getWidth(), backup.getHeight());
+            mPhotoView.notifyImageChange(0);
+        } catch (Throwable t) {
+            Log.w(TAG, "fail to decode thumb", t);
+        }
+    }
+
+    @Override
+    public void resume() {
+        if (mTask == null) {
+            if (mHasFullImage) {
+                mTask = mThreadPool.submit(
+                        mItem.requestLargeImage(), mLargeListener);
+            } else {
+                mTask = mThreadPool.submit(
+                        mItem.requestImage(MediaItem.TYPE_THUMBNAIL),
+                        mThumbListener);
+            }
+        }
+    }
+
+    @Override
+    public void pause() {
+        Future<?> task = mTask;
+        task.cancel();
+        task.waitDone();
+        if (task.get() == null) {
+            mTask = null;
+        }
+        if (mBitmapScreenNail != null) {
+            mBitmapScreenNail.recycle();
+            mBitmapScreenNail = null;
+        }
+    }
+
+    @Override
+    public void moveTo(int index) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void getImageSize(int offset, PhotoView.Size size) {
+        if (offset == 0) {
+            size.width = mItem.getWidth();
+            size.height = mItem.getHeight();
+        } else {
+            size.width = 0;
+            size.height = 0;
+        }
+    }
+
+    @Override
+    public int getImageRotation(int offset) {
+        return (offset == 0) ? mItem.getFullImageRotation() : 0;
+    }
+
+    @Override
+    public ScreenNail getScreenNail(int offset) {
+        return (offset == 0) ? getScreenNail() : null;
+    }
+
+    @Override
+    public void setNeedFullImage(boolean enabled) {
+        // currently not necessary.
+    }
+
+    @Override
+    public boolean isCamera(int offset) {
+        return false;
+    }
+
+    @Override
+    public boolean isPanorama(int offset) {
+        return false;
+    }
+
+    @Override
+    public boolean isStaticCamera(int offset) {
+        return false;
+    }
+
+    @Override
+    public boolean isVideo(int offset) {
+        return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+    }
+
+    @Override
+    public boolean isDeletable(int offset) {
+        return (mItem.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+    }
+
+    @Override
+    public MediaItem getMediaItem(int offset) {
+        return offset == 0 ? mItem : null;
+    }
+
+    @Override
+    public int getCurrentIndex() {
+        return 0;
+    }
+
+    @Override
+    public void setCurrentPhoto(Path path, int indexHint) {
+        // ignore
+    }
+
+    @Override
+    public void setFocusHintDirection(int direction) {
+        // ignore
+    }
+
+    @Override
+    public void setFocusHintPath(Path path) {
+        // ignore
+    }
+
+    @Override
+    public int getLoadingState(int offset) {
+        return mLoadingState;
+    }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoPage.java b/src/com/android/gallery3d/app/SinglePhotoPage.java
new file mode 100644
index 0000000..beb87d3
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoPage.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+public class SinglePhotoPage extends PhotoPage {
+
+}
diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
new file mode 100644
index 0000000..7a0fba5
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.app.SlideshowPage.Slide;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.LinkedList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SlideshowDataAdapter implements SlideshowPage.Model {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SlideshowDataAdapter";
+
+    private static final int IMAGE_QUEUE_CAPACITY = 3;
+
+    public interface SlideshowSource {
+        public void addContentListener(ContentListener listener);
+        public void removeContentListener(ContentListener listener);
+        public long reload();
+        public MediaItem getMediaItem(int index);
+        public int findItemIndex(Path path, int hint);
+    }
+
+    private final SlideshowSource mSource;
+
+    private int mLoadIndex = 0;
+    private int mNextOutput = 0;
+    private boolean mIsActive = false;
+    private boolean mNeedReset;
+    private boolean mDataReady;
+    private Path mInitialPath;
+
+    private final LinkedList<Slide> mImageQueue = new LinkedList<Slide>();
+
+    private Future<Void> mReloadTask;
+    private final ThreadPool mThreadPool;
+
+    private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
+    private final AtomicBoolean mNeedReload = new AtomicBoolean(false);
+    private final SourceListener mSourceListener = new SourceListener();
+
+    // The index is just a hint if initialPath is set
+    public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index,
+            Path initialPath) {
+        mSource = source;
+        mInitialPath = initialPath;
+        mLoadIndex = index;
+        mNextOutput = index;
+        mThreadPool = context.getThreadPool();
+    }
+
+    private MediaItem loadItem() {
+        if (mNeedReload.compareAndSet(true, false)) {
+            long v = mSource.reload();
+            if (v != mDataVersion) {
+                mDataVersion = v;
+                mNeedReset = true;
+                return null;
+            }
+        }
+        int index = mLoadIndex;
+        if (mInitialPath != null) {
+            index = mSource.findItemIndex(mInitialPath, index);
+            mInitialPath = null;
+        }
+        return mSource.getMediaItem(index);
+    }
+
+    private class ReloadTask implements Job<Void> {
+        @Override
+        public Void run(JobContext jc) {
+            while (true) {
+                synchronized (SlideshowDataAdapter.this) {
+                    while (mIsActive && (!mDataReady
+                            || mImageQueue.size() >= IMAGE_QUEUE_CAPACITY)) {
+                        try {
+                            SlideshowDataAdapter.this.wait();
+                        } catch (InterruptedException ex) {
+                            // ignored.
+                        }
+                        continue;
+                    }
+                }
+                if (!mIsActive) return null;
+                mNeedReset = false;
+
+                MediaItem item = loadItem();
+
+                if (mNeedReset) {
+                    synchronized (SlideshowDataAdapter.this) {
+                        mImageQueue.clear();
+                        mLoadIndex = mNextOutput;
+                    }
+                    continue;
+                }
+
+                if (item == null) {
+                    synchronized (SlideshowDataAdapter.this) {
+                        if (!mNeedReload.get()) mDataReady = false;
+                        SlideshowDataAdapter.this.notifyAll();
+                    }
+                    continue;
+                }
+
+                Bitmap bitmap = item
+                        .requestImage(MediaItem.TYPE_THUMBNAIL)
+                        .run(jc);
+
+                if (bitmap != null) {
+                    synchronized (SlideshowDataAdapter.this) {
+                        mImageQueue.addLast(
+                                new Slide(item, mLoadIndex, bitmap));
+                        if (mImageQueue.size() == 1) {
+                            SlideshowDataAdapter.this.notifyAll();
+                        }
+                    }
+                }
+                ++mLoadIndex;
+            }
+        }
+    }
+
+    private class SourceListener implements ContentListener {
+        @Override
+        public void onContentDirty() {
+            synchronized (SlideshowDataAdapter.this) {
+                mNeedReload.set(true);
+                mDataReady = true;
+                SlideshowDataAdapter.this.notifyAll();
+            }
+        }
+    }
+
+    private synchronized Slide innerNextBitmap() {
+        while (mIsActive && mDataReady && mImageQueue.isEmpty()) {
+            try {
+                wait();
+            } catch (InterruptedException t) {
+                throw new AssertionError();
+            }
+        }
+        if (mImageQueue.isEmpty()) return null;
+        mNextOutput++;
+        this.notifyAll();
+        return mImageQueue.removeFirst();
+    }
+
+    @Override
+    public Future<Slide> nextSlide(FutureListener<Slide> listener) {
+        return mThreadPool.submit(new Job<Slide>() {
+            @Override
+            public Slide run(JobContext jc) {
+                jc.setMode(ThreadPool.MODE_NONE);
+                return innerNextBitmap();
+            }
+        }, listener);
+    }
+
+    @Override
+    public void pause() {
+        synchronized (this) {
+            mIsActive = false;
+            notifyAll();
+        }
+        mSource.removeContentListener(mSourceListener);
+        mReloadTask.cancel();
+        mReloadTask.waitDone();
+        mReloadTask = null;
+    }
+
+    @Override
+    public synchronized void resume() {
+        mIsActive = true;
+        mSource.addContentListener(mSourceListener);
+        mNeedReload.set(true);
+        mDataReady = true;
+        mReloadTask = mThreadPool.submit(new ReloadTask());
+    }
+}
diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java
new file mode 100644
index 0000000..174058d
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowPage.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.SlideshowView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+public class SlideshowPage extends ActivityState {
+    private static final String TAG = "SlideshowPage";
+
+    public static final String KEY_SET_PATH = "media-set-path";
+    public static final String KEY_ITEM_PATH = "media-item-path";
+    public static final String KEY_PHOTO_INDEX = "photo-index";
+    public static final String KEY_RANDOM_ORDER = "random-order";
+    public static final String KEY_REPEAT = "repeat";
+    public static final String KEY_DREAM = "dream";
+
+    private static final long SLIDESHOW_DELAY = 3000; // 3 seconds
+
+    private static final int MSG_LOAD_NEXT_BITMAP = 1;
+    private static final int MSG_SHOW_PENDING_BITMAP = 2;
+
+    public static interface Model {
+        public void pause();
+
+        public void resume();
+
+        public Future<Slide> nextSlide(FutureListener<Slide> listener);
+    }
+
+    public static class Slide {
+        public Bitmap bitmap;
+        public MediaItem item;
+        public int index;
+
+        public Slide(MediaItem item, int index, Bitmap bitmap) {
+            this.bitmap = bitmap;
+            this.item = item;
+            this.index = index;
+        }
+    }
+
+    private Handler mHandler;
+    private Model mModel;
+    private SlideshowView mSlideshowView;
+
+    private Slide mPendingSlide = null;
+    private boolean mIsActive = false;
+    private final Intent mResultIntent = new Intent();
+
+    @Override
+    protected int getBackgroundColorId() {
+        return R.color.slideshow_background;
+    }
+
+    private final GLView mRootPane = new GLView() {
+        @Override
+        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+            mSlideshowView.layout(0, 0, right - left, bottom - top);
+        }
+
+        @Override
+        protected boolean onTouch(MotionEvent event) {
+            if (event.getAction() == MotionEvent.ACTION_UP) {
+                onBackPressed();
+            }
+            return true;
+        }
+
+        @Override
+        protected void renderBackground(GLCanvas canvas) {
+            canvas.clearBuffer(getBackgroundColor());
+        }
+    };
+
+    @Override
+    public void onCreate(Bundle data, Bundle restoreState) {
+        super.onCreate(data, restoreState);
+        mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR);
+        if (data.getBoolean(KEY_DREAM)) {
+            // Dream screensaver only keeps screen on for plugged devices.
+            mFlags |= FLAG_SCREEN_ON_WHEN_PLUGGED | FLAG_SHOW_WHEN_LOCKED;
+        } else {
+            // User-initiated slideshow would always keep screen on.
+            mFlags |= FLAG_SCREEN_ON_ALWAYS;
+        }
+
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_SHOW_PENDING_BITMAP:
+                        showPendingBitmap();
+                        break;
+                    case MSG_LOAD_NEXT_BITMAP:
+                        loadNextBitmap();
+                        break;
+                    default: throw new AssertionError();
+                }
+            }
+        };
+        initializeViews();
+        initializeData(data);
+    }
+
+    private void loadNextBitmap() {
+        mModel.nextSlide(new FutureListener<Slide>() {
+            @Override
+            public void onFutureDone(Future<Slide> future) {
+                mPendingSlide = future.get();
+                mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP);
+            }
+        });
+    }
+
+    private void showPendingBitmap() {
+        // mPendingBitmap could be null, if
+        // 1.) there is no more items
+        // 2.) mModel is paused
+        Slide slide = mPendingSlide;
+        if (slide == null) {
+            if (mIsActive) {
+                mActivity.getStateManager().finishState(SlideshowPage.this);
+            }
+            return;
+        }
+
+        mSlideshowView.next(slide.bitmap, slide.item.getRotation());
+
+        setStateResult(Activity.RESULT_OK, mResultIntent
+                .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString())
+                .putExtra(KEY_PHOTO_INDEX, slide.index));
+        mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP, SLIDESHOW_DELAY);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mIsActive = false;
+        mModel.pause();
+        mSlideshowView.release();
+
+        mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP);
+        mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mIsActive = true;
+        mModel.resume();
+
+        if (mPendingSlide != null) {
+            showPendingBitmap();
+        } else {
+            loadNextBitmap();
+        }
+    }
+
+    private void initializeData(Bundle data) {
+        boolean random = data.getBoolean(KEY_RANDOM_ORDER, false);
+
+        // We only want to show slideshow for images only, not videos.
+        String mediaPath = data.getString(KEY_SET_PATH);
+        mediaPath = FilterUtils.newFilterPath(mediaPath, FilterUtils.FILTER_IMAGE_ONLY);
+        MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+
+        if (random) {
+            boolean repeat = data.getBoolean(KEY_REPEAT);
+            mModel = new SlideshowDataAdapter(mActivity,
+                    new ShuffleSource(mediaSet, repeat), 0, null);
+            setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, 0));
+        } else {
+            int index = data.getInt(KEY_PHOTO_INDEX);
+            String itemPath = data.getString(KEY_ITEM_PATH);
+            Path path = itemPath != null ? Path.fromString(itemPath) : null;
+            boolean repeat = data.getBoolean(KEY_REPEAT);
+            mModel = new SlideshowDataAdapter(mActivity, new SequentialSource(mediaSet, repeat),
+                    index, path);
+            setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, index));
+        }
+    }
+
+    private void initializeViews() {
+        mSlideshowView = new SlideshowView();
+        mRootPane.addComponent(mSlideshowView);
+        setContentPane(mRootPane);
+    }
+
+    private static MediaItem findMediaItem(MediaSet mediaSet, int index) {
+        for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) {
+            MediaSet subset = mediaSet.getSubMediaSet(i);
+            int count = subset.getTotalMediaItemCount();
+            if (index < count) {
+                return findMediaItem(subset, index);
+            }
+            index -= count;
+        }
+        ArrayList<MediaItem> list = mediaSet.getMediaItem(index, 1);
+        return list.isEmpty() ? null : list.get(0);
+    }
+
+    private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource {
+        private static final int RETRY_COUNT = 5;
+        private final MediaSet mMediaSet;
+        private final Random mRandom = new Random();
+        private int mOrder[] = new int[0];
+        private final boolean mRepeat;
+        private long mSourceVersion = MediaSet.INVALID_DATA_VERSION;
+        private int mLastIndex = -1;
+
+        public ShuffleSource(MediaSet mediaSet, boolean repeat) {
+            mMediaSet = Utils.checkNotNull(mediaSet);
+            mRepeat = repeat;
+        }
+
+        @Override
+        public int findItemIndex(Path path, int hint) {
+            return hint;
+        }
+
+        @Override
+        public MediaItem getMediaItem(int index) {
+            if (!mRepeat && index >= mOrder.length) return null;
+            if (mOrder.length == 0) return null;
+            mLastIndex = mOrder[index % mOrder.length];
+            MediaItem item = findMediaItem(mMediaSet, mLastIndex);
+            for (int i = 0; i < RETRY_COUNT && item == null; ++i) {
+                Log.w(TAG, "fail to find image: " + mLastIndex);
+                mLastIndex = mRandom.nextInt(mOrder.length);
+                item = findMediaItem(mMediaSet, mLastIndex);
+            }
+            return item;
+        }
+
+        @Override
+        public long reload() {
+            long version = mMediaSet.reload();
+            if (version != mSourceVersion) {
+                mSourceVersion = version;
+                int count = mMediaSet.getTotalMediaItemCount();
+                if (count != mOrder.length) generateOrderArray(count);
+            }
+            return version;
+        }
+
+        private void generateOrderArray(int totalCount) {
+            if (mOrder.length != totalCount) {
+                mOrder = new int[totalCount];
+                for (int i = 0; i < totalCount; ++i) {
+                    mOrder[i] = i;
+                }
+            }
+            for (int i = totalCount - 1; i > 0; --i) {
+                Utils.swap(mOrder, i, mRandom.nextInt(i + 1));
+            }
+            if (mOrder[0] == mLastIndex && totalCount > 1) {
+                Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1);
+            }
+        }
+
+        @Override
+        public void addContentListener(ContentListener listener) {
+            mMediaSet.addContentListener(listener);
+        }
+
+        @Override
+        public void removeContentListener(ContentListener listener) {
+            mMediaSet.removeContentListener(listener);
+        }
+    }
+
+    private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource {
+        private static final int DATA_SIZE = 32;
+
+        private ArrayList<MediaItem> mData = new ArrayList<MediaItem>();
+        private int mDataStart = 0;
+        private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
+        private final MediaSet mMediaSet;
+        private final boolean mRepeat;
+
+        public SequentialSource(MediaSet mediaSet, boolean repeat) {
+            mMediaSet = mediaSet;
+            mRepeat = repeat;
+        }
+
+        @Override
+        public int findItemIndex(Path path, int hint) {
+            return mMediaSet.getIndexOfItem(path, hint);
+        }
+
+        @Override
+        public MediaItem getMediaItem(int index) {
+            int dataEnd = mDataStart + mData.size();
+
+            if (mRepeat) {
+                int count = mMediaSet.getMediaItemCount();
+                if (count == 0) return null;
+                index = index % count;
+            }
+            if (index < mDataStart || index >= dataEnd) {
+                mData = mMediaSet.getMediaItem(index, DATA_SIZE);
+                mDataStart = index;
+                dataEnd = index + mData.size();
+            }
+
+            return (index < mDataStart || index >= dataEnd) ? null : mData.get(index - mDataStart);
+        }
+
+        @Override
+        public long reload() {
+            long version = mMediaSet.reload();
+            if (version != mDataVersion) {
+                mDataVersion = version;
+                mData.clear();
+            }
+            return mDataVersion;
+        }
+
+        @Override
+        public void addContentListener(ContentListener listener) {
+            mMediaSet.addContentListener(listener);
+        }
+
+        @Override
+        public void removeContentListener(ContentListener listener) {
+            mMediaSet.removeContentListener(listener);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
new file mode 100644
index 0000000..53c3fc2
--- /dev/null
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.util.Stack;
+
+public class StateManager {
+    @SuppressWarnings("unused")
+    private static final String TAG = "StateManager";
+    private boolean mIsResumed = false;
+
+    private static final String KEY_MAIN = "activity-state";
+    private static final String KEY_DATA = "data";
+    private static final String KEY_STATE = "bundle";
+    private static final String KEY_CLASS = "class";
+
+    private AbstractGalleryActivity mActivity;
+    private Stack<StateEntry> mStack = new Stack<StateEntry>();
+    private ActivityState.ResultEntry mResult;
+
+    public StateManager(AbstractGalleryActivity activity) {
+        mActivity = activity;
+    }
+
+    public void startState(Class<? extends ActivityState> klass,
+            Bundle data) {
+        Log.v(TAG, "startState " + klass);
+        ActivityState state = null;
+        try {
+            state = klass.newInstance();
+        } catch (Exception e) {
+            throw new AssertionError(e);
+        }
+        if (!mStack.isEmpty()) {
+            ActivityState top = getTopState();
+            top.transitionOnNextPause(top.getClass(), klass,
+                    StateTransitionAnimation.Transition.Incoming);
+            if (mIsResumed) top.onPause();
+        }
+
+        UsageStatistics.onContentViewChanged(
+                UsageStatistics.COMPONENT_GALLERY,
+                klass.getSimpleName());
+        state.initialize(mActivity, data);
+
+        mStack.push(new StateEntry(data, state));
+        state.onCreate(data, null);
+        if (mIsResumed) state.resume();
+    }
+
+    public void startStateForResult(Class<? extends ActivityState> klass,
+            int requestCode, Bundle data) {
+        Log.v(TAG, "startStateForResult " + klass + ", " + requestCode);
+        ActivityState state = null;
+        try {
+            state = klass.newInstance();
+        } catch (Exception e) {
+            throw new AssertionError(e);
+        }
+        state.initialize(mActivity, data);
+        state.mResult = new ActivityState.ResultEntry();
+        state.mResult.requestCode = requestCode;
+
+        if (!mStack.isEmpty()) {
+            ActivityState as = getTopState();
+            as.transitionOnNextPause(as.getClass(), klass,
+                    StateTransitionAnimation.Transition.Incoming);
+            as.mReceivedResults = state.mResult;
+            if (mIsResumed) as.onPause();
+        } else {
+            mResult = state.mResult;
+        }
+        UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+                klass.getSimpleName());
+        mStack.push(new StateEntry(data, state));
+        state.onCreate(data, null);
+        if (mIsResumed) state.resume();
+    }
+
+    public boolean createOptionsMenu(Menu menu) {
+        if (mStack.isEmpty()) {
+            return false;
+        } else {
+            return getTopState().onCreateActionBar(menu);
+        }
+    }
+
+    public void onConfigurationChange(Configuration config) {
+        for (StateEntry entry : mStack) {
+            entry.activityState.onConfigurationChanged(config);
+        }
+    }
+
+    public void resume() {
+        if (mIsResumed) return;
+        mIsResumed = true;
+        if (!mStack.isEmpty()) getTopState().resume();
+    }
+
+    public void pause() {
+        if (!mIsResumed) return;
+        mIsResumed = false;
+        if (!mStack.isEmpty()) getTopState().onPause();
+    }
+
+    public void notifyActivityResult(int requestCode, int resultCode, Intent data) {
+        getTopState().onStateResult(requestCode, resultCode, data);
+    }
+
+    public void clearActivityResult() {
+        if (!mStack.isEmpty()) {
+            getTopState().clearStateResult();
+        }
+    }
+
+    public int getStateCount() {
+        return mStack.size();
+    }
+
+    public boolean itemSelected(MenuItem item) {
+        if (!mStack.isEmpty()) {
+            if (getTopState().onItemSelected(item)) return true;
+            if (item.getItemId() == android.R.id.home) {
+                if (mStack.size() > 1) {
+                    getTopState().onBackPressed();
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void onBackPressed() {
+        if (!mStack.isEmpty()) {
+            getTopState().onBackPressed();
+        }
+    }
+
+    void finishState(ActivityState state) {
+        finishState(state, true);
+    }
+
+    public void clearTasks() {
+        // Remove all the states that are on top of the bottom PhotoPage state
+        while (mStack.size() > 1) {
+            mStack.pop().activityState.onDestroy();
+        }
+    }
+
+    void finishState(ActivityState state, boolean fireOnPause) {
+        // The finish() request could be rejected (only happens under Monkey),
+        // If it is rejected, we won't close the last page.
+        if (mStack.size() == 1) {
+            Activity activity = (Activity) mActivity.getAndroidContext();
+            if (mResult != null) {
+                activity.setResult(mResult.resultCode, mResult.resultData);
+            }
+            activity.finish();
+            if (!activity.isFinishing()) {
+                Log.w(TAG, "finish is rejected, keep the last state");
+                return;
+            }
+            Log.v(TAG, "no more state, finish activity");
+        }
+
+        Log.v(TAG, "finishState " + state);
+        if (state != mStack.peek().activityState) {
+            if (state.isDestroyed()) {
+                Log.d(TAG, "The state is already destroyed");
+                return;
+            } else {
+                throw new IllegalArgumentException("The stateview to be finished"
+                        + " is not at the top of the stack: " + state + ", "
+                        + mStack.peek().activityState);
+            }
+        }
+
+        // Remove the top state.
+        mStack.pop();
+        state.mIsFinishing = true;
+        ActivityState top = !mStack.isEmpty() ? mStack.peek().activityState : null;
+        if (mIsResumed && fireOnPause) {
+            if (top != null) {
+                state.transitionOnNextPause(state.getClass(), top.getClass(),
+                        StateTransitionAnimation.Transition.Outgoing);
+            }
+            state.onPause();
+        }
+        mActivity.getGLRoot().setContentPane(null);
+        state.onDestroy();
+
+        if (top != null && mIsResumed) top.resume();
+        if (top != null) {
+            UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+                    top.getClass().getSimpleName());
+        }
+    }
+
+    public void switchState(ActivityState oldState,
+            Class<? extends ActivityState> klass, Bundle data) {
+        Log.v(TAG, "switchState " + oldState + ", " + klass);
+        if (oldState != mStack.peek().activityState) {
+            throw new IllegalArgumentException("The stateview to be finished"
+                    + " is not at the top of the stack: " + oldState + ", "
+                    + mStack.peek().activityState);
+        }
+        // Remove the top state.
+        mStack.pop();
+        if (!data.containsKey(PhotoPage.KEY_APP_BRIDGE)) {
+            // Do not do the fade out stuff when we are switching camera modes
+            oldState.transitionOnNextPause(oldState.getClass(), klass,
+                    StateTransitionAnimation.Transition.Incoming);
+        }
+        if (mIsResumed) oldState.onPause();
+        oldState.onDestroy();
+
+        // Create new state.
+        ActivityState state = null;
+        try {
+            state = klass.newInstance();
+        } catch (Exception e) {
+            throw new AssertionError(e);
+        }
+        state.initialize(mActivity, data);
+        mStack.push(new StateEntry(data, state));
+        state.onCreate(data, null);
+        if (mIsResumed) state.resume();
+        UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+                klass.getSimpleName());
+    }
+
+    public void destroy() {
+        Log.v(TAG, "destroy");
+        while (!mStack.isEmpty()) {
+            mStack.pop().activityState.onDestroy();
+        }
+        mStack.clear();
+    }
+
+    @SuppressWarnings("unchecked")
+    public void restoreFromState(Bundle inState) {
+        Log.v(TAG, "restoreFromState");
+        Parcelable list[] = inState.getParcelableArray(KEY_MAIN);
+        ActivityState topState = null;
+        for (Parcelable parcelable : list) {
+            Bundle bundle = (Bundle) parcelable;
+            Class<? extends ActivityState> klass =
+                    (Class<? extends ActivityState>) bundle.getSerializable(KEY_CLASS);
+
+            Bundle data = bundle.getBundle(KEY_DATA);
+            Bundle state = bundle.getBundle(KEY_STATE);
+
+            ActivityState activityState;
+            try {
+                Log.v(TAG, "restoreFromState " + klass);
+                activityState = klass.newInstance();
+            } catch (Exception e) {
+                throw new AssertionError(e);
+            }
+            activityState.initialize(mActivity, data);
+            activityState.onCreate(data, state);
+            mStack.push(new StateEntry(data, activityState));
+            topState = activityState;
+        }
+        if (topState != null) {
+            UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+                    topState.getClass().getSimpleName());
+        }
+    }
+
+    public void saveState(Bundle outState) {
+        Log.v(TAG, "saveState");
+
+        Parcelable list[] = new Parcelable[mStack.size()];
+        int i = 0;
+        for (StateEntry entry : mStack) {
+            Bundle bundle = new Bundle();
+            bundle.putSerializable(KEY_CLASS, entry.activityState.getClass());
+            bundle.putBundle(KEY_DATA, entry.data);
+            Bundle state = new Bundle();
+            entry.activityState.onSaveState(state);
+            bundle.putBundle(KEY_STATE, state);
+            Log.v(TAG, "saveState " + entry.activityState.getClass());
+            list[i++] = bundle;
+        }
+        outState.putParcelableArray(KEY_MAIN, list);
+    }
+
+    public boolean hasStateClass(Class<? extends ActivityState> klass) {
+        for (StateEntry entry : mStack) {
+            if (klass.isInstance(entry.activityState)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public ActivityState getTopState() {
+        Utils.assertTrue(!mStack.isEmpty());
+        return mStack.peek().activityState;
+    }
+
+    private static class StateEntry {
+        public Bundle data;
+        public ActivityState activityState;
+
+        public StateEntry(Bundle data, ActivityState state) {
+            this.data = data;
+            this.activityState = state;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/app/StitchingChangeListener.java b/src/com/android/gallery3d/app/StitchingChangeListener.java
new file mode 100644
index 0000000..0b8c2d6
--- /dev/null
+++ b/src/com/android/gallery3d/app/StitchingChangeListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.net.Uri;
+
+public interface StitchingChangeListener {
+    public void onStitchingQueued(Uri uri);
+
+    public void onStitchingResult(Uri uri);
+
+    public void onStitchingProgress(Uri uri, int progress);
+}
diff --git a/src/com/android/gallery3d/app/TimeBar.java b/src/com/android/gallery3d/app/TimeBar.java
new file mode 100644
index 0000000..246346a
--- /dev/null
+++ b/src/com/android/gallery3d/app/TimeBar.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.app;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+
+/**
+ * The time bar view, which includes the current and total time, the progress
+ * bar, and the scrubber.
+ */
+public class TimeBar extends View {
+
+    public interface Listener {
+        void onScrubbingStart();
+
+        void onScrubbingMove(int time);
+
+        void onScrubbingEnd(int time, int start, int end);
+    }
+
+    // Padding around the scrubber to increase its touch target
+    private static final int SCRUBBER_PADDING_IN_DP = 10;
+
+    // The total padding, top plus bottom
+    private static final int V_PADDING_IN_DP = 30;
+
+    private static final int TEXT_SIZE_IN_DP = 14;
+
+    protected final Listener mListener;
+
+    // the bars we use for displaying the progress
+    protected final Rect mProgressBar;
+    protected final Rect mPlayedBar;
+
+    protected final Paint mProgressPaint;
+    protected final Paint mPlayedPaint;
+    protected final Paint mTimeTextPaint;
+
+    protected final Bitmap mScrubber;
+    protected int mScrubberPadding; // adds some touch tolerance around the
+                                    // scrubber
+
+    protected int mScrubberLeft;
+    protected int mScrubberTop;
+    protected int mScrubberCorrection;
+    protected boolean mScrubbing;
+    protected boolean mShowTimes;
+    protected boolean mShowScrubber;
+
+    protected int mTotalTime;
+    protected int mCurrentTime;
+
+    protected final Rect mTimeBounds;
+
+    protected int mVPaddingInPx;
+
+    public TimeBar(Context context, Listener listener) {
+        super(context);
+        mListener = Utils.checkNotNull(listener);
+
+        mShowTimes = true;
+        mShowScrubber = true;
+
+        mProgressBar = new Rect();
+        mPlayedBar = new Rect();
+
+        mProgressPaint = new Paint();
+        mProgressPaint.setColor(0xFF808080);
+        mPlayedPaint = new Paint();
+        mPlayedPaint.setColor(0xFFFFFFFF);
+
+        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+        float textSizeInPx = metrics.density * TEXT_SIZE_IN_DP;
+        mTimeTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mTimeTextPaint.setColor(0xFFCECECE);
+        mTimeTextPaint.setTextSize(textSizeInPx);
+        mTimeTextPaint.setTextAlign(Paint.Align.CENTER);
+
+        mTimeBounds = new Rect();
+        mTimeTextPaint.getTextBounds("0:00:00", 0, 7, mTimeBounds);
+
+        mScrubber = BitmapFactory.decodeResource(getResources(), R.drawable.scrubber_knob);
+        mScrubberPadding = (int) (metrics.density * SCRUBBER_PADDING_IN_DP);
+
+        mVPaddingInPx = (int) (metrics.density * V_PADDING_IN_DP);
+    }
+
+    private void update() {
+        mPlayedBar.set(mProgressBar);
+
+        if (mTotalTime > 0) {
+            mPlayedBar.right =
+                    mPlayedBar.left + (int) ((mProgressBar.width() * (long) mCurrentTime) / mTotalTime);
+        } else {
+            mPlayedBar.right = mProgressBar.left;
+        }
+
+        if (!mScrubbing) {
+            mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2;
+        }
+        invalidate();
+    }
+
+    /**
+     * @return the preferred height of this view, including invisible padding
+     */
+    public int getPreferredHeight() {
+        return mTimeBounds.height() + mVPaddingInPx + mScrubberPadding;
+    }
+
+    /**
+     * @return the height of the time bar, excluding invisible padding
+     */
+    public int getBarHeight() {
+        return mTimeBounds.height() + mVPaddingInPx;
+    }
+
+    public void setTime(int currentTime, int totalTime,
+            int trimStartTime, int trimEndTime) {
+        if (mCurrentTime == currentTime && mTotalTime == totalTime) {
+            return;
+        }
+        mCurrentTime = currentTime;
+        mTotalTime = totalTime;
+        update();
+    }
+
+    private boolean inScrubber(float x, float y) {
+        int scrubberRight = mScrubberLeft + mScrubber.getWidth();
+        int scrubberBottom = mScrubberTop + mScrubber.getHeight();
+        return mScrubberLeft - mScrubberPadding < x && x < scrubberRight + mScrubberPadding
+                && mScrubberTop - mScrubberPadding < y && y < scrubberBottom + mScrubberPadding;
+    }
+
+    private void clampScrubber() {
+        int half = mScrubber.getWidth() / 2;
+        int max = mProgressBar.right - half;
+        int min = mProgressBar.left - half;
+        mScrubberLeft = Math.min(max, Math.max(min, mScrubberLeft));
+    }
+
+    private int getScrubberTime() {
+        return (int) ((long) (mScrubberLeft + mScrubber.getWidth() / 2 - mProgressBar.left)
+                * mTotalTime / mProgressBar.width());
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int w = r - l;
+        int h = b - t;
+        if (!mShowTimes && !mShowScrubber) {
+            mProgressBar.set(0, 0, w, h);
+        } else {
+            int margin = mScrubber.getWidth() / 3;
+            if (mShowTimes) {
+                margin += mTimeBounds.width();
+            }
+            int progressY = (h + mScrubberPadding) / 2;
+            mScrubberTop = progressY - mScrubber.getHeight() / 2 + 1;
+            mProgressBar.set(
+                    getPaddingLeft() + margin, progressY,
+                    w - getPaddingRight() - margin, progressY + 4);
+        }
+        update();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // draw progress bars
+        canvas.drawRect(mProgressBar, mProgressPaint);
+        canvas.drawRect(mPlayedBar, mPlayedPaint);
+
+        // draw scrubber and timers
+        if (mShowScrubber) {
+            canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null);
+        }
+        if (mShowTimes) {
+            canvas.drawText(
+                    stringForTime(mCurrentTime),
+                            mTimeBounds.width() / 2 + getPaddingLeft(),
+                            mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1,
+                    mTimeTextPaint);
+            canvas.drawText(
+                    stringForTime(mTotalTime),
+                            getWidth() - getPaddingRight() - mTimeBounds.width() / 2,
+                            mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1,
+                    mTimeTextPaint);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mShowScrubber) {
+            int x = (int) event.getX();
+            int y = (int) event.getY();
+
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN: {
+                    mScrubberCorrection = inScrubber(x, y)
+                            ? x - mScrubberLeft
+                            : mScrubber.getWidth() / 2;
+                    mScrubbing = true;
+                    mListener.onScrubbingStart();
+                }
+                // fall-through
+                case MotionEvent.ACTION_MOVE: {
+                    mScrubberLeft = x - mScrubberCorrection;
+                    clampScrubber();
+                    mCurrentTime = getScrubberTime();
+                    mListener.onScrubbingMove(mCurrentTime);
+                    invalidate();
+                    return true;
+                }
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_UP: {
+                    mListener.onScrubbingEnd(getScrubberTime(), 0, 0);
+                    mScrubbing = false;
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    protected String stringForTime(long millis) {
+        int totalSeconds = (int) millis / 1000;
+        int seconds = totalSeconds % 60;
+        int minutes = (totalSeconds / 60) % 60;
+        int hours = totalSeconds / 3600;
+        if (hours > 0) {
+            return String.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+        } else {
+            return String.format("%02d:%02d", minutes, seconds).toString();
+        }
+    }
+
+    public void setSeekable(boolean canSeek) {
+        mShowScrubber = canSeek;
+    }
+
+}
diff --git a/src/com/android/gallery3d/app/TransitionStore.java b/src/com/android/gallery3d/app/TransitionStore.java
new file mode 100644
index 0000000..aa38ed7
--- /dev/null
+++ b/src/com/android/gallery3d/app/TransitionStore.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import java.util.HashMap;
+
+public class TransitionStore {
+    private HashMap<Object, Object> mStorage = new HashMap<Object, Object>();
+
+    public void put(Object key, Object value) {
+        mStorage.put(key, value);
+    }
+
+    public <T> void putIfNotPresent(Object key, T valueIfNull) {
+        mStorage.put(key, get(key, valueIfNull));
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T get(Object key) {
+        return (T) mStorage.get(key);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T get(Object key, T valueIfNull) {
+        T value = (T) mStorage.get(key);
+        return value == null ? valueIfNull : value;
+    }
+
+    public void clear() {
+        mStorage.clear();
+    }
+}
diff --git a/src/com/android/gallery3d/app/TrimControllerOverlay.java b/src/com/android/gallery3d/app/TrimControllerOverlay.java
new file mode 100644
index 0000000..cae0166
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimControllerOverlay.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * The controller for the Trimming Video.
+ */
+public class TrimControllerOverlay extends CommonControllerOverlay  {
+
+    public TrimControllerOverlay(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void createTimeBar(Context context) {
+        mTimeBar = new TrimTimeBar(context, this);
+    }
+
+    private void hidePlayButtonIfPlaying() {
+        if (mState == State.PLAYING) {
+            mPlayPauseReplayView.setVisibility(View.INVISIBLE);
+        }
+        if (ApiHelper.HAS_OBJECT_ANIMATION) {
+            mPlayPauseReplayView.setAlpha(1f);
+        }
+    }
+
+    @Override
+    public void showPlaying() {
+        super.showPlaying();
+        if (ApiHelper.HAS_OBJECT_ANIMATION) {
+            // Add animation to hide the play button while playing.
+            ObjectAnimator anim = ObjectAnimator.ofFloat(mPlayPauseReplayView, "alpha", 1f, 0f);
+            anim.setDuration(200);
+            anim.start();
+            anim.addListener(new AnimatorListener() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    hidePlayButtonIfPlaying();
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    hidePlayButtonIfPlaying();
+                }
+
+                @Override
+                public void onAnimationRepeat(Animator animation) {
+                }
+            });
+        } else {
+            hidePlayButtonIfPlaying();
+        }
+    }
+
+    @Override
+    public void setTimes(int currentTime, int totalTime, int trimStartTime, int trimEndTime) {
+        mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (super.onTouchEvent(event)) {
+            return true;
+        }
+
+        // The special thing here is that the State.ENDED include both cases of
+        // the video completed and current == trimEnd. Both request a replay.
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                if (mState == State.PLAYING || mState == State.PAUSED) {
+                    mListener.onPlayPause();
+                } else if (mState == State.ENDED) {
+                    if (mCanReplay) {
+                        mListener.onReplay();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                break;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/app/TrimTimeBar.java b/src/com/android/gallery3d/app/TrimTimeBar.java
new file mode 100644
index 0000000..f8dbc74
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimTimeBar.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+
+/**
+ * The trim time bar view, which includes the current and total time, the progress
+ * bar, and the scrubbers for current time, start and end time for trimming.
+ */
+public class TrimTimeBar extends TimeBar {
+
+    public static final int SCRUBBER_NONE = 0;
+    public static final int SCRUBBER_START = 1;
+    public static final int SCRUBBER_CURRENT = 2;
+    public static final int SCRUBBER_END = 3;
+
+    private int mPressedThumb = SCRUBBER_NONE;
+
+    // On touch event, the setting order is Scrubber Position -> Time ->
+    // PlayedBar. At the setTimes(), activity can update the Time directly, then
+    // PlayedBar will be updated too.
+    private int mTrimStartScrubberLeft;
+    private int mTrimEndScrubberLeft;
+
+    private int mTrimStartScrubberTop;
+    private int mTrimEndScrubberTop;
+
+    private int mTrimStartTime;
+    private int mTrimEndTime;
+
+    private final Bitmap mTrimStartScrubber;
+    private final Bitmap mTrimEndScrubber;
+    public TrimTimeBar(Context context, Listener listener) {
+        super(context, listener);
+
+        mTrimStartTime = 0;
+        mTrimEndTime = 0;
+        mTrimStartScrubberLeft = 0;
+        mTrimEndScrubberLeft = 0;
+        mTrimStartScrubberTop = 0;
+        mTrimEndScrubberTop = 0;
+
+        mTrimStartScrubber = BitmapFactory.decodeResource(getResources(),
+                R.drawable.text_select_handle_left);
+        mTrimEndScrubber = BitmapFactory.decodeResource(getResources(),
+                R.drawable.text_select_handle_right);
+        // Increase the size of this trimTimeBar, but minimize the scrubber
+        // touch padding since we have 3 scrubbers now.
+        mScrubberPadding = 0;
+        mVPaddingInPx = mVPaddingInPx * 3 / 2;
+    }
+
+    private int getBarPosFromTime(int time) {
+        return mProgressBar.left +
+                (int) ((mProgressBar.width() * (long) time) / mTotalTime);
+    }
+
+    private int trimStartScrubberTipOffset() {
+        return mTrimStartScrubber.getWidth() * 3 / 4;
+    }
+
+    private int trimEndScrubberTipOffset() {
+        return mTrimEndScrubber.getWidth() / 4;
+    }
+
+    // Based on all the time info (current, total, trimStart, trimEnd), we
+    // decide the playedBar size.
+    private void updatePlayedBarAndScrubberFromTime() {
+        // According to the Time, update the Played Bar
+        mPlayedBar.set(mProgressBar);
+        if (mTotalTime > 0) {
+            // set playedBar according to the trim time.
+            mPlayedBar.left = getBarPosFromTime(mTrimStartTime);
+            mPlayedBar.right = getBarPosFromTime(mCurrentTime);
+            if (!mScrubbing) {
+                mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2;
+                mTrimStartScrubberLeft = mPlayedBar.left - trimStartScrubberTipOffset();
+                mTrimEndScrubberLeft = getBarPosFromTime(mTrimEndTime)
+                        - trimEndScrubberTipOffset();
+            }
+        } else {
+            // If the video is not prepared, just show the scrubber at the end
+            // of progressBar
+            mPlayedBar.right = mProgressBar.left;
+            mScrubberLeft = mProgressBar.left - mScrubber.getWidth() / 2;
+            mTrimStartScrubberLeft = mProgressBar.left - trimStartScrubberTipOffset();
+            mTrimEndScrubberLeft = mProgressBar.right - trimEndScrubberTipOffset();
+        }
+    }
+
+    private void initTrimTimeIfNeeded() {
+        if (mTotalTime > 0 && mTrimEndTime == 0) {
+            mTrimEndTime = mTotalTime;
+        }
+    }
+
+    private void update() {
+        initTrimTimeIfNeeded();
+        updatePlayedBarAndScrubberFromTime();
+        invalidate();
+    }
+
+    @Override
+    public void setTime(int currentTime, int totalTime,
+            int trimStartTime, int trimEndTime) {
+        if (mCurrentTime == currentTime && mTotalTime == totalTime
+                && mTrimStartTime == trimStartTime && mTrimEndTime == trimEndTime) {
+            return;
+        }
+        mCurrentTime = currentTime;
+        mTotalTime = totalTime;
+        mTrimStartTime = trimStartTime;
+        mTrimEndTime = trimEndTime;
+        update();
+    }
+
+    private int whichScrubber(float x, float y) {
+        if (inScrubber(x, y, mTrimStartScrubberLeft, mTrimStartScrubberTop, mTrimStartScrubber)) {
+            return SCRUBBER_START;
+        } else if (inScrubber(x, y, mTrimEndScrubberLeft, mTrimEndScrubberTop, mTrimEndScrubber)) {
+            return SCRUBBER_END;
+        } else if (inScrubber(x, y, mScrubberLeft, mScrubberTop, mScrubber)) {
+            return SCRUBBER_CURRENT;
+        }
+        return SCRUBBER_NONE;
+    }
+
+    private boolean inScrubber(float x, float y, int startX, int startY, Bitmap scrubber) {
+        int scrubberRight = startX + scrubber.getWidth();
+        int scrubberBottom = startY + scrubber.getHeight();
+        return startX < x && x < scrubberRight && startY < y && y < scrubberBottom;
+    }
+
+    private int clampScrubber(int scrubberLeft, int offset, int lowerBound, int upperBound) {
+        int max = upperBound - offset;
+        int min = lowerBound - offset;
+        return Math.min(max, Math.max(min, scrubberLeft));
+    }
+
+    private int getScrubberTime(int scrubberLeft, int offset) {
+        return (int) ((long) (scrubberLeft + offset - mProgressBar.left)
+                * mTotalTime / mProgressBar.width());
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int w = r - l;
+        int h = b - t;
+        if (!mShowTimes && !mShowScrubber) {
+            mProgressBar.set(0, 0, w, h);
+        } else {
+            int margin = mScrubber.getWidth() / 3;
+            if (mShowTimes) {
+                margin += mTimeBounds.width();
+            }
+            int progressY = h / 4;
+            int scrubberY = progressY - mScrubber.getHeight() / 2 + 1;
+            mScrubberTop = scrubberY;
+            mTrimStartScrubberTop = progressY;
+            mTrimEndScrubberTop = progressY;
+            mProgressBar.set(
+                    getPaddingLeft() + margin, progressY,
+                    w - getPaddingRight() - margin, progressY + 4);
+        }
+        update();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // draw progress bars
+        canvas.drawRect(mProgressBar, mProgressPaint);
+        canvas.drawRect(mPlayedBar, mPlayedPaint);
+
+        if (mShowTimes) {
+            canvas.drawText(
+                    stringForTime(mCurrentTime),
+                            mTimeBounds.width() / 2 + getPaddingLeft(),
+                            mTimeBounds.height() / 2 +  mTrimStartScrubberTop,
+                    mTimeTextPaint);
+            canvas.drawText(
+                    stringForTime(mTotalTime),
+                            getWidth() - getPaddingRight() - mTimeBounds.width() / 2,
+                            mTimeBounds.height() / 2 +  mTrimStartScrubberTop,
+                    mTimeTextPaint);
+        }
+
+        // draw extra scrubbers
+        if (mShowScrubber) {
+            canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null);
+            canvas.drawBitmap(mTrimStartScrubber, mTrimStartScrubberLeft,
+                    mTrimStartScrubberTop, null);
+            canvas.drawBitmap(mTrimEndScrubber, mTrimEndScrubberLeft,
+                    mTrimEndScrubberTop, null);
+        }
+    }
+
+    private void updateTimeFromPos() {
+        mCurrentTime = getScrubberTime(mScrubberLeft, mScrubber.getWidth() / 2);
+        mTrimStartTime = getScrubberTime(mTrimStartScrubberLeft, trimStartScrubberTipOffset());
+        mTrimEndTime = getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset());
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mShowScrubber) {
+            int x = (int) event.getX();
+            int y = (int) event.getY();
+
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    mPressedThumb = whichScrubber(x, y);
+                    switch (mPressedThumb) {
+                        case SCRUBBER_NONE:
+                            break;
+                        case SCRUBBER_CURRENT:
+                            mScrubbing = true;
+                            mScrubberCorrection = x - mScrubberLeft;
+                            break;
+                        case SCRUBBER_START:
+                            mScrubbing = true;
+                            mScrubberCorrection = x - mTrimStartScrubberLeft;
+                            break;
+                        case SCRUBBER_END:
+                            mScrubbing = true;
+                            mScrubberCorrection = x - mTrimEndScrubberLeft;
+                            break;
+                    }
+                    if (mScrubbing == true) {
+                        mListener.onScrubbingStart();
+                        return true;
+                    }
+                    break;
+                case MotionEvent.ACTION_MOVE:
+                    if (mScrubbing) {
+                        int seekToTime = -1;
+                        int lowerBound = mTrimStartScrubberLeft + trimStartScrubberTipOffset();
+                        int upperBound = mTrimEndScrubberLeft + trimEndScrubberTipOffset();
+                        switch (mPressedThumb) {
+                            case SCRUBBER_CURRENT:
+                                mScrubberLeft = x - mScrubberCorrection;
+                                mScrubberLeft =
+                                        clampScrubber(mScrubberLeft,
+                                                mScrubber.getWidth() / 2,
+                                                lowerBound, upperBound);
+                                seekToTime = getScrubberTime(mScrubberLeft,
+                                        mScrubber.getWidth() / 2);
+                                break;
+                            case SCRUBBER_START:
+                                mTrimStartScrubberLeft = x - mScrubberCorrection;
+                                // Limit start <= end
+                                if (mTrimStartScrubberLeft > mTrimEndScrubberLeft) {
+                                    mTrimStartScrubberLeft = mTrimEndScrubberLeft;
+                                }
+                                lowerBound = mProgressBar.left;
+                                mTrimStartScrubberLeft =
+                                        clampScrubber(mTrimStartScrubberLeft,
+                                                trimStartScrubberTipOffset(),
+                                                lowerBound, upperBound);
+                                seekToTime = getScrubberTime(mTrimStartScrubberLeft,
+                                        trimStartScrubberTipOffset());
+                                break;
+                            case SCRUBBER_END:
+                                mTrimEndScrubberLeft = x - mScrubberCorrection;
+                                upperBound = mProgressBar.right;
+                                mTrimEndScrubberLeft =
+                                        clampScrubber(mTrimEndScrubberLeft,
+                                                trimEndScrubberTipOffset(),
+                                                lowerBound, upperBound);
+                                seekToTime = getScrubberTime(mTrimEndScrubberLeft,
+                                        trimEndScrubberTipOffset());
+                                break;
+                        }
+                        updateTimeFromPos();
+                        updatePlayedBarAndScrubberFromTime();
+                        if (seekToTime != -1) {
+                            mListener.onScrubbingMove(seekToTime);
+                        }
+                        invalidate();
+                        return true;
+                    }
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_UP:
+                    if (mScrubbing) {
+                        int seekToTime = 0;
+                        switch (mPressedThumb) {
+                            case SCRUBBER_CURRENT:
+                                seekToTime = getScrubberTime(mScrubberLeft,
+                                        mScrubber.getWidth() / 2);
+                                break;
+                            case SCRUBBER_START:
+                                seekToTime = getScrubberTime(mTrimStartScrubberLeft,
+                                        trimStartScrubberTipOffset());
+                                mScrubberLeft = mTrimStartScrubberLeft +
+                                        trimStartScrubberTipOffset() - mScrubber.getWidth() / 2;
+                                break;
+                            case SCRUBBER_END:
+                                seekToTime = getScrubberTime(mTrimEndScrubberLeft,
+                                        trimEndScrubberTipOffset());
+                                mScrubberLeft = mTrimEndScrubberLeft +
+                                        trimEndScrubberTipOffset() - mScrubber.getWidth() / 2;
+                                break;
+                        }
+                        updateTimeFromPos();
+                        mListener.onScrubbingEnd(seekToTime,
+                                getScrubberTime(mTrimStartScrubberLeft,
+                                        trimStartScrubberTipOffset()),
+                                getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset()));
+                        mScrubbing = false;
+                        mPressedThumb = SCRUBBER_NONE;
+                        return true;
+                    }
+                    break;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java
new file mode 100644
index 0000000..1e77281
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimVideo.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.VideoView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.android.gallery3d.util.SaveVideoFileUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class TrimVideo extends Activity implements
+        MediaPlayer.OnErrorListener,
+        MediaPlayer.OnCompletionListener,
+        ControllerOverlay.Listener {
+
+    private VideoView mVideoView;
+    private TextView mSaveVideoTextView;
+    private TrimControllerOverlay mController;
+    private Context mContext;
+    private Uri mUri;
+    private final Handler mHandler = new Handler();
+    public static final String TRIM_ACTION = "com.android.camera.action.TRIM";
+
+    public ProgressDialog mProgress;
+
+    private int mTrimStartTime = 0;
+    private int mTrimEndTime = 0;
+    private int mVideoPosition = 0;
+    public static final String KEY_TRIM_START = "trim_start";
+    public static final String KEY_TRIM_END = "trim_end";
+    public static final String KEY_VIDEO_POSITION = "video_pos";
+    private boolean mHasPaused = false;
+
+    private String mSrcVideoPath = null;
+    private static final String TIME_STAMP_NAME = "'TRIM'_yyyyMMdd_HHmmss";
+    private SaveVideoFileInfo mDstFileInfo = null;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        mContext = getApplicationContext();
+        super.onCreate(savedInstanceState);
+
+        requestWindowFeature(Window.FEATURE_ACTION_BAR);
+        requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+        ActionBar actionBar = getActionBar();
+        int displayOptions = ActionBar.DISPLAY_SHOW_HOME;
+        actionBar.setDisplayOptions(0, displayOptions);
+        displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM;
+        actionBar.setDisplayOptions(displayOptions, displayOptions);
+        actionBar.setCustomView(R.layout.trim_menu);
+
+        mSaveVideoTextView = (TextView) findViewById(R.id.start_trim);
+        mSaveVideoTextView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                trimVideo();
+            }
+        });
+        mSaveVideoTextView.setEnabled(false);
+
+        Intent intent = getIntent();
+        mUri = intent.getData();
+        mSrcVideoPath = intent.getStringExtra(PhotoPage.KEY_MEDIA_ITEM_PATH);
+        setContentView(R.layout.trim_view);
+        View rootView = findViewById(R.id.trim_view_root);
+
+        mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+
+        mController = new TrimControllerOverlay(mContext);
+        ((ViewGroup) rootView).addView(mController.getView());
+        mController.setListener(this);
+        mController.setCanReplay(true);
+
+        mVideoView.setOnErrorListener(this);
+        mVideoView.setOnCompletionListener(this);
+        mVideoView.setVideoURI(mUri);
+
+        playVideo();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (mHasPaused) {
+            mVideoView.seekTo(mVideoPosition);
+            mVideoView.resume();
+            mHasPaused = false;
+        }
+        mHandler.post(mProgressChecker);
+    }
+
+    @Override
+    public void onPause() {
+        mHasPaused = true;
+        mHandler.removeCallbacksAndMessages(null);
+        mVideoPosition = mVideoView.getCurrentPosition();
+        mVideoView.suspend();
+        super.onPause();
+    }
+
+    @Override
+    public void onStop() {
+        if (mProgress != null) {
+            mProgress.dismiss();
+            mProgress = null;
+        }
+        super.onStop();
+    }
+
+    @Override
+    public void onDestroy() {
+        mVideoView.stopPlayback();
+        super.onDestroy();
+    }
+
+    private final Runnable mProgressChecker = new Runnable() {
+        @Override
+        public void run() {
+            int pos = setProgress();
+            mHandler.postDelayed(mProgressChecker, 200 - (pos % 200));
+        }
+    };
+
+    @Override
+    public void onSaveInstanceState(Bundle savedInstanceState) {
+        savedInstanceState.putInt(KEY_TRIM_START, mTrimStartTime);
+        savedInstanceState.putInt(KEY_TRIM_END, mTrimEndTime);
+        savedInstanceState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+        super.onSaveInstanceState(savedInstanceState);
+    }
+
+    @Override
+    public void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(savedInstanceState);
+        mTrimStartTime = savedInstanceState.getInt(KEY_TRIM_START, 0);
+        mTrimEndTime = savedInstanceState.getInt(KEY_TRIM_END, 0);
+        mVideoPosition = savedInstanceState.getInt(KEY_VIDEO_POSITION, 0);
+    }
+
+    // This updates the time bar display (if necessary). It is called by
+    // mProgressChecker and also from places where the time bar needs
+    // to be updated immediately.
+    private int setProgress() {
+        mVideoPosition = mVideoView.getCurrentPosition();
+        // If the video position is smaller than the starting point of trimming,
+        // correct it.
+        if (mVideoPosition < mTrimStartTime) {
+            mVideoView.seekTo(mTrimStartTime);
+            mVideoPosition = mTrimStartTime;
+        }
+        // If the position is bigger than the end point of trimming, show the
+        // replay button and pause.
+        if (mVideoPosition >= mTrimEndTime && mTrimEndTime > 0) {
+            if (mVideoPosition > mTrimEndTime) {
+                mVideoView.seekTo(mTrimEndTime);
+                mVideoPosition = mTrimEndTime;
+            }
+            mController.showEnded();
+            mVideoView.pause();
+        }
+
+        int duration = mVideoView.getDuration();
+        if (duration > 0 && mTrimEndTime == 0) {
+            mTrimEndTime = duration;
+        }
+        mController.setTimes(mVideoPosition, duration, mTrimStartTime, mTrimEndTime);
+        return mVideoPosition;
+    }
+
+    private void playVideo() {
+        mVideoView.start();
+        mController.showPlaying();
+        setProgress();
+    }
+
+    private void pauseVideo() {
+        mVideoView.pause();
+        mController.showPaused();
+    }
+
+
+    private boolean isModified() {
+        int delta = mTrimEndTime - mTrimStartTime;
+
+        // Considering that we only trim at sync frame, we don't want to trim
+        // when the time interval is too short or too close to the origin.
+        if (delta < 100 || Math.abs(mVideoView.getDuration() - delta) < 100) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private void trimVideo() {
+
+        mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME,
+                getContentResolver(), mUri, getString(R.string.folder_download));
+        final File mSrcFile = new File(mSrcVideoPath);
+
+        showProgressDialog();
+
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    VideoUtils.startTrim(mSrcFile, mDstFileInfo.mFile,
+                            mTrimStartTime, mTrimEndTime);
+                    // Update the database for adding a new video file.
+                    SaveVideoFileUtils.insertContent(mDstFileInfo,
+                            getContentResolver(), mUri);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+                // After trimming is done, trigger the UI changed.
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        Toast.makeText(getApplicationContext(),
+                            getString(R.string.save_into, mDstFileInfo.mFolderName),
+                            Toast.LENGTH_SHORT)
+                            .show();
+                        // TODO: change trimming into a service to avoid
+                        // this progressDialog and add notification properly.
+                        if (mProgress != null) {
+                            mProgress.dismiss();
+                            mProgress = null;
+                            // Show the result only when the activity not stopped.
+                            Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
+                            intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*");
+                            intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
+                            startActivity(intent);
+                            finish();
+                        }
+                    }
+                });
+            }
+        }).start();
+    }
+
+    private void showProgressDialog() {
+        // create a background thread to trim the video.
+        // and show the progress.
+        mProgress = new ProgressDialog(this);
+        mProgress.setTitle(getString(R.string.trimming));
+        mProgress.setMessage(getString(R.string.please_wait));
+        // TODO: make this cancelable.
+        mProgress.setCancelable(false);
+        mProgress.setCanceledOnTouchOutside(false);
+        mProgress.show();
+    }
+
+    @Override
+    public void onPlayPause() {
+        if (mVideoView.isPlaying()) {
+            pauseVideo();
+        } else {
+            playVideo();
+        }
+    }
+
+    @Override
+    public void onSeekStart() {
+        pauseVideo();
+    }
+
+    @Override
+    public void onSeekMove(int time) {
+        mVideoView.seekTo(time);
+    }
+
+    @Override
+    public void onSeekEnd(int time, int start, int end) {
+        mVideoView.seekTo(time);
+        mTrimStartTime = start;
+        mTrimEndTime = end;
+        setProgress();
+        // Enable save if there's modifications
+        mSaveVideoTextView.setEnabled(isModified());
+    }
+
+    @Override
+    public void onShown() {
+    }
+
+    @Override
+    public void onHidden() {
+    }
+
+    @Override
+    public void onReplay() {
+        mVideoView.seekTo(mTrimStartTime);
+        playVideo();
+    }
+
+    @Override
+    public void onCompletion(MediaPlayer mp) {
+        mController.showEnded();
+    }
+
+    @Override
+    public boolean onError(MediaPlayer mp, int what, int extra) {
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/app/VideoUtils.java b/src/com/android/gallery3d/app/VideoUtils.java
new file mode 100644
index 0000000..a3c3ef2
--- /dev/null
+++ b/src/com/android/gallery3d/app/VideoUtils.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+// Modified example based on mp4parser google code open source project.
+// http://code.google.com/p/mp4parser/source/browse/trunk/examples/src/main/java/com/googlecode/mp4parser/ShortenExample.java
+
+package com.android.gallery3d.app;
+
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaMuxer;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.coremedia.iso.IsoFile;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.googlecode.mp4parser.authoring.Movie;
+import com.googlecode.mp4parser.authoring.Track;
+import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
+import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
+import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+public class VideoUtils {
+    private static final String LOGTAG = "VideoUtils";
+    private static final int DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024;
+
+    /**
+     * Remove the sound track.
+     */
+    public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo)
+            throws IOException {
+        if (ApiHelper.HAS_MEDIA_MUXER) {
+            genVideoUsingMuxer(filePath, dstFileInfo.mFile.getPath(), -1, -1,
+                    false, true);
+        } else {
+            startMuteUsingMp4Parser(filePath, dstFileInfo);
+        }
+    }
+
+    /**
+     * Shortens/Crops tracks
+     */
+    public static void startTrim(File src, File dst, int startMs, int endMs)
+            throws IOException {
+        if (ApiHelper.HAS_MEDIA_MUXER) {
+            genVideoUsingMuxer(src.getPath(), dst.getPath(), startMs, endMs,
+                    true, true);
+        } else {
+            trimUsingMp4Parser(src, dst, startMs, endMs);
+        }
+    }
+
+    private static void startMuteUsingMp4Parser(String filePath,
+            SaveVideoFileInfo dstFileInfo) throws FileNotFoundException, IOException {
+        File dst = dstFileInfo.mFile;
+        File src = new File(filePath);
+        RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
+        Movie movie = MovieCreator.build(randomAccessFile.getChannel());
+
+        // remove all tracks we will create new tracks from the old
+        List<Track> tracks = movie.getTracks();
+        movie.setTracks(new LinkedList<Track>());
+
+        for (Track track : tracks) {
+            if (track.getHandler().equals("vide")) {
+                movie.addTrack(track);
+            }
+        }
+        writeMovieIntoFile(dst, movie);
+        randomAccessFile.close();
+    }
+
+    private static void writeMovieIntoFile(File dst, Movie movie)
+            throws IOException {
+        if (!dst.exists()) {
+            dst.createNewFile();
+        }
+
+        IsoFile out = new DefaultMp4Builder().build(movie);
+        FileOutputStream fos = new FileOutputStream(dst);
+        FileChannel fc = fos.getChannel();
+        out.getBox(fc); // This one build up the memory.
+
+        fc.close();
+        fos.close();
+    }
+
+    /**
+     * @param srcPath the path of source video file.
+     * @param dstPath the path of destination video file.
+     * @param startMs starting time in milliseconds for trimming. Set to
+     *            negative if starting from beginning.
+     * @param endMs end time for trimming in milliseconds. Set to negative if
+     *            no trimming at the end.
+     * @param useAudio true if keep the audio track from the source.
+     * @param useVideo true if keep the video track from the source.
+     * @throws IOException
+     */
+    private static void genVideoUsingMuxer(String srcPath, String dstPath,
+            int startMs, int endMs, boolean useAudio, boolean useVideo)
+            throws IOException {
+        // Set up MediaExtractor to read from the source.
+        MediaExtractor extractor = new MediaExtractor();
+        extractor.setDataSource(srcPath);
+
+        int trackCount = extractor.getTrackCount();
+
+        // Set up MediaMuxer for the destination.
+        MediaMuxer muxer;
+        muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+        // Set up the tracks and retrieve the max buffer size for selected
+        // tracks.
+        HashMap<Integer, Integer> indexMap = new HashMap<Integer,
+                Integer>(trackCount);
+        int bufferSize = -1;
+        for (int i = 0; i < trackCount; i++) {
+            MediaFormat format = extractor.getTrackFormat(i);
+            String mime = format.getString(MediaFormat.KEY_MIME);
+
+            boolean selectCurrentTrack = false;
+
+            if (mime.startsWith("audio/") && useAudio) {
+                selectCurrentTrack = true;
+            } else if (mime.startsWith("video/") && useVideo) {
+                selectCurrentTrack = true;
+            }
+
+            if (selectCurrentTrack) {
+                extractor.selectTrack(i);
+                int dstIndex = muxer.addTrack(format);
+                indexMap.put(i, dstIndex);
+                if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
+                    int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
+                    bufferSize = newSize > bufferSize ? newSize : bufferSize;
+                }
+            }
+        }
+
+        if (bufferSize < 0) {
+            bufferSize = DEFAULT_BUFFER_SIZE;
+        }
+
+        // Set up the orientation and starting time for extractor.
+        MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever();
+        retrieverSrc.setDataSource(srcPath);
+        String degreesString = retrieverSrc.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+        if (degreesString != null) {
+            int degrees = Integer.parseInt(degreesString);
+            if (degrees >= 0) {
+                muxer.setOrientationHint(degrees);
+            }
+        }
+
+        if (startMs > 0) {
+            extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
+        }
+
+        // Copy the samples from MediaExtractor to MediaMuxer. We will loop
+        // for copying each sample and stop when we get to the end of the source
+        // file or exceed the end time of the trimming.
+        int offset = 0;
+        int trackIndex = -1;
+        ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
+        BufferInfo bufferInfo = new BufferInfo();
+
+        muxer.start();
+        while (true) {
+            bufferInfo.offset = offset;
+            bufferInfo.size = extractor.readSampleData(dstBuf, offset);
+            if (bufferInfo.size < 0) {
+                Log.d(LOGTAG, "Saw input EOS.");
+                bufferInfo.size = 0;
+                break;
+            } else {
+                bufferInfo.presentationTimeUs = extractor.getSampleTime();
+                if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) {
+                    Log.d(LOGTAG, "The current sample is over the trim end time.");
+                    break;
+                } else {
+                    bufferInfo.flags = extractor.getSampleFlags();
+                    trackIndex = extractor.getSampleTrackIndex();
+
+                    muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
+                            bufferInfo);
+                    extractor.advance();
+                }
+            }
+        }
+
+        muxer.stop();
+        muxer.release();
+        return;
+    }
+
+    private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs)
+            throws FileNotFoundException, IOException {
+        RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
+        Movie movie = MovieCreator.build(randomAccessFile.getChannel());
+
+        // remove all tracks we will create new tracks from the old
+        List<Track> tracks = movie.getTracks();
+        movie.setTracks(new LinkedList<Track>());
+
+        double startTime = startMs / 1000;
+        double endTime = endMs / 1000;
+
+        boolean timeCorrected = false;
+
+        // Here we try to find a track that has sync samples. Since we can only
+        // start decoding at such a sample we SHOULD make sure that the start of
+        // the new fragment is exactly such a frame.
+        for (Track track : tracks) {
+            if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
+                if (timeCorrected) {
+                    // This exception here could be a false positive in case we
+                    // have multiple tracks with sync samples at exactly the
+                    // same positions. E.g. a single movie containing multiple
+                    // qualities of the same video (Microsoft Smooth Streaming
+                    // file)
+                    throw new RuntimeException(
+                            "The startTime has already been corrected by" +
+                            " another track with SyncSample. Not Supported.");
+                }
+                startTime = correctTimeToSyncSample(track, startTime, false);
+                endTime = correctTimeToSyncSample(track, endTime, true);
+                timeCorrected = true;
+            }
+        }
+
+        for (Track track : tracks) {
+            long currentSample = 0;
+            double currentTime = 0;
+            long startSample = -1;
+            long endSample = -1;
+
+            for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+                TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+                for (int j = 0; j < entry.getCount(); j++) {
+                    // entry.getDelta() is the amount of time the current sample
+                    // covers.
+
+                    if (currentTime <= startTime) {
+                        // current sample is still before the new starttime
+                        startSample = currentSample;
+                    }
+                    if (currentTime <= endTime) {
+                        // current sample is after the new start time and still
+                        // before the new endtime
+                        endSample = currentSample;
+                    } else {
+                        // current sample is after the end of the cropped video
+                        break;
+                    }
+                    currentTime += (double) entry.getDelta()
+                            / (double) track.getTrackMetaData().getTimescale();
+                    currentSample++;
+                }
+            }
+            movie.addTrack(new CroppedTrack(track, startSample, endSample));
+        }
+        writeMovieIntoFile(dst, movie);
+        randomAccessFile.close();
+    }
+
+    private static double correctTimeToSyncSample(Track track, double cutHere,
+            boolean next) {
+        double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
+        long currentSample = 0;
+        double currentTime = 0;
+        for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+            TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+            for (int j = 0; j < entry.getCount(); j++) {
+                if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
+                    // samples always start with 1 but we start with zero
+                    // therefore +1
+                    timeOfSyncSamples[Arrays.binarySearch(
+                            track.getSyncSamples(), currentSample + 1)] = currentTime;
+                }
+                currentTime += (double) entry.getDelta()
+                        / (double) track.getTrackMetaData().getTimescale();
+                currentSample++;
+            }
+        }
+        double previous = 0;
+        for (double timeOfSyncSample : timeOfSyncSamples) {
+            if (timeOfSyncSample > cutHere) {
+                if (next) {
+                    return timeOfSyncSample;
+                } else {
+                    return previous;
+                }
+            }
+            previous = timeOfSyncSample;
+        }
+        return timeOfSyncSamples[timeOfSyncSamples.length - 1];
+    }
+
+}
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
new file mode 100644
index 0000000..b0a26c2
--- /dev/null
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -0,0 +1,135 @@
+/*
+ * 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.gallery3d.app;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Display;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+
+/**
+ * Wallpaper picker for the gallery application. This just redirects to the
+ * standard pick action.
+ */
+public class Wallpaper extends Activity {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Wallpaper";
+
+    private static final String IMAGE_TYPE = "image/*";
+    private static final String KEY_STATE = "activity-state";
+    private static final String KEY_PICKED_ITEM = "picked-item";
+
+    private static final int STATE_INIT = 0;
+    private static final int STATE_PHOTO_PICKED = 1;
+
+    private int mState = STATE_INIT;
+    private Uri mPickedItem;
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        if (bundle != null) {
+            mState = bundle.getInt(KEY_STATE);
+            mPickedItem = (Uri) bundle.getParcelable(KEY_PICKED_ITEM);
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle saveState) {
+        saveState.putInt(KEY_STATE, mState);
+        if (mPickedItem != null) {
+            saveState.putParcelable(KEY_PICKED_ITEM, mPickedItem);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
+    private Point getDefaultDisplaySize(Point size) {
+        Display d = getWindowManager().getDefaultDisplay();
+        if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) {
+            d.getSize(size);
+        } else {
+            size.set(d.getWidth(), d.getHeight());
+        }
+        return size;
+    }
+
+    @SuppressWarnings("fallthrough")
+    @Override
+    protected void onResume() {
+        super.onResume();
+        Intent intent = getIntent();
+        switch (mState) {
+            case STATE_INIT: {
+                mPickedItem = intent.getData();
+                if (mPickedItem == null) {
+                    Intent request = new Intent(Intent.ACTION_GET_CONTENT)
+                            .setClass(this, DialogPicker.class)
+                            .setType(IMAGE_TYPE);
+                    startActivityForResult(request, STATE_PHOTO_PICKED);
+                    return;
+                }
+                mState = STATE_PHOTO_PICKED;
+                // fall-through
+            }
+            case STATE_PHOTO_PICKED: {
+                int width = getWallpaperDesiredMinimumWidth();
+                int height = getWallpaperDesiredMinimumHeight();
+                Point size = getDefaultDisplaySize(new Point());
+                float spotlightX = (float) size.x / width;
+                float spotlightY = (float) size.y / height;
+                Intent request = new Intent(CropActivity.CROP_ACTION)
+                        .setDataAndType(mPickedItem, IMAGE_TYPE)
+                        .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+                        .putExtra(CropExtras.KEY_OUTPUT_X, width)
+                        .putExtra(CropExtras.KEY_OUTPUT_Y, height)
+                        .putExtra(CropExtras.KEY_ASPECT_X, width)
+                        .putExtra(CropExtras.KEY_ASPECT_Y, height)
+                        .putExtra(CropExtras.KEY_SPOTLIGHT_X, spotlightX)
+                        .putExtra(CropExtras.KEY_SPOTLIGHT_Y, spotlightY)
+                        .putExtra(CropExtras.KEY_SCALE, true)
+                        .putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true)
+                        .putExtra(CropExtras.KEY_SET_AS_WALLPAPER, true);
+                startActivity(request);
+                finish();
+            }
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode != RESULT_OK) {
+            setResult(resultCode);
+            finish();
+            return;
+        }
+        mState = requestCode;
+        if (mState == STATE_PHOTO_PICKED) {
+            mPickedItem = data.getData();
+        }
+
+        // onResume() would be called next
+    }
+}
diff --git a/src/com/android/gallery3d/data/ActionImage.java b/src/com/android/gallery3d/data/ActionImage.java
new file mode 100644
index 0000000..58e30b1
--- /dev/null
+++ b/src/com/android/gallery3d/data/ActionImage.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class ActionImage extends MediaItem {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ActionImage";
+    private GalleryApp mApplication;
+    private int mResourceId;
+
+    public ActionImage(Path path, GalleryApp application, int resourceId) {
+        super(path, nextVersionNumber());
+        mApplication = Utils.checkNotNull(application);
+        mResourceId = resourceId;
+    }
+
+    @Override
+    public Job<Bitmap> requestImage(int type) {
+        return new BitmapJob(type);
+    }
+
+    @Override
+    public Job<BitmapRegionDecoder> requestLargeImage() {
+        return null;
+    }
+
+    private class BitmapJob implements Job<Bitmap> {
+        private int mType;
+
+        protected BitmapJob(int type) {
+            mType = type;
+        }
+
+        @Override
+        public Bitmap run(JobContext jc) {
+            int targetSize = MediaItem.getTargetSize(mType);
+            Bitmap bitmap = BitmapFactory.decodeResource(mApplication.getResources(),
+                    mResourceId);
+
+            if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+                bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
+            } else {
+                bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
+            }
+            return bitmap;
+        }
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return SUPPORT_ACTION;
+    }
+
+    @Override
+    public int getMediaType() {
+        return MEDIA_TYPE_UNKNOWN;
+    }
+
+    @Override
+    public Uri getContentUri() {
+        return null;
+    }
+
+    @Override
+    public String getMimeType() {
+        return "";
+    }
+
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
+}
diff --git a/src/com/android/gallery3d/data/BucketHelper.java b/src/com/android/gallery3d/data/BucketHelper.java
new file mode 100644
index 0000000..3418daf
--- /dev/null
+++ b/src/com/android/gallery3d/data/BucketHelper.java
@@ -0,0 +1,241 @@
+package com.android.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Files;
+import android.provider.MediaStore.Files.FileColumns;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+class BucketHelper {
+
+    private static final String TAG = "BucketHelper";
+    private static final String EXTERNAL_MEDIA = "external";
+
+    // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory
+    // name of where an image or video is in. BUCKET_ID is a hash of the path
+    // name of that directory (see computeBucketValues() in MediaProvider for
+    // details). MEDIA_TYPE is video, image, audio, etc.
+    //
+    // The "albums" are not explicitly recorded in the database, but each image
+    // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an
+    // "album" to be the collection of images/videos which have the same value
+    // for the two columns.
+    //
+    // The goal of the query (used in loadSubMediaSetsFromFilesTable()) is to
+    // find all albums, that is, all unique values for (BUCKET_ID, MEDIA_TYPE).
+    // In the meantime sort them by the timestamp of the latest image/video in
+    // each of the album.
+    //
+    // The order of columns below is important: it must match to the index in
+    // MediaStore.
+    private static final String[] PROJECTION_BUCKET = {
+            ImageColumns.BUCKET_ID,
+            FileColumns.MEDIA_TYPE,
+            ImageColumns.BUCKET_DISPLAY_NAME};
+
+    // The indices should match the above projections.
+    private static final int INDEX_BUCKET_ID = 0;
+    private static final int INDEX_MEDIA_TYPE = 1;
+    private static final int INDEX_BUCKET_NAME = 2;
+
+    // We want to order the albums by reverse chronological order. We abuse the
+    // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement.
+    // The template for "WHERE" parameter is like:
+    //    SELECT ... FROM ... WHERE (%s)
+    // and we make it look like:
+    //    SELECT ... FROM ... WHERE (1) GROUP BY 1,(2)
+    // The "(1)" means true. The "1,(2)" means the first two columns specified
+    // after SELECT. Note that because there is a ")" in the template, we use
+    // "(2" to match it.
+    private static final String BUCKET_GROUP_BY = "1) GROUP BY 1,(2";
+
+    private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
+
+    // Before HoneyComb there is no Files table. Thus, we need to query the
+    // bucket info from the Images and Video tables and then merge them
+    // together.
+    //
+    // A bucket can exist in both tables. In this case, we need to find the
+    // latest timestamp from the two tables and sort ourselves. So we add the
+    // MAX(date_taken) to the projection and remove the media_type since we
+    // already know the media type from the table we query from.
+    private static final String[] PROJECTION_BUCKET_IN_ONE_TABLE = {
+            ImageColumns.BUCKET_ID,
+            "MAX(datetaken)",
+            ImageColumns.BUCKET_DISPLAY_NAME};
+
+    // We keep the INDEX_BUCKET_ID and INDEX_BUCKET_NAME the same as
+    // PROJECTION_BUCKET so we can reuse the values defined before.
+    private static final int INDEX_DATE_TAKEN = 1;
+
+    // When query from the Images or Video tables, we only need to group by BUCKET_ID.
+    private static final String BUCKET_GROUP_BY_IN_ONE_TABLE = "1) GROUP BY (1";
+
+    public static BucketEntry[] loadBucketEntries(
+            JobContext jc, ContentResolver resolver, int type) {
+        if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+            return loadBucketEntriesFromFilesTable(jc, resolver, type);
+        } else {
+            return loadBucketEntriesFromImagesAndVideoTable(jc, resolver, type);
+        }
+    }
+
+    private static void updateBucketEntriesFromTable(JobContext jc,
+            ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets) {
+        Cursor cursor = resolver.query(tableUri, PROJECTION_BUCKET_IN_ONE_TABLE,
+                BUCKET_GROUP_BY_IN_ONE_TABLE, null, null);
+        if (cursor == null) {
+            Log.w(TAG, "cannot open media database: " + tableUri);
+            return;
+        }
+        try {
+            while (cursor.moveToNext()) {
+                int bucketId = cursor.getInt(INDEX_BUCKET_ID);
+                int dateTaken = cursor.getInt(INDEX_DATE_TAKEN);
+                BucketEntry entry = buckets.get(bucketId);
+                if (entry == null) {
+                    entry = new BucketEntry(bucketId, cursor.getString(INDEX_BUCKET_NAME));
+                    buckets.put(bucketId, entry);
+                    entry.dateTaken = dateTaken;
+                } else {
+                    entry.dateTaken = Math.max(entry.dateTaken, dateTaken);
+                }
+            }
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+    }
+
+    private static BucketEntry[] loadBucketEntriesFromImagesAndVideoTable(
+            JobContext jc, ContentResolver resolver, int type) {
+        HashMap<Integer, BucketEntry> buckets = new HashMap<Integer, BucketEntry>(64);
+        if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+            updateBucketEntriesFromTable(
+                    jc, resolver, Images.Media.EXTERNAL_CONTENT_URI, buckets);
+        }
+        if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+            updateBucketEntriesFromTable(
+                    jc, resolver, Video.Media.EXTERNAL_CONTENT_URI, buckets);
+        }
+        BucketEntry[] entries = buckets.values().toArray(new BucketEntry[buckets.size()]);
+        Arrays.sort(entries, new Comparator<BucketEntry>() {
+            @Override
+            public int compare(BucketEntry a, BucketEntry b) {
+                // sorted by dateTaken in descending order
+                return b.dateTaken - a.dateTaken;
+            }
+        });
+        return entries;
+    }
+
+    private static BucketEntry[] loadBucketEntriesFromFilesTable(
+            JobContext jc, ContentResolver resolver, int type) {
+        Uri uri = getFilesContentUri();
+
+        Cursor cursor = resolver.query(uri,
+                PROJECTION_BUCKET, BUCKET_GROUP_BY,
+                null, BUCKET_ORDER_BY);
+        if (cursor == null) {
+            Log.w(TAG, "cannot open local database: " + uri);
+            return new BucketEntry[0];
+        }
+        ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
+        int typeBits = 0;
+        if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+            typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
+        }
+        if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+            typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
+        }
+        try {
+            while (cursor.moveToNext()) {
+                if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
+                    BucketEntry entry = new BucketEntry(
+                            cursor.getInt(INDEX_BUCKET_ID),
+                            cursor.getString(INDEX_BUCKET_NAME));
+                    if (!buffer.contains(entry)) {
+                        buffer.add(entry);
+                    }
+                }
+                if (jc.isCancelled()) return null;
+            }
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+        return buffer.toArray(new BucketEntry[buffer.size()]);
+    }
+
+    private static String getBucketNameInTable(
+            ContentResolver resolver, Uri tableUri, int bucketId) {
+        String selectionArgs[] = new String[] {String.valueOf(bucketId)};
+        Uri uri = tableUri.buildUpon()
+                .appendQueryParameter("limit", "1")
+                .build();
+        Cursor cursor = resolver.query(uri, PROJECTION_BUCKET_IN_ONE_TABLE,
+                "bucket_id = ?", selectionArgs, null);
+        try {
+            if (cursor != null && cursor.moveToNext()) {
+                return cursor.getString(INDEX_BUCKET_NAME);
+            }
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+        return null;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static Uri getFilesContentUri() {
+        return Files.getContentUri(EXTERNAL_MEDIA);
+    }
+
+    public static String getBucketName(ContentResolver resolver, int bucketId) {
+        if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+            String result = getBucketNameInTable(resolver, getFilesContentUri(), bucketId);
+            return result == null ? "" : result;
+        } else {
+            String result = getBucketNameInTable(
+                    resolver, Images.Media.EXTERNAL_CONTENT_URI, bucketId);
+            if (result != null) return result;
+            result = getBucketNameInTable(
+                    resolver, Video.Media.EXTERNAL_CONTENT_URI, bucketId);
+            return result == null ? "" : result;
+        }
+    }
+
+    public static class BucketEntry {
+        public String bucketName;
+        public int bucketId;
+        public int dateTaken;
+
+        public BucketEntry(int id, String name) {
+            bucketId = id;
+            bucketName = Utils.ensureNotNull(name);
+        }
+
+        @Override
+        public int hashCode() {
+            return bucketId;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (!(object instanceof BucketEntry)) return false;
+            BucketEntry entry = (BucketEntry) object;
+            return bucketId == entry.bucketId;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/BytesBufferPool.java b/src/com/android/gallery3d/data/BytesBufferPool.java
new file mode 100644
index 0000000..d2da323
--- /dev/null
+++ b/src/com/android/gallery3d/data/BytesBufferPool.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class BytesBufferPool {
+
+    private static final int READ_STEP = 4096;
+
+    public static class BytesBuffer {
+        public byte[] data;
+        public int offset;
+        public int length;
+
+        private BytesBuffer(int capacity) {
+            this.data = new byte[capacity];
+        }
+
+        // an helper function to read content from FileDescriptor
+        public void readFrom(JobContext jc, FileDescriptor fd) throws IOException {
+            FileInputStream fis = new FileInputStream(fd);
+            length = 0;
+            try {
+                int capacity = data.length;
+                while (true) {
+                    int step = Math.min(READ_STEP, capacity - length);
+                    int rc = fis.read(data, length, step);
+                    if (rc < 0 || jc.isCancelled()) return;
+                    length += rc;
+
+                    if (length == capacity) {
+                        byte[] newData = new byte[data.length * 2];
+                        System.arraycopy(data, 0, newData, 0, data.length);
+                        data = newData;
+                        capacity = data.length;
+                    }
+                }
+            } finally {
+                fis.close();
+            }
+        }
+    }
+
+    private final int mPoolSize;
+    private final int mBufferSize;
+    private final ArrayList<BytesBuffer> mList;
+
+    public BytesBufferPool(int poolSize, int bufferSize) {
+        mList = new ArrayList<BytesBuffer>(poolSize);
+        mPoolSize = poolSize;
+        mBufferSize = bufferSize;
+    }
+
+    public synchronized BytesBuffer get() {
+        int n = mList.size();
+        return n > 0 ? mList.remove(n - 1) : new BytesBuffer(mBufferSize);
+    }
+
+    public synchronized void recycle(BytesBuffer buffer) {
+        if (buffer.data.length != mBufferSize) return;
+        if (mList.size() < mPoolSize) {
+            buffer.offset = 0;
+            buffer.length = 0;
+            mList.add(buffer);
+        }
+    }
+
+    public synchronized void clear() {
+        mList.clear();
+    }
+}
diff --git a/src/com/android/gallery3d/data/CameraShortcutImage.java b/src/com/android/gallery3d/data/CameraShortcutImage.java
new file mode 100644
index 0000000..865270b
--- /dev/null
+++ b/src/com/android/gallery3d/data/CameraShortcutImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+
+public class CameraShortcutImage extends ActionImage {
+    @SuppressWarnings("unused")
+    private static final String TAG = "CameraShortcutImage";
+
+    public CameraShortcutImage(Path path, GalleryApp application) {
+        super(path, application, R.drawable.placeholder_camera);
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return super.getSupportedOperations() | SUPPORT_CAMERA_SHORTCUT;
+    }
+}
diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java
new file mode 100644
index 0000000..558a864
--- /dev/null
+++ b/src/com/android/gallery3d/data/ChangeNotifier.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// This handles change notification for media sets.
+public class ChangeNotifier {
+
+    private MediaSet mMediaSet;
+    private AtomicBoolean mContentDirty = new AtomicBoolean(true);
+
+    public ChangeNotifier(MediaSet set, Uri uri, GalleryApp application) {
+        mMediaSet = set;
+        application.getDataManager().registerChangeNotifier(uri, this);
+    }
+
+    public ChangeNotifier(MediaSet set, Uri[] uris, GalleryApp application) {
+        mMediaSet = set;
+        for (int i = 0; i < uris.length; i++) {
+            application.getDataManager().registerChangeNotifier(uris[i], this);
+        }
+    }
+
+    // Returns the dirty flag and clear it.
+    public boolean isDirty() {
+        return mContentDirty.compareAndSet(true, false);
+    }
+
+    public void fakeChange() {
+        onChange(false);
+    }
+
+    protected void onChange(boolean selfChange) {
+        if (mContentDirty.compareAndSet(false, true)) {
+            mMediaSet.notifyContentChanged();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/data/ClusterAlbum.java b/src/com/android/gallery3d/data/ClusterAlbum.java
new file mode 100644
index 0000000..8681952
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbum.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import java.util.ArrayList;
+
+public class ClusterAlbum extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ClusterAlbum";
+    private ArrayList<Path> mPaths = new ArrayList<Path>();
+    private String mName = "";
+    private DataManager mDataManager;
+    private MediaSet mClusterAlbumSet;
+    private MediaItem mCover;
+
+    public ClusterAlbum(Path path, DataManager dataManager,
+            MediaSet clusterAlbumSet) {
+        super(path, nextVersionNumber());
+        mDataManager = dataManager;
+        mClusterAlbumSet = clusterAlbumSet;
+        mClusterAlbumSet.addContentListener(this);
+    }
+
+    public void setCoverMediaItem(MediaItem cover) {
+        mCover = cover;
+    }
+
+    @Override
+    public MediaItem getCoverMediaItem() {
+        return mCover != null ? mCover : super.getCoverMediaItem();
+    }
+
+    void setMediaItems(ArrayList<Path> paths) {
+        mPaths = paths;
+    }
+
+    ArrayList<Path> getMediaItems() {
+        return mPaths;
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return mPaths.size();
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        return getMediaItemFromPath(mPaths, start, count, mDataManager);
+    }
+
+    public static ArrayList<MediaItem> getMediaItemFromPath(
+            ArrayList<Path> paths, int start, int count,
+            DataManager dataManager) {
+        if (start >= paths.size()) {
+            return new ArrayList<MediaItem>();
+        }
+        int end = Math.min(start + count, paths.size());
+        ArrayList<Path> subset = new ArrayList<Path>(paths.subList(start, end));
+        final MediaItem[] buf = new MediaItem[end - start];
+        ItemConsumer consumer = new ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                buf[index] = item;
+            }
+        };
+        dataManager.mapMediaItems(subset, consumer, 0);
+        ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start);
+        for (int i = 0; i < buf.length; i++) {
+            result.add(buf[i]);
+        }
+        return result;
+    }
+
+    @Override
+    protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) {
+        mDataManager.mapMediaItems(mPaths, consumer, startIndex);
+        return mPaths.size();
+    }
+
+    @Override
+    public int getTotalMediaItemCount() {
+        return mPaths.size();
+    }
+
+    @Override
+    public long reload() {
+        if (mClusterAlbumSet.reload() > mDataVersion) {
+            mDataVersion = nextVersionNumber();
+        }
+        return mDataVersion;
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return SUPPORT_SHARE | SUPPORT_DELETE | SUPPORT_INFO;
+    }
+
+    @Override
+    public void delete() {
+        ItemConsumer consumer = new ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) {
+                    item.delete();
+                }
+            }
+        };
+        mDataManager.mapMediaItems(mPaths, consumer, 0);
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/data/ClusterAlbumSet.java b/src/com/android/gallery3d/data/ClusterAlbumSet.java
new file mode 100644
index 0000000..cb212ba
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class ClusterAlbumSet extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ClusterAlbumSet";
+    private GalleryApp mApplication;
+    private MediaSet mBaseSet;
+    private int mKind;
+    private ArrayList<ClusterAlbum> mAlbums = new ArrayList<ClusterAlbum>();
+    private boolean mFirstReloadDone;
+
+    public ClusterAlbumSet(Path path, GalleryApp application,
+            MediaSet baseSet, int kind) {
+        super(path, INVALID_DATA_VERSION);
+        mApplication = application;
+        mBaseSet = baseSet;
+        mKind = kind;
+        baseSet.addContentListener(this);
+    }
+
+    @Override
+    public MediaSet getSubMediaSet(int index) {
+        return mAlbums.get(index);
+    }
+
+    @Override
+    public int getSubMediaSetCount() {
+        return mAlbums.size();
+    }
+
+    @Override
+    public String getName() {
+        return mBaseSet.getName();
+    }
+
+    @Override
+    public long reload() {
+        if (mBaseSet.reload() > mDataVersion) {
+            if (mFirstReloadDone) {
+                updateClustersContents();
+            } else {
+                updateClusters();
+                mFirstReloadDone = true;
+            }
+            mDataVersion = nextVersionNumber();
+        }
+        return mDataVersion;
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    private void updateClusters() {
+        mAlbums.clear();
+        Clustering clustering;
+        Context context = mApplication.getAndroidContext();
+        switch (mKind) {
+            case ClusterSource.CLUSTER_ALBUMSET_TIME:
+                clustering = new TimeClustering(context);
+                break;
+            case ClusterSource.CLUSTER_ALBUMSET_LOCATION:
+                clustering = new LocationClustering(context);
+                break;
+            case ClusterSource.CLUSTER_ALBUMSET_TAG:
+                clustering = new TagClustering(context);
+                break;
+            case ClusterSource.CLUSTER_ALBUMSET_FACE:
+                clustering = new FaceClustering(context);
+                break;
+            default: /* CLUSTER_ALBUMSET_SIZE */
+                clustering = new SizeClustering(context);
+                break;
+        }
+
+        clustering.run(mBaseSet);
+        int n = clustering.getNumberOfClusters();
+        DataManager dataManager = mApplication.getDataManager();
+        for (int i = 0; i < n; i++) {
+            Path childPath;
+            String childName = clustering.getClusterName(i);
+            if (mKind == ClusterSource.CLUSTER_ALBUMSET_TAG) {
+                childPath = mPath.getChild(Uri.encode(childName));
+            } else if (mKind == ClusterSource.CLUSTER_ALBUMSET_SIZE) {
+                long minSize = ((SizeClustering) clustering).getMinSize(i);
+                childPath = mPath.getChild(minSize);
+            } else {
+                childPath = mPath.getChild(i);
+            }
+
+            ClusterAlbum album;
+            synchronized (DataManager.LOCK) {
+                album = (ClusterAlbum) dataManager.peekMediaObject(childPath);
+                if (album == null) {
+                    album = new ClusterAlbum(childPath, dataManager, this);
+                }
+            }
+            album.setMediaItems(clustering.getCluster(i));
+            album.setName(childName);
+            album.setCoverMediaItem(clustering.getClusterCover(i));
+            mAlbums.add(album);
+        }
+    }
+
+    private void updateClustersContents() {
+        final HashSet<Path> existing = new HashSet<Path>();
+        mBaseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                existing.add(item.getPath());
+            }
+        });
+
+        int n = mAlbums.size();
+
+        // The loop goes backwards because we may remove empty albums from
+        // mAlbums.
+        for (int i = n - 1; i >= 0; i--) {
+            ArrayList<Path> oldPaths = mAlbums.get(i).getMediaItems();
+            ArrayList<Path> newPaths = new ArrayList<Path>();
+            int m = oldPaths.size();
+            for (int j = 0; j < m; j++) {
+                Path p = oldPaths.get(j);
+                if (existing.contains(p)) {
+                    newPaths.add(p);
+                }
+            }
+            mAlbums.get(i).setMediaItems(newPaths);
+            if (newPaths.isEmpty()) {
+                mAlbums.remove(i);
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/ClusterSource.java b/src/com/android/gallery3d/data/ClusterSource.java
new file mode 100644
index 0000000..a1f22e5
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterSource.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+class ClusterSource extends MediaSource {
+    static final int CLUSTER_ALBUMSET_TIME = 0;
+    static final int CLUSTER_ALBUMSET_LOCATION = 1;
+    static final int CLUSTER_ALBUMSET_TAG = 2;
+    static final int CLUSTER_ALBUMSET_SIZE = 3;
+    static final int CLUSTER_ALBUMSET_FACE = 4;
+
+    static final int CLUSTER_ALBUM_TIME = 0x100;
+    static final int CLUSTER_ALBUM_LOCATION = 0x101;
+    static final int CLUSTER_ALBUM_TAG = 0x102;
+    static final int CLUSTER_ALBUM_SIZE = 0x103;
+    static final int CLUSTER_ALBUM_FACE = 0x104;
+
+    GalleryApp mApplication;
+    PathMatcher mMatcher;
+
+    public ClusterSource(GalleryApp application) {
+        super("cluster");
+        mApplication = application;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/cluster/*/time", CLUSTER_ALBUMSET_TIME);
+        mMatcher.add("/cluster/*/location", CLUSTER_ALBUMSET_LOCATION);
+        mMatcher.add("/cluster/*/tag", CLUSTER_ALBUMSET_TAG);
+        mMatcher.add("/cluster/*/size", CLUSTER_ALBUMSET_SIZE);
+        mMatcher.add("/cluster/*/face", CLUSTER_ALBUMSET_FACE);
+
+        mMatcher.add("/cluster/*/time/*", CLUSTER_ALBUM_TIME);
+        mMatcher.add("/cluster/*/location/*", CLUSTER_ALBUM_LOCATION);
+        mMatcher.add("/cluster/*/tag/*", CLUSTER_ALBUM_TAG);
+        mMatcher.add("/cluster/*/size/*", CLUSTER_ALBUM_SIZE);
+        mMatcher.add("/cluster/*/face/*", CLUSTER_ALBUM_FACE);
+    }
+
+    // The names we accept are:
+    // /cluster/{set}/time      /cluster/{set}/time/k
+    // /cluster/{set}/location  /cluster/{set}/location/k
+    // /cluster/{set}/tag       /cluster/{set}/tag/encoded_tag
+    // /cluster/{set}/size      /cluster/{set}/size/min_size
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        int matchType = mMatcher.match(path);
+        String setsName = mMatcher.getVar(0);
+        DataManager dataManager = mApplication.getDataManager();
+        MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+        switch (matchType) {
+            case CLUSTER_ALBUMSET_TIME:
+            case CLUSTER_ALBUMSET_LOCATION:
+            case CLUSTER_ALBUMSET_TAG:
+            case CLUSTER_ALBUMSET_SIZE:
+            case CLUSTER_ALBUMSET_FACE:
+                return new ClusterAlbumSet(path, mApplication, sets[0], matchType);
+            case CLUSTER_ALBUM_TIME:
+            case CLUSTER_ALBUM_LOCATION:
+            case CLUSTER_ALBUM_TAG:
+            case CLUSTER_ALBUM_SIZE:
+            case CLUSTER_ALBUM_FACE: {
+                MediaSet parent = dataManager.getMediaSet(path.getParent());
+                // The actual content in the ClusterAlbum will be filled later
+                // when the reload() method in the parent is run.
+                return new ClusterAlbum(path, dataManager, parent);
+            }
+            default:
+                throw new RuntimeException("bad path: " + path);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/Clustering.java b/src/com/android/gallery3d/data/Clustering.java
new file mode 100644
index 0000000..4072bf5
--- /dev/null
+++ b/src/com/android/gallery3d/data/Clustering.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import java.util.ArrayList;
+
+public abstract class Clustering {
+    public abstract void run(MediaSet baseSet);
+    public abstract int getNumberOfClusters();
+    public abstract ArrayList<Path> getCluster(int index);
+    public abstract String getClusterName(int index);
+    public MediaItem getClusterCover(int index) {
+        return null;
+    }
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java
new file mode 100644
index 0000000..cadd9f8
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbum.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.data;
+
+import com.android.gallery3d.util.Future;
+
+import java.util.ArrayList;
+
+// ComboAlbum combines multiple media sets into one. It lists all media items
+// from the input albums.
+// This only handles SubMediaSets, not MediaItems. (That's all we need now)
+public class ComboAlbum extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ComboAlbum";
+    private final MediaSet[] mSets;
+    private String mName;
+
+    public ComboAlbum(Path path, MediaSet[] mediaSets, String name) {
+        super(path, nextVersionNumber());
+        mSets = mediaSets;
+        for (MediaSet set : mSets) {
+            set.addContentListener(this);
+        }
+        mName = name;
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        ArrayList<MediaItem> items = new ArrayList<MediaItem>();
+        for (MediaSet set : mSets) {
+            int size = set.getMediaItemCount();
+            if (count < 1) break;
+            if (start < size) {
+                int fetchCount = (start + count <= size) ? count : size - start;
+                ArrayList<MediaItem> fetchItems = set.getMediaItem(start, fetchCount);
+                items.addAll(fetchItems);
+                count -= fetchItems.size();
+                start = 0;
+            } else {
+                start -= size;
+            }
+        }
+        return items;
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        int count = 0;
+        for (MediaSet set : mSets) {
+            count += set.getMediaItemCount();
+        }
+        return count;
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    public void useNameOfChild(int i) {
+        if (i < mSets.length) mName = mSets[i].getName();
+    }
+
+    @Override
+    public long reload() {
+        boolean changed = false;
+        for (int i = 0, n = mSets.length; i < n; ++i) {
+            long version = mSets[i].reload();
+            if (version > mDataVersion) changed = true;
+        }
+        if (changed) mDataVersion = nextVersionNumber();
+        return mDataVersion;
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    @Override
+    public Future<Integer> requestSync(SyncListener listener) {
+        return requestSyncOnMultipleSets(mSets, listener);
+    }
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java
new file mode 100644
index 0000000..3f36745
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbumSet.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.util.Future;
+
+// ComboAlbumSet combines multiple media sets into one. It lists all sub
+// media sets from the input album sets.
+// This only handles SubMediaSets, not MediaItems. (That's all we need now)
+public class ComboAlbumSet extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ComboAlbumSet";
+    private final MediaSet[] mSets;
+    private final String mName;
+
+    public ComboAlbumSet(Path path, GalleryApp application, MediaSet[] mediaSets) {
+        super(path, nextVersionNumber());
+        mSets = mediaSets;
+        for (MediaSet set : mSets) {
+            set.addContentListener(this);
+        }
+        mName = application.getResources().getString(
+                R.string.set_label_all_albums);
+    }
+
+    @Override
+    public MediaSet getSubMediaSet(int index) {
+        for (MediaSet set : mSets) {
+            int size = set.getSubMediaSetCount();
+            if (index < size) {
+                return set.getSubMediaSet(index);
+            }
+            index -= size;
+        }
+        return null;
+    }
+
+    @Override
+    public int getSubMediaSetCount() {
+        int count = 0;
+        for (MediaSet set : mSets) {
+            count += set.getSubMediaSetCount();
+        }
+        return count;
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public boolean isLoading() {
+        for (int i = 0, n = mSets.length; i < n; ++i) {
+            if (mSets[i].isLoading()) return true;
+        }
+        return false;
+    }
+
+    @Override
+    public long reload() {
+        boolean changed = false;
+        for (int i = 0, n = mSets.length; i < n; ++i) {
+            long version = mSets[i].reload();
+            if (version > mDataVersion) changed = true;
+        }
+        if (changed) mDataVersion = nextVersionNumber();
+        return mDataVersion;
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    @Override
+    public Future<Integer> requestSync(SyncListener listener) {
+        return requestSyncOnMultipleSets(mSets, listener);
+    }
+}
diff --git a/src/com/android/gallery3d/data/ComboSource.java b/src/com/android/gallery3d/data/ComboSource.java
new file mode 100644
index 0000000..867d47e
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboSource.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+class ComboSource extends MediaSource {
+    private static final int COMBO_ALBUMSET = 0;
+    private static final int COMBO_ALBUM = 1;
+    private GalleryApp mApplication;
+    private PathMatcher mMatcher;
+
+    public ComboSource(GalleryApp application) {
+        super("combo");
+        mApplication = application;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/combo/*", COMBO_ALBUMSET);
+        mMatcher.add("/combo/*/*", COMBO_ALBUM);
+    }
+
+    // The only path we accept is "/combo/{set1, set2, ...} and /combo/item/{set1, set2, ...}"
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        String[] segments = path.split();
+        if (segments.length < 2) {
+            throw new RuntimeException("bad path: " + path);
+        }
+
+        DataManager dataManager = mApplication.getDataManager();
+        switch (mMatcher.match(path)) {
+            case COMBO_ALBUMSET:
+                return new ComboAlbumSet(path, mApplication,
+                        dataManager.getMediaSetsFromString(segments[1]));
+
+            case COMBO_ALBUM:
+                return new ComboAlbum(path,
+                        dataManager.getMediaSetsFromString(segments[2]), segments[1]);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/gallery3d/data/ContentListener.java b/src/com/android/gallery3d/data/ContentListener.java
new file mode 100644
index 0000000..5e29526
--- /dev/null
+++ b/src/com/android/gallery3d/data/ContentListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+public interface ContentListener {
+    public void onContentDirty();
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
new file mode 100644
index 0000000..38865e9
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSource.PathId;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+import java.util.WeakHashMap;
+
+// DataManager manages all media sets and media items in the system.
+//
+// Each MediaSet and MediaItem has a unique 64 bits id. The most significant
+// 32 bits represents its parent, and the least significant 32 bits represents
+// the self id. For MediaSet the self id is is globally unique, but for
+// MediaItem it's unique only relative to its parent.
+//
+// To make sure the id is the same when the MediaSet is re-created, a child key
+// is provided to obtainSetId() to make sure the same self id will be used as
+// when the parent and key are the same. A sequence of child keys is called a
+// path. And it's used to identify a specific media set even if the process is
+// killed and re-created, so child keys should be stable identifiers.
+
+public class DataManager implements StitchingChangeListener {
+    public static final int INCLUDE_IMAGE = 1;
+    public static final int INCLUDE_VIDEO = 2;
+    public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO;
+    public static final int INCLUDE_LOCAL_ONLY = 4;
+    public static final int INCLUDE_LOCAL_IMAGE_ONLY =
+            INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE;
+    public static final int INCLUDE_LOCAL_VIDEO_ONLY =
+            INCLUDE_LOCAL_ONLY | INCLUDE_VIDEO;
+    public static final int INCLUDE_LOCAL_ALL_ONLY =
+            INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE | INCLUDE_VIDEO;
+
+    // Any one who would like to access data should require this lock
+    // to prevent concurrency issue.
+    public static final Object LOCK = new Object();
+
+    public static DataManager from(Context context) {
+        GalleryApp app = (GalleryApp) context.getApplicationContext();
+        return app.getDataManager();
+    }
+
+    private static final String TAG = "DataManager";
+
+    // This is the path for the media set seen by the user at top level.
+    private static final String TOP_SET_PATH = "/combo/{/local/all,/picasa/all}";
+
+    private static final String TOP_IMAGE_SET_PATH = "/combo/{/local/image,/picasa/image}";
+
+    private static final String TOP_VIDEO_SET_PATH =
+            "/combo/{/local/video,/picasa/video}";
+
+    private static final String TOP_LOCAL_SET_PATH = "/local/all";
+
+    private static final String TOP_LOCAL_IMAGE_SET_PATH = "/local/image";
+
+    private static final String TOP_LOCAL_VIDEO_SET_PATH = "/local/video";
+
+    public static final Comparator<MediaItem> sDateTakenComparator =
+            new DateTakenComparator();
+
+    private static class DateTakenComparator implements Comparator<MediaItem> {
+        @Override
+        public int compare(MediaItem item1, MediaItem item2) {
+            return -Utils.compare(item1.getDateInMs(), item2.getDateInMs());
+        }
+    }
+
+    private final Handler mDefaultMainHandler;
+
+    private GalleryApp mApplication;
+    private int mActiveCount = 0;
+
+    private HashMap<Uri, NotifyBroker> mNotifierMap =
+            new HashMap<Uri, NotifyBroker>();
+
+
+    private HashMap<String, MediaSource> mSourceMap =
+            new LinkedHashMap<String, MediaSource>();
+
+    public DataManager(GalleryApp application) {
+        mApplication = application;
+        mDefaultMainHandler = new Handler(application.getMainLooper());
+    }
+
+    public synchronized void initializeSourceMap() {
+        if (!mSourceMap.isEmpty()) return;
+
+        // the order matters, the UriSource must come last
+        addSource(new LocalSource(mApplication));
+        addSource(new PicasaSource(mApplication));
+        addSource(new ComboSource(mApplication));
+        addSource(new ClusterSource(mApplication));
+        addSource(new FilterSource(mApplication));
+        addSource(new SecureSource(mApplication));
+        addSource(new UriSource(mApplication));
+        addSource(new SnailSource(mApplication));
+
+        if (mActiveCount > 0) {
+            for (MediaSource source : mSourceMap.values()) {
+                source.resume();
+            }
+        }
+    }
+
+    public String getTopSetPath(int typeBits) {
+
+        switch (typeBits) {
+            case INCLUDE_IMAGE: return TOP_IMAGE_SET_PATH;
+            case INCLUDE_VIDEO: return TOP_VIDEO_SET_PATH;
+            case INCLUDE_ALL: return TOP_SET_PATH;
+            case INCLUDE_LOCAL_IMAGE_ONLY: return TOP_LOCAL_IMAGE_SET_PATH;
+            case INCLUDE_LOCAL_VIDEO_ONLY: return TOP_LOCAL_VIDEO_SET_PATH;
+            case INCLUDE_LOCAL_ALL_ONLY: return TOP_LOCAL_SET_PATH;
+            default: throw new IllegalArgumentException();
+        }
+    }
+
+    // open for debug
+    void addSource(MediaSource source) {
+        if (source == null) return;
+        mSourceMap.put(source.getPrefix(), source);
+    }
+
+    // A common usage of this method is:
+    // synchronized (DataManager.LOCK) {
+    //     MediaObject object = peekMediaObject(path);
+    //     if (object == null) {
+    //         object = createMediaObject(...);
+    //     }
+    // }
+    public MediaObject peekMediaObject(Path path) {
+        return path.getObject();
+    }
+
+    public MediaObject getMediaObject(Path path) {
+        synchronized (LOCK) {
+            MediaObject obj = path.getObject();
+            if (obj != null) return obj;
+
+            MediaSource source = mSourceMap.get(path.getPrefix());
+            if (source == null) {
+                Log.w(TAG, "cannot find media source for path: " + path);
+                return null;
+            }
+
+            try {
+                MediaObject object = source.createMediaObject(path);
+                if (object == null) {
+                    Log.w(TAG, "cannot create media object: " + path);
+                }
+                return object;
+            } catch (Throwable t) {
+                Log.w(TAG, "exception in creating media object: " + path, t);
+                return null;
+            }
+        }
+    }
+
+    public MediaObject getMediaObject(String s) {
+        return getMediaObject(Path.fromString(s));
+    }
+
+    public MediaSet getMediaSet(Path path) {
+        return (MediaSet) getMediaObject(path);
+    }
+
+    public MediaSet getMediaSet(String s) {
+        return (MediaSet) getMediaObject(s);
+    }
+
+    public MediaSet[] getMediaSetsFromString(String segment) {
+        String[] seq = Path.splitSequence(segment);
+        int n = seq.length;
+        MediaSet[] sets = new MediaSet[n];
+        for (int i = 0; i < n; i++) {
+            sets[i] = getMediaSet(seq[i]);
+        }
+        return sets;
+    }
+
+    // Maps a list of Paths to MediaItems, and invoke consumer.consume()
+    // for each MediaItem (may not be in the same order as the input list).
+    // An index number is also passed to consumer.consume() to identify
+    // the original position in the input list of the corresponding Path (plus
+    // startIndex).
+    public void mapMediaItems(ArrayList<Path> list, ItemConsumer consumer,
+            int startIndex) {
+        HashMap<String, ArrayList<PathId>> map =
+                new HashMap<String, ArrayList<PathId>>();
+
+        // Group the path by the prefix.
+        int n = list.size();
+        for (int i = 0; i < n; i++) {
+            Path path = list.get(i);
+            String prefix = path.getPrefix();
+            ArrayList<PathId> group = map.get(prefix);
+            if (group == null) {
+                group = new ArrayList<PathId>();
+                map.put(prefix, group);
+            }
+            group.add(new PathId(path, i + startIndex));
+        }
+
+        // For each group, ask the corresponding media source to map it.
+        for (Entry<String, ArrayList<PathId>> entry : map.entrySet()) {
+            String prefix = entry.getKey();
+            MediaSource source = mSourceMap.get(prefix);
+            source.mapMediaItems(entry.getValue(), consumer);
+        }
+    }
+
+    // The following methods forward the request to the proper object.
+    public int getSupportedOperations(Path path) {
+        return getMediaObject(path).getSupportedOperations();
+    }
+
+    public void getPanoramaSupport(Path path, PanoramaSupportCallback callback) {
+        getMediaObject(path).getPanoramaSupport(callback);
+    }
+
+    public void delete(Path path) {
+        getMediaObject(path).delete();
+    }
+
+    public void rotate(Path path, int degrees) {
+        getMediaObject(path).rotate(degrees);
+    }
+
+    public Uri getContentUri(Path path) {
+        return getMediaObject(path).getContentUri();
+    }
+
+    public int getMediaType(Path path) {
+        return getMediaObject(path).getMediaType();
+    }
+
+    public Path findPathByUri(Uri uri, String type) {
+        if (uri == null) return null;
+        for (MediaSource source : mSourceMap.values()) {
+            Path path = source.findPathByUri(uri, type);
+            if (path != null) return path;
+        }
+        return null;
+    }
+
+    public Path getDefaultSetOf(Path item) {
+        MediaSource source = mSourceMap.get(item.getPrefix());
+        return source == null ? null : source.getDefaultSetOf(item);
+    }
+
+    // Returns number of bytes used by cached pictures currently downloaded.
+    public long getTotalUsedCacheSize() {
+        long sum = 0;
+        for (MediaSource source : mSourceMap.values()) {
+            sum += source.getTotalUsedCacheSize();
+        }
+        return sum;
+    }
+
+    // Returns number of bytes used by cached pictures if all pending
+    // downloads and removals are completed.
+    public long getTotalTargetCacheSize() {
+        long sum = 0;
+        for (MediaSource source : mSourceMap.values()) {
+            sum += source.getTotalTargetCacheSize();
+        }
+        return sum;
+    }
+
+    public void registerChangeNotifier(Uri uri, ChangeNotifier notifier) {
+        NotifyBroker broker = null;
+        synchronized (mNotifierMap) {
+            broker = mNotifierMap.get(uri);
+            if (broker == null) {
+                broker = new NotifyBroker(mDefaultMainHandler);
+                mApplication.getContentResolver()
+                        .registerContentObserver(uri, true, broker);
+                mNotifierMap.put(uri, broker);
+            }
+        }
+        broker.registerNotifier(notifier);
+    }
+
+    public void resume() {
+        if (++mActiveCount == 1) {
+            for (MediaSource source : mSourceMap.values()) {
+                source.resume();
+            }
+        }
+    }
+
+    public void pause() {
+        if (--mActiveCount == 0) {
+            for (MediaSource source : mSourceMap.values()) {
+                source.pause();
+            }
+        }
+    }
+
+    private static class NotifyBroker extends ContentObserver {
+        private WeakHashMap<ChangeNotifier, Object> mNotifiers =
+                new WeakHashMap<ChangeNotifier, Object>();
+
+        public NotifyBroker(Handler handler) {
+            super(handler);
+        }
+
+        public synchronized void registerNotifier(ChangeNotifier notifier) {
+            mNotifiers.put(notifier, null);
+        }
+
+        @Override
+        public synchronized void onChange(boolean selfChange) {
+            for(ChangeNotifier notifier : mNotifiers.keySet()) {
+                notifier.onChange(selfChange);
+            }
+        }
+    }
+
+    @Override
+    public void onStitchingQueued(Uri uri) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onStitchingResult(Uri uri) {
+        Path path = findPathByUri(uri, null);
+        if (path != null) {
+            MediaObject mediaObject = getMediaObject(path);
+            if (mediaObject != null) {
+                mediaObject.clearCachedPanoramaSupport();
+            }
+        }
+    }
+
+    @Override
+    public void onStitchingProgress(Uri uri, int progress) {
+        // Do nothing.
+    }
+}
diff --git a/src/com/android/gallery3d/data/DataSourceType.java b/src/com/android/gallery3d/data/DataSourceType.java
new file mode 100644
index 0000000..ab534d0
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataSourceType.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.util.MediaSetUtils;
+
+public final class DataSourceType {
+    public static final int TYPE_NOT_CATEGORIZED = 0;
+    public static final int TYPE_LOCAL = 1;
+    public static final int TYPE_PICASA = 2;
+    public static final int TYPE_CAMERA = 3;
+
+    private static final Path PICASA_ROOT = Path.fromString("/picasa");
+    private static final Path LOCAL_ROOT = Path.fromString("/local");
+
+    public static int identifySourceType(MediaSet set) {
+        if (set == null) {
+            return TYPE_NOT_CATEGORIZED;
+        }
+
+        Path path = set.getPath();
+        if (MediaSetUtils.isCameraSource(path)) return TYPE_CAMERA;
+
+        Path prefix = path.getPrefixPath();
+
+        if (prefix == PICASA_ROOT) return TYPE_PICASA;
+        if (prefix == LOCAL_ROOT) return TYPE_LOCAL;
+
+        return TYPE_NOT_CATEGORIZED;
+    }
+}
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
new file mode 100644
index 0000000..fa70915
--- /dev/null
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Build;
+import android.util.FloatMath;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.ui.Log;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+public class DecodeUtils {
+    private static final String TAG = "DecodeUtils";
+
+    private static class DecodeCanceller implements CancelListener {
+        Options mOptions;
+
+        public DecodeCanceller(Options options) {
+            mOptions = options;
+        }
+
+        @Override
+        public void onCancel() {
+            mOptions.requestCancelDecode();
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    public static void setOptionsMutable(Options options) {
+        if (ApiHelper.HAS_OPTIONS_IN_MUTABLE) options.inMutable = true;
+    }
+
+    public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) {
+        if (options == null) options = new Options();
+        jc.setCancelListener(new DecodeCanceller(options));
+        setOptionsMutable(options);
+        return ensureGLCompatibleBitmap(
+                BitmapFactory.decodeFileDescriptor(fd, null, options));
+    }
+
+    public static void decodeBounds(JobContext jc, FileDescriptor fd,
+            Options options) {
+        Utils.assertTrue(options != null);
+        options.inJustDecodeBounds = true;
+        jc.setCancelListener(new DecodeCanceller(options));
+        BitmapFactory.decodeFileDescriptor(fd, null, options);
+        options.inJustDecodeBounds = false;
+    }
+
+    public static Bitmap decode(JobContext jc, byte[] bytes, Options options) {
+        return decode(jc, bytes, 0, bytes.length, options);
+    }
+
+    public static Bitmap decode(JobContext jc, byte[] bytes, int offset,
+            int length, Options options) {
+        if (options == null) options = new Options();
+        jc.setCancelListener(new DecodeCanceller(options));
+        setOptionsMutable(options);
+        return ensureGLCompatibleBitmap(
+                BitmapFactory.decodeByteArray(bytes, offset, length, options));
+    }
+
+    public static void decodeBounds(JobContext jc, byte[] bytes, int offset,
+            int length, Options options) {
+        Utils.assertTrue(options != null);
+        options.inJustDecodeBounds = true;
+        jc.setCancelListener(new DecodeCanceller(options));
+        BitmapFactory.decodeByteArray(bytes, offset, length, options);
+        options.inJustDecodeBounds = false;
+    }
+
+    public static Bitmap decodeThumbnail(
+            JobContext jc, String filePath, Options options, int targetSize, int type) {
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(filePath);
+            FileDescriptor fd = fis.getFD();
+            return decodeThumbnail(jc, fd, options, targetSize, type);
+        } catch (Exception ex) {
+            Log.w(TAG, ex);
+            return null;
+        } finally {
+            Utils.closeSilently(fis);
+        }
+    }
+
+    public static Bitmap decodeThumbnail(
+            JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) {
+        if (options == null) options = new Options();
+        jc.setCancelListener(new DecodeCanceller(options));
+
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFileDescriptor(fd, null, options);
+        if (jc.isCancelled()) return null;
+
+        int w = options.outWidth;
+        int h = options.outHeight;
+
+        if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+            // We center-crop the original image as it's micro thumbnail. In this case,
+            // we want to make sure the shorter side >= "targetSize".
+            float scale = (float) targetSize / Math.min(w, h);
+            options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+
+            // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding
+            // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here.
+            final int MAX_PIXEL_COUNT = 640000; // 400 x 1600
+            if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) {
+                options.inSampleSize = BitmapUtils.computeSampleSize(
+                        FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h)));
+            }
+        } else {
+            // For screen nail, we only want to keep the longer side >= targetSize.
+            float scale = (float) targetSize / Math.max(w, h);
+            options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+        }
+
+        options.inJustDecodeBounds = false;
+        setOptionsMutable(options);
+
+        Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options);
+        if (result == null) return null;
+
+        // We need to resize down if the decoder does not support inSampleSize
+        // (For example, GIF images)
+        float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL
+                ? Math.min(result.getWidth(), result.getHeight())
+                : Math.max(result.getWidth(), result.getHeight()));
+
+        if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true);
+        return ensureGLCompatibleBitmap(result);
+    }
+
+    /**
+     * Decodes the bitmap from the given byte array if the image size is larger than the given
+     * requirement.
+     *
+     * Note: The returned image may be resized down. However, both width and height must be
+     * larger than the <code>targetSize</code>.
+     */
+    public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data,
+            Options options, int targetSize) {
+        if (options == null) options = new Options();
+        jc.setCancelListener(new DecodeCanceller(options));
+
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeByteArray(data, 0, data.length, options);
+        if (jc.isCancelled()) return null;
+        if (options.outWidth < targetSize || options.outHeight < targetSize) {
+            return null;
+        }
+        options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
+                options.outWidth, options.outHeight, targetSize);
+        options.inJustDecodeBounds = false;
+        setOptionsMutable(options);
+
+        return ensureGLCompatibleBitmap(
+                BitmapFactory.decodeByteArray(data, 0, data.length, options));
+    }
+
+    // TODO: This function should not be called directly from
+    // DecodeUtils.requestDecode(...), since we don't have the knowledge
+    // if the bitmap will be uploaded to GL.
+    public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
+        if (bitmap == null || bitmap.getConfig() != null) return bitmap;
+        Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
+        bitmap.recycle();
+        return newBitmap;
+    }
+
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
+            JobContext jc, byte[] bytes, int offset, int length,
+            boolean shareable) {
+        if (offset < 0 || length <= 0 || offset + length > bytes.length) {
+            throw new IllegalArgumentException(String.format(
+                    "offset = %s, length = %s, bytes = %s",
+                    offset, length, bytes.length));
+        }
+
+        try {
+            return BitmapRegionDecoder.newInstance(
+                    bytes, offset, length, shareable);
+        } catch (Throwable t)  {
+            Log.w(TAG, t);
+            return null;
+        }
+    }
+
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
+            JobContext jc, String filePath, boolean shareable) {
+        try {
+            return BitmapRegionDecoder.newInstance(filePath, shareable);
+        } catch (Throwable t)  {
+            Log.w(TAG, t);
+            return null;
+        }
+    }
+
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
+            JobContext jc, FileDescriptor fd, boolean shareable) {
+        try {
+            return BitmapRegionDecoder.newInstance(fd, shareable);
+        } catch (Throwable t)  {
+            Log.w(TAG, t);
+            return null;
+        }
+    }
+
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
+            JobContext jc, InputStream is, boolean shareable) {
+        try {
+            return BitmapRegionDecoder.newInstance(is, shareable);
+        } catch (Throwable t)  {
+            // We often cancel the creating of bitmap region decoder,
+            // so just log one line.
+            Log.w(TAG, "requestCreateBitmapRegionDecoder: " + t);
+            return null;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static Bitmap decodeUsingPool(JobContext jc, byte[] data, int offset,
+            int length, BitmapFactory.Options options) {
+        if (options == null) options = new BitmapFactory.Options();
+        if (options.inSampleSize < 1) options.inSampleSize = 1;
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        options.inBitmap = (options.inSampleSize == 1)
+                ? findCachedBitmap(jc, data, offset, length, options) : null;
+        try {
+            Bitmap bitmap = decode(jc, data, offset, length, options);
+            if (options.inBitmap != null && options.inBitmap != bitmap) {
+                GalleryBitmapPool.getInstance().put(options.inBitmap);
+                options.inBitmap = null;
+            }
+            return bitmap;
+        } catch (IllegalArgumentException e) {
+            if (options.inBitmap == null) throw e;
+
+            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+            GalleryBitmapPool.getInstance().put(options.inBitmap);
+            options.inBitmap = null;
+            return decode(jc, data, offset, length, options);
+        }
+    }
+
+    // This is the same as the method above except the source data comes
+    // from a file descriptor instead of a byte array.
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static Bitmap decodeUsingPool(JobContext jc,
+            FileDescriptor fileDescriptor, Options options) {
+        if (options == null) options = new BitmapFactory.Options();
+        if (options.inSampleSize < 1) options.inSampleSize = 1;
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        options.inBitmap = (options.inSampleSize == 1)
+                ? findCachedBitmap(jc, fileDescriptor, options) : null;
+        try {
+            Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
+            if (options.inBitmap != null && options.inBitmap != bitmap) {
+                GalleryBitmapPool.getInstance().put(options.inBitmap);
+                options.inBitmap = null;
+            }
+            return bitmap;
+        } catch (IllegalArgumentException e) {
+            if (options.inBitmap == null) throw e;
+
+            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+            GalleryBitmapPool.getInstance().put(options.inBitmap);
+            options.inBitmap = null;
+            return decode(jc, fileDescriptor, options);
+        }
+    }
+
+    private static Bitmap findCachedBitmap(JobContext jc, byte[] data,
+            int offset, int length, Options options) {
+        decodeBounds(jc, data, offset, length, options);
+        return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
+    }
+
+    private static Bitmap findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor,
+            Options options) {
+        decodeBounds(jc, fileDescriptor, options);
+        return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
+    }
+}
diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java
new file mode 100644
index 0000000..be7820b
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadCache.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.LruCache;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DownloadEntry.Columns;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class DownloadCache {
+    private static final String TAG = "DownloadCache";
+    private static final int MAX_DELETE_COUNT = 16;
+    private static final int LRU_CAPACITY = 4;
+
+    private static final String TABLE_NAME = DownloadEntry.SCHEMA.getTableName();
+
+    private static final String QUERY_PROJECTION[] = {Columns.ID, Columns.DATA};
+    private static final String WHERE_HASH_AND_URL = String.format(
+            "%s = ? AND %s = ?", Columns.HASH_CODE, Columns.CONTENT_URL);
+    private static final int QUERY_INDEX_ID = 0;
+    private static final int QUERY_INDEX_DATA = 1;
+
+    private static final String FREESPACE_PROJECTION[] = {
+            Columns.ID, Columns.DATA, Columns.CONTENT_URL, Columns.CONTENT_SIZE};
+    private static final String FREESPACE_ORDER_BY =
+            String.format("%s ASC", Columns.LAST_ACCESS);
+    private static final int FREESPACE_IDNEX_ID = 0;
+    private static final int FREESPACE_IDNEX_DATA = 1;
+    private static final int FREESPACE_INDEX_CONTENT_URL = 2;
+    private static final int FREESPACE_INDEX_CONTENT_SIZE = 3;
+
+    private static final String ID_WHERE = Columns.ID + " = ?";
+
+    private static final String SUM_PROJECTION[] =
+            {String.format("sum(%s)", Columns.CONTENT_SIZE)};
+    private static final int SUM_INDEX_SUM = 0;
+
+    private final LruCache<String, Entry> mEntryMap =
+            new LruCache<String, Entry>(LRU_CAPACITY);
+    private final HashMap<String, DownloadTask> mTaskMap =
+            new HashMap<String, DownloadTask>();
+    private final File mRoot;
+    private final GalleryApp mApplication;
+    private final SQLiteDatabase mDatabase;
+    private final long mCapacity;
+
+    private long mTotalBytes = 0;
+    private boolean mInitialized = false;
+
+    public DownloadCache(GalleryApp application, File root, long capacity) {
+        mRoot = Utils.checkNotNull(root);
+        mApplication = Utils.checkNotNull(application);
+        mCapacity = capacity;
+        mDatabase = new DatabaseHelper(application.getAndroidContext())
+                .getWritableDatabase();
+    }
+
+    private Entry findEntryInDatabase(String stringUrl) {
+        long hash = Utils.crc64Long(stringUrl);
+        String whereArgs[] = {String.valueOf(hash), stringUrl};
+        Cursor cursor = mDatabase.query(TABLE_NAME, QUERY_PROJECTION,
+                WHERE_HASH_AND_URL, whereArgs, null, null, null);
+        try {
+            if (cursor.moveToNext()) {
+                File file = new File(cursor.getString(QUERY_INDEX_DATA));
+                long id = cursor.getInt(QUERY_INDEX_ID);
+                Entry entry = null;
+                synchronized (mEntryMap) {
+                    entry = mEntryMap.get(stringUrl);
+                    if (entry == null) {
+                        entry = new Entry(id, file);
+                        mEntryMap.put(stringUrl, entry);
+                    }
+                }
+                return entry;
+            }
+        } finally {
+            cursor.close();
+        }
+        return null;
+    }
+
+    public Entry download(JobContext jc, URL url) {
+        if (!mInitialized) initialize();
+
+        String stringUrl = url.toString();
+
+        // First find in the entry-pool
+        synchronized (mEntryMap) {
+            Entry entry = mEntryMap.get(stringUrl);
+            if (entry != null) {
+                updateLastAccess(entry.mId);
+                return entry;
+            }
+        }
+
+        // Then, find it in database
+        TaskProxy proxy = new TaskProxy();
+        synchronized (mTaskMap) {
+            Entry entry = findEntryInDatabase(stringUrl);
+            if (entry != null) {
+                updateLastAccess(entry.mId);
+                return entry;
+            }
+
+            // Finally, we need to download the file ....
+            // First check if we are downloading it now ...
+            DownloadTask task = mTaskMap.get(stringUrl);
+            if (task == null) { // if not, start the download task now
+                task = new DownloadTask(stringUrl);
+                mTaskMap.put(stringUrl, task);
+                task.mFuture = mApplication.getThreadPool().submit(task, task);
+            }
+            task.addProxy(proxy);
+        }
+
+        return proxy.get(jc);
+    }
+
+    private void updateLastAccess(long id) {
+        ContentValues values = new ContentValues();
+        values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+        mDatabase.update(TABLE_NAME, values,
+                ID_WHERE, new String[] {String.valueOf(id)});
+    }
+
+    private synchronized void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
+        if (mTotalBytes <= mCapacity) return;
+        Cursor cursor = mDatabase.query(TABLE_NAME,
+                FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY);
+        try {
+            while (maxDeleteFileCount > 0
+                    && mTotalBytes > mCapacity && cursor.moveToNext()) {
+                long id = cursor.getLong(FREESPACE_IDNEX_ID);
+                String url = cursor.getString(FREESPACE_INDEX_CONTENT_URL);
+                long size = cursor.getLong(FREESPACE_INDEX_CONTENT_SIZE);
+                String path = cursor.getString(FREESPACE_IDNEX_DATA);
+                boolean containsKey;
+                synchronized (mEntryMap) {
+                    containsKey = mEntryMap.containsKey(url);
+                }
+                if (!containsKey) {
+                    --maxDeleteFileCount;
+                    mTotalBytes -= size;
+                    new File(path).delete();
+                    mDatabase.delete(TABLE_NAME,
+                            ID_WHERE, new String[]{String.valueOf(id)});
+                } else {
+                    // skip delete, since it is being used
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private synchronized long insertEntry(String url, File file) {
+        long size = file.length();
+        mTotalBytes += size;
+
+        ContentValues values = new ContentValues();
+        String hashCode = String.valueOf(Utils.crc64Long(url));
+        values.put(Columns.DATA, file.getAbsolutePath());
+        values.put(Columns.HASH_CODE, hashCode);
+        values.put(Columns.CONTENT_URL, url);
+        values.put(Columns.CONTENT_SIZE, size);
+        values.put(Columns.LAST_UPDATED, System.currentTimeMillis());
+        return mDatabase.insert(TABLE_NAME, "", values);
+    }
+
+    private synchronized void initialize() {
+        if (mInitialized) return;
+        mInitialized = true;
+        if (!mRoot.isDirectory()) mRoot.mkdirs();
+        if (!mRoot.isDirectory()) {
+            throw new RuntimeException("cannot create " + mRoot.getAbsolutePath());
+        }
+
+        Cursor cursor = mDatabase.query(
+                TABLE_NAME, SUM_PROJECTION, null, null, null, null, null);
+        mTotalBytes = 0;
+        try {
+            if (cursor.moveToNext()) {
+                mTotalBytes = cursor.getLong(SUM_INDEX_SUM);
+            }
+        } finally {
+            cursor.close();
+        }
+        if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+    }
+
+    private final class DatabaseHelper extends SQLiteOpenHelper {
+        public static final String DATABASE_NAME = "download.db";
+        public static final int DATABASE_VERSION = 2;
+
+        public DatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            DownloadEntry.SCHEMA.createTables(db);
+            // Delete old files
+            for (File file : mRoot.listFiles()) {
+                if (!file.delete()) {
+                    Log.w(TAG, "fail to remove: " + file.getAbsolutePath());
+                }
+            }
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            //reset everything
+            DownloadEntry.SCHEMA.dropTables(db);
+            onCreate(db);
+        }
+    }
+
+    public class Entry {
+        public File cacheFile;
+        protected long mId;
+
+        Entry(long id, File cacheFile) {
+            mId = id;
+            this.cacheFile = Utils.checkNotNull(cacheFile);
+        }
+    }
+
+    private class DownloadTask implements Job<File>, FutureListener<File> {
+        private HashSet<TaskProxy> mProxySet = new HashSet<TaskProxy>();
+        private Future<File> mFuture;
+        private final String mUrl;
+
+        public DownloadTask(String url) {
+            mUrl = Utils.checkNotNull(url);
+        }
+
+        public void removeProxy(TaskProxy proxy) {
+            synchronized (mTaskMap) {
+                Utils.assertTrue(mProxySet.remove(proxy));
+                if (mProxySet.isEmpty()) {
+                    mFuture.cancel();
+                    mTaskMap.remove(mUrl);
+                }
+            }
+        }
+
+        // should be used in synchronized block of mDatabase
+        public void addProxy(TaskProxy proxy) {
+            proxy.mTask = this;
+            mProxySet.add(proxy);
+        }
+
+        @Override
+        public void onFutureDone(Future<File> future) {
+            File file = future.get();
+            long id = 0;
+            if (file != null) { // insert to database
+                id = insertEntry(mUrl, file);
+            }
+
+            if (future.isCancelled()) {
+                Utils.assertTrue(mProxySet.isEmpty());
+                return;
+            }
+
+            synchronized (mTaskMap) {
+                Entry entry = null;
+                synchronized (mEntryMap) {
+                    if (file != null) {
+                        entry = new Entry(id, file);
+                        Utils.assertTrue(mEntryMap.put(mUrl, entry) == null);
+                    }
+                }
+                for (TaskProxy proxy : mProxySet) {
+                    proxy.setResult(entry);
+                }
+                mTaskMap.remove(mUrl);
+                freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+            }
+        }
+
+        @Override
+        public File run(JobContext jc) {
+            // TODO: utilize etag
+            jc.setMode(ThreadPool.MODE_NETWORK);
+            File tempFile = null;
+            try {
+                URL url = new URL(mUrl);
+                tempFile = File.createTempFile("cache", ".tmp", mRoot);
+                // download from url to tempFile
+                jc.setMode(ThreadPool.MODE_NETWORK);
+                boolean downloaded = DownloadUtils.requestDownload(jc, url, tempFile);
+                jc.setMode(ThreadPool.MODE_NONE);
+                if (downloaded) return tempFile;
+            } catch (Exception e) {
+                Log.e(TAG, String.format("fail to download %s", mUrl), e);
+            } finally {
+                jc.setMode(ThreadPool.MODE_NONE);
+            }
+            if (tempFile != null) tempFile.delete();
+            return null;
+        }
+    }
+
+    public static class TaskProxy {
+        private DownloadTask mTask;
+        private boolean mIsCancelled = false;
+        private Entry mEntry;
+
+        synchronized void setResult(Entry entry) {
+            if (mIsCancelled) return;
+            mEntry = entry;
+            notifyAll();
+        }
+
+        public synchronized Entry get(JobContext jc) {
+            jc.setCancelListener(new CancelListener() {
+                @Override
+                public void onCancel() {
+                    mTask.removeProxy(TaskProxy.this);
+                    synchronized (TaskProxy.this) {
+                        mIsCancelled = true;
+                        TaskProxy.this.notifyAll();
+                    }
+                }
+            });
+            while (!mIsCancelled && mEntry == null) {
+                try {
+                    wait();
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "ignore interrupt", e);
+                }
+            }
+            jc.setCancelListener(null);
+            return mEntry;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/DownloadEntry.java b/src/com/android/gallery3d/data/DownloadEntry.java
new file mode 100644
index 0000000..578523f
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadEntry.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.common.Entry;
+import com.android.gallery3d.common.EntrySchema;
+
+
+@Entry.Table("download")
+public class DownloadEntry extends Entry {
+    public static final EntrySchema SCHEMA = new EntrySchema(DownloadEntry.class);
+
+    public static interface Columns extends Entry.Columns {
+        public static final String HASH_CODE = "hash_code";
+        public static final String CONTENT_URL = "content_url";
+        public static final String CONTENT_SIZE = "_size";
+        public static final String ETAG = "etag";
+        public static final String LAST_ACCESS = "last_access";
+        public static final String LAST_UPDATED = "last_updated";
+        public static final String DATA = "_data";
+    }
+
+    @Column(value = "hash_code", indexed = true)
+    public long hashCode;
+
+    @Column("content_url")
+    public String contentUrl;
+
+    @Column("_size")
+    public long contentSize;
+
+    @Column("etag")
+    public String eTag;
+
+    @Column(value = "last_access", indexed = true)
+    public long lastAccessTime;
+
+    @Column(value = "last_updated")
+    public long lastUpdatedTime;
+
+    @Column("_data")
+    public String path;
+
+    @Override
+    public String toString() {
+        // Note: THIS IS REQUIRED. We used all the fields here. Otherwise,
+        //       ProGuard will remove these UNUSED fields. However, these
+        //       fields are needed to generate database.
+        return new StringBuilder()
+                .append("hash_code: ").append(hashCode).append(", ")
+                .append("content_url").append(contentUrl).append(", ")
+                .append("_size").append(contentSize).append(", ")
+                .append("etag").append(eTag).append(", ")
+                .append("last_access").append(lastAccessTime).append(", ")
+                .append("last_updated").append(lastUpdatedTime).append(",")
+                .append("_data").append(path)
+                .toString();
+    }
+}
diff --git a/src/com/android/gallery3d/data/DownloadUtils.java b/src/com/android/gallery3d/data/DownloadUtils.java
new file mode 100644
index 0000000..137898e
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.URL;
+
+public class DownloadUtils {
+    private static final String TAG = "DownloadService";
+
+    public static boolean requestDownload(JobContext jc, URL url, File file) {
+        FileOutputStream fos = null;
+        try {
+            fos = new FileOutputStream(file);
+            return download(jc, url, fos);
+        } catch (Throwable t) {
+            return false;
+        } finally {
+            Utils.closeSilently(fos);
+        }
+    }
+
+    public static void dump(JobContext jc, InputStream is, OutputStream os)
+            throws IOException {
+        byte buffer[] = new byte[4096];
+        int rc = is.read(buffer, 0, buffer.length);
+        final Thread thread = Thread.currentThread();
+        jc.setCancelListener(new CancelListener() {
+            @Override
+            public void onCancel() {
+                thread.interrupt();
+            }
+        });
+        while (rc > 0) {
+            if (jc.isCancelled()) throw new InterruptedIOException();
+            os.write(buffer, 0, rc);
+            rc = is.read(buffer, 0, buffer.length);
+        }
+        jc.setCancelListener(null);
+        Thread.interrupted(); // consume the interrupt signal
+    }
+
+    public static boolean download(JobContext jc, URL url, OutputStream output) {
+        InputStream input = null;
+        try {
+            input = url.openStream();
+            dump(jc, input, output);
+            return true;
+        } catch (Throwable t) {
+            Log.w(TAG, "fail to download", t);
+            return false;
+        } finally {
+            Utils.closeSilently(input);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/data/EmptyAlbumImage.java b/src/com/android/gallery3d/data/EmptyAlbumImage.java
new file mode 100644
index 0000000..6f8c37c
--- /dev/null
+++ b/src/com/android/gallery3d/data/EmptyAlbumImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+
+public class EmptyAlbumImage extends ActionImage {
+    @SuppressWarnings("unused")
+    private static final String TAG = "EmptyAlbumImage";
+
+    public EmptyAlbumImage(Path path, GalleryApp application) {
+        super(path, application, R.drawable.placeholder_empty);
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return super.getSupportedOperations() | SUPPORT_BACK;
+    }
+}
diff --git a/src/com/android/gallery3d/data/Exif.java b/src/com/android/gallery3d/data/Exif.java
new file mode 100644
index 0000000..950e7de
--- /dev/null
+++ b/src/com/android/gallery3d/data/Exif.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.data;
+
+import android.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Exif {
+    private static final String TAG = "CameraExif";
+
+    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+    public static int getOrientation(InputStream is) {
+        if (is == null) {
+            return 0;
+        }
+        ExifInterface exif = new ExifInterface();
+        try {
+            exif.readExif(is);
+            Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+            if (val == null) {
+                return 0;
+            } else {
+                return ExifInterface.getRotationForOrientationValue(val.shortValue());
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to read EXIF orientation", e);
+            return 0;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java
new file mode 100644
index 0000000..d2dc22b
--- /dev/null
+++ b/src/com/android/gallery3d/data/Face.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.data;
+
+import android.graphics.Rect;
+
+import com.android.gallery3d.common.Utils;
+
+import java.util.StringTokenizer;
+
+public class Face implements Comparable<Face> {
+    private String mName;
+    private String mPersonId;
+    private Rect mPosition;
+
+    public Face(String name, String personId, String rect) {
+        mName = name;
+        mPersonId = personId;
+        Utils.assertTrue(mName != null && mPersonId != null && rect != null);
+        StringTokenizer tokenizer = new StringTokenizer(rect);
+        mPosition = new Rect();
+        while (tokenizer.hasMoreElements()) {
+            mPosition.left = Integer.parseInt(tokenizer.nextToken());
+            mPosition.top = Integer.parseInt(tokenizer.nextToken());
+            mPosition.right = Integer.parseInt(tokenizer.nextToken());
+            mPosition.bottom = Integer.parseInt(tokenizer.nextToken());
+        }
+    }
+
+    public Rect getPosition() {
+        return mPosition;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof Face) {
+            Face face = (Face) obj;
+            return mPersonId.equals(face.mPersonId);
+        }
+        return false;
+    }
+
+    @Override
+    public int compareTo(Face another) {
+        return mName.compareTo(another.mName);
+    }
+}
diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java
new file mode 100644
index 0000000..819915e
--- /dev/null
+++ b/src/com/android/gallery3d/data/FaceClustering.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.data;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import java.util.ArrayList;
+import java.util.TreeMap;
+
+public class FaceClustering extends Clustering {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FaceClustering";
+
+    private FaceCluster[] mClusters;
+    private String mUntaggedString;
+    private Context mContext;
+
+    private class FaceCluster {
+        ArrayList<Path> mPaths = new ArrayList<Path>();
+        String mName;
+        MediaItem mCoverItem;
+        Rect mCoverRegion;
+        int mCoverFaceIndex;
+
+        public FaceCluster(String name) {
+            mName = name;
+        }
+
+        public void add(MediaItem item, int faceIndex) {
+            Path path = item.getPath();
+            mPaths.add(path);
+            Face[] faces = item.getFaces();
+            if (faces != null) {
+                Face face = faces[faceIndex];
+                if (mCoverItem == null) {
+                    mCoverItem = item;
+                    mCoverRegion = face.getPosition();
+                    mCoverFaceIndex = faceIndex;
+                } else {
+                    Rect region = face.getPosition();
+                    if (mCoverRegion.width() < region.width() &&
+                            mCoverRegion.height() < region.height()) {
+                        mCoverItem = item;
+                        mCoverRegion = face.getPosition();
+                        mCoverFaceIndex = faceIndex;
+                    }
+                }
+            }
+        }
+
+        public int size() {
+            return mPaths.size();
+        }
+
+        public MediaItem getCover() {
+            if (mCoverItem != null) {
+                if (PicasaSource.isPicasaImage(mCoverItem)) {
+                    return PicasaSource.getFaceItem(mContext, mCoverItem, mCoverFaceIndex);
+                } else {
+                    return mCoverItem;
+                }
+            }
+            return null;
+        }
+    }
+
+    public FaceClustering(Context context) {
+        mUntaggedString = context.getResources().getString(R.string.untagged);
+        mContext = context;
+    }
+
+    @Override
+    public void run(MediaSet baseSet) {
+        final TreeMap<Face, FaceCluster> map =
+                new TreeMap<Face, FaceCluster>();
+        final FaceCluster untagged = new FaceCluster(mUntaggedString);
+
+        baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                Face[] faces = item.getFaces();
+                if (faces == null || faces.length == 0) {
+                    untagged.add(item, -1);
+                    return;
+                }
+                for (int j = 0; j < faces.length; j++) {
+                    Face face = faces[j];
+                    FaceCluster cluster = map.get(face);
+                    if (cluster == null) {
+                        cluster = new FaceCluster(face.getName());
+                        map.put(face, cluster);
+                    }
+                    cluster.add(item, j);
+                }
+            }
+        });
+
+        int m = map.size();
+        mClusters = map.values().toArray(new FaceCluster[m + ((untagged.size() > 0) ? 1 : 0)]);
+        if (untagged.size() > 0) {
+            mClusters[m] = untagged;
+        }
+    }
+
+    @Override
+    public int getNumberOfClusters() {
+        return mClusters.length;
+    }
+
+    @Override
+    public ArrayList<Path> getCluster(int index) {
+        return mClusters[index].mPaths;
+    }
+
+    @Override
+    public String getClusterName(int index) {
+        return mClusters[index].mName;
+    }
+
+    @Override
+    public MediaItem getClusterCover(int index) {
+        return mClusters[index].getCover();
+    }
+}
diff --git a/src/com/android/gallery3d/data/FilterDeleteSet.java b/src/com/android/gallery3d/data/FilterDeleteSet.java
new file mode 100644
index 0000000..c76412f
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterDeleteSet.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import java.util.ArrayList;
+
+// FilterDeleteSet filters a base MediaSet to remove some deletion items (we
+// expect the number to be small). The user can use the following methods to
+// add/remove deletion items:
+//
+// void addDeletion(Path path, int index);
+// void removeDelection(Path path);
+// void clearDeletion();
+// int getNumberOfDeletions();
+//
+public class FilterDeleteSet extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilterDeleteSet";
+
+    private static final int REQUEST_ADD = 1;
+    private static final int REQUEST_REMOVE = 2;
+    private static final int REQUEST_CLEAR = 3;
+
+    private static class Request {
+        int type;  // one of the REQUEST_* constants
+        Path path;
+        int indexHint;
+        public Request(int type, Path path, int indexHint) {
+            this.type = type;
+            this.path = path;
+            this.indexHint = indexHint;
+        }
+    }
+
+    private static class Deletion {
+        Path path;
+        int index;
+        public Deletion(Path path, int index) {
+            this.path = path;
+            this.index = index;
+        }
+    }
+
+    // The underlying MediaSet
+    private final MediaSet mBaseSet;
+
+    // Pending Requests
+    private ArrayList<Request> mRequests = new ArrayList<Request>();
+
+    // Deletions currently in effect, ordered by index
+    private ArrayList<Deletion> mCurrent = new ArrayList<Deletion>();
+
+    public FilterDeleteSet(Path path, MediaSet baseSet) {
+        super(path, INVALID_DATA_VERSION);
+        mBaseSet = baseSet;
+        mBaseSet.addContentListener(this);
+    }
+
+    @Override
+    public boolean isCameraRoll() {
+        return mBaseSet.isCameraRoll();
+    }
+
+    @Override
+    public String getName() {
+        return mBaseSet.getName();
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return mBaseSet.getMediaItemCount() - mCurrent.size();
+    }
+
+    // Gets the MediaItems whose (post-deletion) index are in the range [start,
+    // start + count). Because we remove some of the MediaItems, the index need
+    // to be adjusted.
+    //
+    // For example, if there are 12 items in total. The deleted items are 3, 5,
+    // 10, and the the requested range is [3, 7]:
+    //
+    // The original index:   0 1 2 3 4 5 6 7 8 9 A B C
+    // The deleted items:          X   X         X
+    // The new index:        0 1 2   3   4 5 6 7   8 9
+    // Requested:                    *   * * * *
+    //
+    // We need to figure out the [3, 7] actually maps to the original index 4,
+    // 6, 7, 8, 9.
+    //
+    // We can break the MediaItems into segments, each segment other than the
+    // last one ends in a deleted item. The difference between the new index and
+    // the original index increases with each segment:
+    //
+    // 0 1 2 X     (new index = old index)
+    // 4 X         (new index = old index - 1)
+    // 6 7 8 9 X   (new index = old index - 2)
+    // B C         (new index = old index - 3)
+    //
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        if (count <= 0) return new ArrayList<MediaItem>();
+
+        int end = start + count - 1;
+        int n = mCurrent.size();
+        // Find the segment that "start" falls into. Count the number of items
+        // not yet deleted until it reaches "start".
+        int i = 0;
+        for (i = 0; i < n; i++) {
+            Deletion d = mCurrent.get(i);
+            if (d.index - i > start) break;
+        }
+        // Find the segment that "end" falls into.
+        int j = i;
+        for (; j < n; j++) {
+            Deletion d = mCurrent.get(j);
+            if (d.index - j > end) break;
+        }
+
+        // Now get enough to cover deleted items in [start, end]
+        ArrayList<MediaItem> base = mBaseSet.getMediaItem(start + i, count + (j - i));
+
+        // Remove the deleted items.
+        for (int m = j - 1; m >= i; m--) {
+            Deletion d = mCurrent.get(m);
+            int k = d.index - (start + i);
+            base.remove(k);
+        }
+        return base;
+    }
+
+    // We apply the pending requests in the mRequests to construct mCurrent in reload().
+    @Override
+    public long reload() {
+        boolean newData = mBaseSet.reload() > mDataVersion;
+        synchronized (mRequests) {
+            if (!newData && mRequests.isEmpty()) {
+                return mDataVersion;
+            }
+            for (int i = 0; i < mRequests.size(); i++) {
+                Request r = mRequests.get(i);
+                switch (r.type) {
+                    case REQUEST_ADD: {
+                        // Add the path into mCurrent if there is no duplicate.
+                        int n = mCurrent.size();
+                        int j;
+                        for (j = 0; j < n; j++) {
+                            if (mCurrent.get(j).path == r.path) break;
+                        }
+                        if (j == n) {
+                            mCurrent.add(new Deletion(r.path, r.indexHint));
+                        }
+                        break;
+                    }
+                    case REQUEST_REMOVE: {
+                        // Remove the path from mCurrent.
+                        int n = mCurrent.size();
+                        for (int j = 0; j < n; j++) {
+                            if (mCurrent.get(j).path == r.path) {
+                                mCurrent.remove(j);
+                                break;
+                            }
+                        }
+                        break;
+                    }
+                    case REQUEST_CLEAR: {
+                        mCurrent.clear();
+                        break;
+                    }
+                }
+            }
+            mRequests.clear();
+        }
+
+        if (!mCurrent.isEmpty()) {
+            // See if the elements in mCurrent can be found in the MediaSet. We
+            // don't want to search the whole mBaseSet, so we just search a
+            // small window that contains the index hints (plus some margin).
+            int minIndex = mCurrent.get(0).index;
+            int maxIndex = minIndex;
+            for (int i = 1; i < mCurrent.size(); i++) {
+                Deletion d = mCurrent.get(i);
+                minIndex = Math.min(d.index, minIndex);
+                maxIndex = Math.max(d.index, maxIndex);
+            }
+
+            int n = mBaseSet.getMediaItemCount();
+            int from = Math.max(minIndex - 5, 0);
+            int to = Math.min(maxIndex + 5, n);
+            ArrayList<MediaItem> items = mBaseSet.getMediaItem(from, to - from);
+            ArrayList<Deletion> result = new ArrayList<Deletion>();
+            for (int i = 0; i < items.size(); i++) {
+                MediaItem item = items.get(i);
+                if (item == null) continue;
+                Path p = item.getPath();
+                // Find the matching path in mCurrent, if found move it to result
+                for (int j = 0; j < mCurrent.size(); j++) {
+                    Deletion d = mCurrent.get(j);
+                    if (d.path == p) {
+                        d.index = from + i;
+                        result.add(d);
+                        mCurrent.remove(j);
+                        break;
+                    }
+                }
+            }
+            mCurrent = result;
+        }
+
+        mDataVersion = nextVersionNumber();
+        return mDataVersion;
+    }
+
+    private void sendRequest(int type, Path path, int indexHint) {
+        Request r = new Request(type, path, indexHint);
+        synchronized (mRequests) {
+            mRequests.add(r);
+        }
+        notifyContentChanged();
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    public void addDeletion(Path path, int indexHint) {
+        sendRequest(REQUEST_ADD, path, indexHint);
+    }
+
+    public void removeDeletion(Path path) {
+        sendRequest(REQUEST_REMOVE, path, 0 /* unused */);
+    }
+
+    public void clearDeletion() {
+        sendRequest(REQUEST_CLEAR, null /* unused */ , 0 /* unused */);
+    }
+
+    // Returns number of deletions _in effect_ (the number will only gets
+    // updated after a reload()).
+    public int getNumberOfDeletions() {
+        return mCurrent.size();
+    }
+}
diff --git a/src/com/android/gallery3d/data/FilterEmptyPromptSet.java b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java
new file mode 100644
index 0000000..b576e06
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import java.util.ArrayList;
+
+public class FilterEmptyPromptSet extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilterEmptyPromptSet";
+
+    private ArrayList<MediaItem> mEmptyItem;
+    private MediaSet mBaseSet;
+
+    public FilterEmptyPromptSet(Path path, MediaSet baseSet, MediaItem emptyItem) {
+        super(path, INVALID_DATA_VERSION);
+        mEmptyItem = new ArrayList<MediaItem>(1);
+        mEmptyItem.add(emptyItem);
+        mBaseSet = baseSet;
+        mBaseSet.addContentListener(this);
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        int itemCount = mBaseSet.getMediaItemCount();
+        if (itemCount > 0) {
+            return itemCount;
+        } else {
+            return 1;
+        }
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        int itemCount = mBaseSet.getMediaItemCount();
+        if (itemCount > 0) {
+            return mBaseSet.getMediaItem(start, count);
+        } else if (start == 0 && count == 1) {
+            return mEmptyItem;
+        } else {
+            throw new ArrayIndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+
+    @Override
+    public boolean isCameraRoll() {
+        return mBaseSet.isCameraRoll();
+    }
+
+    @Override
+    public long reload() {
+        return mBaseSet.reload();
+    }
+
+    @Override
+    public String getName() {
+        return mBaseSet.getName();
+    }
+}
diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java
new file mode 100644
index 0000000..d689fe3
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterSource.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+public class FilterSource extends MediaSource {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilterSource";
+    private static final int FILTER_BY_MEDIATYPE = 0;
+    private static final int FILTER_BY_DELETE = 1;
+    private static final int FILTER_BY_EMPTY = 2;
+    private static final int FILTER_BY_EMPTY_ITEM = 3;
+    private static final int FILTER_BY_CAMERA_SHORTCUT = 4;
+    private static final int FILTER_BY_CAMERA_SHORTCUT_ITEM = 5;
+
+    public static final String FILTER_EMPTY_ITEM = "/filter/empty_prompt";
+    public static final String FILTER_CAMERA_SHORTCUT = "/filter/camera_shortcut";
+    private static final String FILTER_CAMERA_SHORTCUT_ITEM = "/filter/camera_shortcut_item";
+
+    private GalleryApp mApplication;
+    private PathMatcher mMatcher;
+    private MediaItem mEmptyItem;
+    private MediaItem mCameraShortcutItem;
+
+    public FilterSource(GalleryApp application) {
+        super("filter");
+        mApplication = application;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE);
+        mMatcher.add("/filter/delete/*", FILTER_BY_DELETE);
+        mMatcher.add("/filter/empty/*", FILTER_BY_EMPTY);
+        mMatcher.add(FILTER_EMPTY_ITEM, FILTER_BY_EMPTY_ITEM);
+        mMatcher.add(FILTER_CAMERA_SHORTCUT, FILTER_BY_CAMERA_SHORTCUT);
+        mMatcher.add(FILTER_CAMERA_SHORTCUT_ITEM, FILTER_BY_CAMERA_SHORTCUT_ITEM);
+
+        mEmptyItem = new EmptyAlbumImage(Path.fromString(FILTER_EMPTY_ITEM),
+                mApplication);
+        mCameraShortcutItem = new CameraShortcutImage(
+                Path.fromString(FILTER_CAMERA_SHORTCUT_ITEM), mApplication);
+    }
+
+    // The name we accept are:
+    // /filter/mediatype/k/{set}    where k is the media type we want.
+    // /filter/delete/{set}
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        int matchType = mMatcher.match(path);
+        DataManager dataManager = mApplication.getDataManager();
+        switch (matchType) {
+            case FILTER_BY_MEDIATYPE: {
+                int mediaType = mMatcher.getIntVar(0);
+                String setsName = mMatcher.getVar(1);
+                MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+                return new FilterTypeSet(path, dataManager, sets[0], mediaType);
+            }
+            case FILTER_BY_DELETE: {
+                String setsName = mMatcher.getVar(0);
+                MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+                return new FilterDeleteSet(path, sets[0]);
+            }
+            case FILTER_BY_EMPTY: {
+                String setsName = mMatcher.getVar(0);
+                MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+                return new FilterEmptyPromptSet(path, sets[0], mEmptyItem);
+            }
+            case FILTER_BY_EMPTY_ITEM: {
+                return mEmptyItem;
+            }
+            case FILTER_BY_CAMERA_SHORTCUT: {
+                return new SingleItemAlbum(path, mCameraShortcutItem);
+            }
+            case FILTER_BY_CAMERA_SHORTCUT_ITEM: {
+                return mCameraShortcutItem;
+            }
+            default:
+                throw new RuntimeException("bad path: " + path);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/FilterTypeSet.java b/src/com/android/gallery3d/data/FilterTypeSet.java
new file mode 100644
index 0000000..477ef73
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterTypeSet.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import java.util.ArrayList;
+
+// FilterTypeSet filters a base MediaSet according to a matching media type.
+public class FilterTypeSet extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FilterTypeSet";
+
+    private final DataManager mDataManager;
+    private final MediaSet mBaseSet;
+    private final int mMediaType;
+    private final ArrayList<Path> mPaths = new ArrayList<Path>();
+    private final ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
+
+    public FilterTypeSet(Path path, DataManager dataManager, MediaSet baseSet,
+            int mediaType) {
+        super(path, INVALID_DATA_VERSION);
+        mDataManager = dataManager;
+        mBaseSet = baseSet;
+        mMediaType = mediaType;
+        mBaseSet.addContentListener(this);
+    }
+
+    @Override
+    public String getName() {
+        return mBaseSet.getName();
+    }
+
+    @Override
+    public MediaSet getSubMediaSet(int index) {
+        return mAlbums.get(index);
+    }
+
+    @Override
+    public int getSubMediaSetCount() {
+        return mAlbums.size();
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return mPaths.size();
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        return ClusterAlbum.getMediaItemFromPath(
+                mPaths, start, count, mDataManager);
+    }
+
+    @Override
+    public long reload() {
+        if (mBaseSet.reload() > mDataVersion) {
+            updateData();
+            mDataVersion = nextVersionNumber();
+        }
+        return mDataVersion;
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    private void updateData() {
+        // Albums
+        mAlbums.clear();
+        String basePath = "/filter/mediatype/" + mMediaType;
+
+        for (int i = 0, n = mBaseSet.getSubMediaSetCount(); i < n; i++) {
+            MediaSet set = mBaseSet.getSubMediaSet(i);
+            String filteredPath = basePath + "/{" + set.getPath().toString() + "}";
+            MediaSet filteredSet = mDataManager.getMediaSet(filteredPath);
+            filteredSet.reload();
+            if (filteredSet.getMediaItemCount() > 0
+                    || filteredSet.getSubMediaSetCount() > 0) {
+                mAlbums.add(filteredSet);
+            }
+        }
+
+        // Items
+        mPaths.clear();
+        final int total = mBaseSet.getMediaItemCount();
+        final Path[] buf = new Path[total];
+
+        mBaseSet.enumerateMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                if (item.getMediaType() == mMediaType) {
+                    if (index < 0 || index >= total) return;
+                    Path path = item.getPath();
+                    buf[index] = path;
+                }
+            }
+        });
+
+        for (int i = 0; i < total; i++) {
+            if (buf[i] != null) {
+                mPaths.add(buf[i]);
+            }
+        }
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return SUPPORT_SHARE | SUPPORT_DELETE;
+    }
+
+    @Override
+    public void delete() {
+        ItemConsumer consumer = new ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) {
+                    item.delete();
+                }
+            }
+        };
+        mDataManager.mapMediaItems(mPaths, consumer, 0);
+    }
+}
diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java
new file mode 100644
index 0000000..6cbc5c5
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheRequest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+abstract class ImageCacheRequest implements Job<Bitmap> {
+    private static final String TAG = "ImageCacheRequest";
+
+    protected GalleryApp mApplication;
+    private Path mPath;
+    private int mType;
+    private int mTargetSize;
+    private long mTimeModified;
+
+    public ImageCacheRequest(GalleryApp application,
+            Path path, long timeModified, int type, int targetSize) {
+        mApplication = application;
+        mPath = path;
+        mType = type;
+        mTargetSize = targetSize;
+        mTimeModified = timeModified;
+    }
+
+    private String debugTag() {
+        return mPath + "," + mTimeModified + "," +
+                ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" :
+                (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?");
+    }
+
+    @Override
+    public Bitmap run(JobContext jc) {
+        ImageCacheService cacheService = mApplication.getImageCacheService();
+
+        BytesBuffer buffer = MediaItem.getBytesBufferPool().get();
+        try {
+            boolean found = cacheService.getImageData(mPath, mTimeModified, mType, buffer);
+            if (jc.isCancelled()) return null;
+            if (found) {
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+                Bitmap bitmap;
+                if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+                    bitmap = DecodeUtils.decodeUsingPool(jc,
+                            buffer.data, buffer.offset, buffer.length, options);
+                } else {
+                    bitmap = DecodeUtils.decodeUsingPool(jc,
+                            buffer.data, buffer.offset, buffer.length, options);
+                }
+                if (bitmap == null && !jc.isCancelled()) {
+                    Log.w(TAG, "decode cached failed " + debugTag());
+                }
+                return bitmap;
+            }
+        } finally {
+            MediaItem.getBytesBufferPool().recycle(buffer);
+        }
+        Bitmap bitmap = onDecodeOriginal(jc, mType);
+        if (jc.isCancelled()) return null;
+
+        if (bitmap == null) {
+            Log.w(TAG, "decode orig failed " + debugTag());
+            return null;
+        }
+
+        if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+            bitmap = BitmapUtils.resizeAndCropCenter(bitmap, mTargetSize, true);
+        } else {
+            bitmap = BitmapUtils.resizeDownBySideLength(bitmap, mTargetSize, true);
+        }
+        if (jc.isCancelled()) return null;
+
+        byte[] array = BitmapUtils.compressToBytes(bitmap);
+        if (jc.isCancelled()) return null;
+
+        cacheService.putImageData(mPath, mTimeModified, mType, array);
+        return bitmap;
+    }
+
+    public abstract Bitmap onDecodeOriginal(JobContext jc, int targetSize);
+}
diff --git a/src/com/android/gallery3d/data/ImageCacheService.java b/src/com/android/gallery3d/data/ImageCacheService.java
new file mode 100644
index 0000000..1c7cb8c
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheService.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.Context;
+
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.common.BlobCache.LookupRequest;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class ImageCacheService {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ImageCacheService";
+
+    private static final String IMAGE_CACHE_FILE = "imgcache";
+    private static final int IMAGE_CACHE_MAX_ENTRIES = 5000;
+    private static final int IMAGE_CACHE_MAX_BYTES = 200 * 1024 * 1024;
+    private static final int IMAGE_CACHE_VERSION = 7;
+
+    private BlobCache mCache;
+
+    public ImageCacheService(Context context) {
+        mCache = CacheManager.getCache(context, IMAGE_CACHE_FILE,
+                IMAGE_CACHE_MAX_ENTRIES, IMAGE_CACHE_MAX_BYTES,
+                IMAGE_CACHE_VERSION);
+    }
+
+    /**
+     * Gets the cached image data for the given <code>path</code>,
+     *  <code>timeModified</code> and <code>type</code>.
+     *
+     * The image data will be stored in <code>buffer.data</code>, started from
+     * <code>buffer.offset</code> for <code>buffer.length</code> bytes. If the
+     * buffer.data is not big enough, a new byte array will be allocated and returned.
+     *
+     * @return true if the image data is found; false if not found.
+     */
+    public boolean getImageData(Path path, long timeModified, int type, BytesBuffer buffer) {
+        byte[] key = makeKey(path, timeModified, type);
+        long cacheKey = Utils.crc64Long(key);
+        try {
+            LookupRequest request = new LookupRequest();
+            request.key = cacheKey;
+            request.buffer = buffer.data;
+            synchronized (mCache) {
+                if (!mCache.lookup(request)) return false;
+            }
+            if (isSameKey(key, request.buffer)) {
+                buffer.data = request.buffer;
+                buffer.offset = key.length;
+                buffer.length = request.length - buffer.offset;
+                return true;
+            }
+        } catch (IOException ex) {
+            // ignore.
+        }
+        return false;
+    }
+
+    public void putImageData(Path path, long timeModified, int type, byte[] value) {
+        byte[] key = makeKey(path, timeModified, type);
+        long cacheKey = Utils.crc64Long(key);
+        ByteBuffer buffer = ByteBuffer.allocate(key.length + value.length);
+        buffer.put(key);
+        buffer.put(value);
+        synchronized (mCache) {
+            try {
+                mCache.insert(cacheKey, buffer.array());
+            } catch (IOException ex) {
+                // ignore.
+            }
+        }
+    }
+
+    public void clearImageData(Path path, long timeModified, int type) {
+        byte[] key = makeKey(path, timeModified, type);
+        long cacheKey = Utils.crc64Long(key);
+        synchronized (mCache) {
+            try {
+                mCache.clearEntry(cacheKey);
+            } catch (IOException ex) {
+                // ignore.
+            }
+        }
+    }
+
+    private static byte[] makeKey(Path path, long timeModified, int type) {
+        return GalleryUtils.getBytes(path.toString() + "+" + timeModified + "+" + type);
+    }
+
+    private static boolean isSameKey(byte[] key, byte[] buffer) {
+        int n = key.length;
+        if (buffer.length < n) {
+            return false;
+        }
+        for (int i = 0; i < n; ++i) {
+            if (key[i] != buffer[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java
new file mode 100644
index 0000000..7b7015a
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbum.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+
+// LocalAlbumSet lists all media items in one bucket on local storage.
+// The media items need to be all images or all videos, but not both.
+public class LocalAlbum extends MediaSet {
+    private static final String TAG = "LocalAlbum";
+    private static final String[] COUNT_PROJECTION = { "count(*)" };
+
+    private static final int INVALID_COUNT = -1;
+    private final String mWhereClause;
+    private final String mOrderClause;
+    private final Uri mBaseUri;
+    private final String[] mProjection;
+
+    private final GalleryApp mApplication;
+    private final ContentResolver mResolver;
+    private final int mBucketId;
+    private final String mName;
+    private final boolean mIsImage;
+    private final ChangeNotifier mNotifier;
+    private final Path mItemPath;
+    private int mCachedCount = INVALID_COUNT;
+
+    public LocalAlbum(Path path, GalleryApp application, int bucketId,
+            boolean isImage, String name) {
+        super(path, nextVersionNumber());
+        mApplication = application;
+        mResolver = application.getContentResolver();
+        mBucketId = bucketId;
+        mName = name;
+        mIsImage = isImage;
+
+        if (isImage) {
+            mWhereClause = ImageColumns.BUCKET_ID + " = ?";
+            mOrderClause = ImageColumns.DATE_TAKEN + " DESC, "
+                    + ImageColumns._ID + " DESC";
+            mBaseUri = Images.Media.EXTERNAL_CONTENT_URI;
+            mProjection = LocalImage.PROJECTION;
+            mItemPath = LocalImage.ITEM_PATH;
+        } else {
+            mWhereClause = VideoColumns.BUCKET_ID + " = ?";
+            mOrderClause = VideoColumns.DATE_TAKEN + " DESC, "
+                    + VideoColumns._ID + " DESC";
+            mBaseUri = Video.Media.EXTERNAL_CONTENT_URI;
+            mProjection = LocalVideo.PROJECTION;
+            mItemPath = LocalVideo.ITEM_PATH;
+        }
+
+        mNotifier = new ChangeNotifier(this, mBaseUri, application);
+    }
+
+    public LocalAlbum(Path path, GalleryApp application, int bucketId,
+            boolean isImage) {
+        this(path, application, bucketId, isImage,
+                BucketHelper.getBucketName(
+                application.getContentResolver(), bucketId));
+    }
+
+    @Override
+    public boolean isCameraRoll() {
+        return mBucketId == MediaSetUtils.CAMERA_BUCKET_ID;
+    }
+
+    @Override
+    public Uri getContentUri() {
+        if (mIsImage) {
+            return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+                            String.valueOf(mBucketId)).build();
+        } else {
+            return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+                            String.valueOf(mBucketId)).build();
+        }
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        DataManager dataManager = mApplication.getDataManager();
+        Uri uri = mBaseUri.buildUpon()
+                .appendQueryParameter("limit", start + "," + count).build();
+        ArrayList<MediaItem> list = new ArrayList<MediaItem>();
+        GalleryUtils.assertNotInRenderThread();
+        Cursor cursor = mResolver.query(
+                uri, mProjection, mWhereClause,
+                new String[]{String.valueOf(mBucketId)},
+                mOrderClause);
+        if (cursor == null) {
+            Log.w(TAG, "query fail: " + uri);
+            return list;
+        }
+
+        try {
+            while (cursor.moveToNext()) {
+                int id = cursor.getInt(0);  // _id must be in the first column
+                Path childPath = mItemPath.getChild(id);
+                MediaItem item = loadOrUpdateItem(childPath, cursor,
+                        dataManager, mApplication, mIsImage);
+                list.add(item);
+            }
+        } finally {
+            cursor.close();
+        }
+        return list;
+    }
+
+    private static MediaItem loadOrUpdateItem(Path path, Cursor cursor,
+            DataManager dataManager, GalleryApp app, boolean isImage) {
+        synchronized (DataManager.LOCK) {
+            LocalMediaItem item = (LocalMediaItem) dataManager.peekMediaObject(path);
+            if (item == null) {
+                if (isImage) {
+                    item = new LocalImage(path, app, cursor);
+                } else {
+                    item = new LocalVideo(path, app, cursor);
+                }
+            } else {
+                item.updateContent(cursor);
+            }
+            return item;
+        }
+    }
+
+    // The pids array are sorted by the (path) id.
+    public static MediaItem[] getMediaItemById(
+            GalleryApp application, boolean isImage, ArrayList<Integer> ids) {
+        // get the lower and upper bound of (path) id
+        MediaItem[] result = new MediaItem[ids.size()];
+        if (ids.isEmpty()) return result;
+        int idLow = ids.get(0);
+        int idHigh = ids.get(ids.size() - 1);
+
+        // prepare the query parameters
+        Uri baseUri;
+        String[] projection;
+        Path itemPath;
+        if (isImage) {
+            baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+            projection = LocalImage.PROJECTION;
+            itemPath = LocalImage.ITEM_PATH;
+        } else {
+            baseUri = Video.Media.EXTERNAL_CONTENT_URI;
+            projection = LocalVideo.PROJECTION;
+            itemPath = LocalVideo.ITEM_PATH;
+        }
+
+        ContentResolver resolver = application.getContentResolver();
+        DataManager dataManager = application.getDataManager();
+        Cursor cursor = resolver.query(baseUri, projection, "_id BETWEEN ? AND ?",
+                new String[]{String.valueOf(idLow), String.valueOf(idHigh)},
+                "_id");
+        if (cursor == null) {
+            Log.w(TAG, "query fail" + baseUri);
+            return result;
+        }
+        try {
+            int n = ids.size();
+            int i = 0;
+
+            while (i < n && cursor.moveToNext()) {
+                int id = cursor.getInt(0);  // _id must be in the first column
+
+                // Match id with the one on the ids list.
+                if (ids.get(i) > id) {
+                    continue;
+                }
+
+                while (ids.get(i) < id) {
+                    if (++i >= n) {
+                        return result;
+                    }
+                }
+
+                Path childPath = itemPath.getChild(id);
+                MediaItem item = loadOrUpdateItem(childPath, cursor, dataManager,
+                        application, isImage);
+                result[i] = item;
+                ++i;
+            }
+            return result;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    public static Cursor getItemCursor(ContentResolver resolver, Uri uri,
+            String[] projection, int id) {
+        return resolver.query(uri, projection, "_id=?",
+                new String[]{String.valueOf(id)}, null);
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        if (mCachedCount == INVALID_COUNT) {
+            Cursor cursor = mResolver.query(
+                    mBaseUri, COUNT_PROJECTION, mWhereClause,
+                    new String[]{String.valueOf(mBucketId)}, null);
+            if (cursor == null) {
+                Log.w(TAG, "query fail");
+                return 0;
+            }
+            try {
+                Utils.assertTrue(cursor.moveToNext());
+                mCachedCount = cursor.getInt(0);
+            } finally {
+                cursor.close();
+            }
+        }
+        return mCachedCount;
+    }
+
+    @Override
+    public String getName() {
+        return getLocalizedName(mApplication.getResources(), mBucketId, mName);
+    }
+
+    @Override
+    public long reload() {
+        if (mNotifier.isDirty()) {
+            mDataVersion = nextVersionNumber();
+            mCachedCount = INVALID_COUNT;
+        }
+        return mDataVersion;
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_INFO;
+    }
+
+    @Override
+    public void delete() {
+        GalleryUtils.assertNotInRenderThread();
+        mResolver.delete(mBaseUri, mWhereClause,
+                new String[]{String.valueOf(mBucketId)});
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+
+    public static String getLocalizedName(Resources res, int bucketId,
+            String name) {
+        if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
+            return res.getString(R.string.folder_camera);
+        } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
+            return res.getString(R.string.folder_download);
+        } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
+            return res.getString(R.string.folder_imported);
+        } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
+            return res.getString(R.string.folder_screenshot);
+        } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
+            return res.getString(R.string.folder_edited_online_photos);
+        } else {
+            return name;
+        }
+    }
+
+    // Relative path is the absolute path minus external storage path
+    public static String getRelativePath(int bucketId) {
+        String relativePath = "/";
+        if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
+            relativePath += BucketNames.CAMERA;
+        } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
+            relativePath += BucketNames.DOWNLOAD;
+        } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
+            relativePath += BucketNames.IMPORTED;
+        } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
+            relativePath += BucketNames.SCREENSHOTS;
+        } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
+            relativePath += BucketNames.EDITED_ONLINE_PHOTOS;
+        } else {
+            // If the first few cases didn't hit the matching path, do a
+            // thorough search in the local directories.
+            File extStorage = Environment.getExternalStorageDirectory();
+            String path = GalleryUtils.searchDirForPath(extStorage, bucketId);
+            if (path == null) {
+                Log.w(TAG, "Relative path for bucket id: " + bucketId + " is not found.");
+                relativePath = null;
+            } else {
+                relativePath = path.substring(extStorage.getAbsolutePath().length());
+            }
+        }
+        return relativePath;
+    }
+
+}
diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java
new file mode 100644
index 0000000..b2b4b8c
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbumSet.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.BucketHelper.BucketEntry;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+// LocalAlbumSet lists all image or video albums in the local storage.
+// The path should be "/local/image", "local/video" or "/local/all"
+public class LocalAlbumSet extends MediaSet
+        implements FutureListener<ArrayList<MediaSet>> {
+    @SuppressWarnings("unused")
+    private static final String TAG = "LocalAlbumSet";
+
+    public static final Path PATH_ALL = Path.fromString("/local/all");
+    public static final Path PATH_IMAGE = Path.fromString("/local/image");
+    public static final Path PATH_VIDEO = Path.fromString("/local/video");
+
+    private static final Uri[] mWatchUris =
+        {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
+
+    private final GalleryApp mApplication;
+    private final int mType;
+    private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
+    private final ChangeNotifier mNotifier;
+    private final String mName;
+    private final Handler mHandler;
+    private boolean mIsLoading;
+
+    private Future<ArrayList<MediaSet>> mLoadTask;
+    private ArrayList<MediaSet> mLoadBuffer;
+
+    public LocalAlbumSet(Path path, GalleryApp application) {
+        super(path, nextVersionNumber());
+        mApplication = application;
+        mHandler = new Handler(application.getMainLooper());
+        mType = getTypeFromPath(path);
+        mNotifier = new ChangeNotifier(this, mWatchUris, application);
+        mName = application.getResources().getString(
+                R.string.set_label_local_albums);
+    }
+
+    private static int getTypeFromPath(Path path) {
+        String name[] = path.split();
+        if (name.length < 2) {
+            throw new IllegalArgumentException(path.toString());
+        }
+        return getTypeFromString(name[1]);
+    }
+
+    @Override
+    public MediaSet getSubMediaSet(int index) {
+        return mAlbums.get(index);
+    }
+
+    @Override
+    public int getSubMediaSetCount() {
+        return mAlbums.size();
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    private static int findBucket(BucketEntry entries[], int bucketId) {
+        for (int i = 0, n = entries.length; i < n; ++i) {
+            if (entries[i].bucketId == bucketId) return i;
+        }
+        return -1;
+    }
+
+    private class AlbumsLoader implements ThreadPool.Job<ArrayList<MediaSet>> {
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public ArrayList<MediaSet> run(JobContext jc) {
+            // Note: it will be faster if we only select media_type and bucket_id.
+            //       need to test the performance if that is worth
+            BucketEntry[] entries = BucketHelper.loadBucketEntries(
+                    jc, mApplication.getContentResolver(), mType);
+
+            if (jc.isCancelled()) return null;
+
+            int offset = 0;
+            // Move camera and download bucket to the front, while keeping the
+            // order of others.
+            int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
+            if (index != -1) {
+                circularShiftRight(entries, offset++, index);
+            }
+            index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
+            if (index != -1) {
+                circularShiftRight(entries, offset++, index);
+            }
+
+            ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
+            DataManager dataManager = mApplication.getDataManager();
+            for (BucketEntry entry : entries) {
+                MediaSet album = getLocalAlbum(dataManager,
+                        mType, mPath, entry.bucketId, entry.bucketName);
+                albums.add(album);
+            }
+            return albums;
+        }
+    }
+
+    private MediaSet getLocalAlbum(
+            DataManager manager, int type, Path parent, int id, String name) {
+        synchronized (DataManager.LOCK) {
+            Path path = parent.getChild(id);
+            MediaObject object = manager.peekMediaObject(path);
+            if (object != null) return (MediaSet) object;
+            switch (type) {
+                case MEDIA_TYPE_IMAGE:
+                    return new LocalAlbum(path, mApplication, id, true, name);
+                case MEDIA_TYPE_VIDEO:
+                    return new LocalAlbum(path, mApplication, id, false, name);
+                case MEDIA_TYPE_ALL:
+                    Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
+                    return new LocalMergeAlbum(path, comp, new MediaSet[] {
+                            getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name),
+                            getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}, id);
+            }
+            throw new IllegalArgumentException(String.valueOf(type));
+        }
+    }
+
+    @Override
+    public synchronized boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Override
+    // synchronized on this function for
+    //   1. Prevent calling reload() concurrently.
+    //   2. Prevent calling onFutureDone() and reload() concurrently
+    public synchronized long reload() {
+        if (mNotifier.isDirty()) {
+            if (mLoadTask != null) mLoadTask.cancel();
+            mIsLoading = true;
+            mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this);
+        }
+        if (mLoadBuffer != null) {
+            mAlbums = mLoadBuffer;
+            mLoadBuffer = null;
+            for (MediaSet album : mAlbums) {
+                album.reload();
+            }
+            mDataVersion = nextVersionNumber();
+        }
+        return mDataVersion;
+    }
+
+    @Override
+    public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) {
+        if (mLoadTask != future) return; // ignore, wait for the latest task
+        mLoadBuffer = future.get();
+        mIsLoading = false;
+        if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>();
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                notifyContentChanged();
+            }
+        });
+    }
+
+    // For debug only. Fake there is a ContentObserver.onChange() event.
+    void fakeChange() {
+        mNotifier.fakeChange();
+    }
+
+    // Circular shift the array range from a[i] to a[j] (inclusive). That is,
+    // a[i] -> a[i+1] -> a[i+2] -> ... -> a[j], and a[j] -> a[i]
+    private static <T> void circularShiftRight(T[] array, int i, int j) {
+        T temp = array[j];
+        for (int k = j; k > i; k--) {
+            array[k] = array[k - 1];
+        }
+        array[i] = temp;
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
new file mode 100644
index 0000000..cc70dd4
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
+import com.android.gallery3d.app.StitchingProgressManager;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+// LocalImage represents an image in the local storage.
+public class LocalImage extends LocalMediaItem {
+    private static final String TAG = "LocalImage";
+
+    static final Path ITEM_PATH = Path.fromString("/local/image/item");
+
+    // Must preserve order between these indices and the order of the terms in
+    // the following PROJECTION array.
+    private static final int INDEX_ID = 0;
+    private static final int INDEX_CAPTION = 1;
+    private static final int INDEX_MIME_TYPE = 2;
+    private static final int INDEX_LATITUDE = 3;
+    private static final int INDEX_LONGITUDE = 4;
+    private static final int INDEX_DATE_TAKEN = 5;
+    private static final int INDEX_DATE_ADDED = 6;
+    private static final int INDEX_DATE_MODIFIED = 7;
+    private static final int INDEX_DATA = 8;
+    private static final int INDEX_ORIENTATION = 9;
+    private static final int INDEX_BUCKET_ID = 10;
+    private static final int INDEX_SIZE = 11;
+    private static final int INDEX_WIDTH = 12;
+    private static final int INDEX_HEIGHT = 13;
+
+    static final String[] PROJECTION =  {
+            ImageColumns._ID,           // 0
+            ImageColumns.TITLE,         // 1
+            ImageColumns.MIME_TYPE,     // 2
+            ImageColumns.LATITUDE,      // 3
+            ImageColumns.LONGITUDE,     // 4
+            ImageColumns.DATE_TAKEN,    // 5
+            ImageColumns.DATE_ADDED,    // 6
+            ImageColumns.DATE_MODIFIED, // 7
+            ImageColumns.DATA,          // 8
+            ImageColumns.ORIENTATION,   // 9
+            ImageColumns.BUCKET_ID,     // 10
+            ImageColumns.SIZE,          // 11
+            "0",                        // 12
+            "0"                         // 13
+    };
+
+    static {
+        updateWidthAndHeightProjection();
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private static void updateWidthAndHeightProjection() {
+        if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
+            PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH;
+            PROJECTION[INDEX_HEIGHT] = MediaColumns.HEIGHT;
+        }
+    }
+
+    private final GalleryApp mApplication;
+
+    public int rotation;
+
+    private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
+
+    public LocalImage(Path path, GalleryApp application, Cursor cursor) {
+        super(path, nextVersionNumber());
+        mApplication = application;
+        loadFromCursor(cursor);
+    }
+
+    public LocalImage(Path path, GalleryApp application, int id) {
+        super(path, nextVersionNumber());
+        mApplication = application;
+        ContentResolver resolver = mApplication.getContentResolver();
+        Uri uri = Images.Media.EXTERNAL_CONTENT_URI;
+        Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id);
+        if (cursor == null) {
+            throw new RuntimeException("cannot get cursor for: " + path);
+        }
+        try {
+            if (cursor.moveToNext()) {
+                loadFromCursor(cursor);
+            } else {
+                throw new RuntimeException("cannot find data for: " + path);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void loadFromCursor(Cursor cursor) {
+        id = cursor.getInt(INDEX_ID);
+        caption = cursor.getString(INDEX_CAPTION);
+        mimeType = cursor.getString(INDEX_MIME_TYPE);
+        latitude = cursor.getDouble(INDEX_LATITUDE);
+        longitude = cursor.getDouble(INDEX_LONGITUDE);
+        dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
+        dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+        dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+        filePath = cursor.getString(INDEX_DATA);
+        rotation = cursor.getInt(INDEX_ORIENTATION);
+        bucketId = cursor.getInt(INDEX_BUCKET_ID);
+        fileSize = cursor.getLong(INDEX_SIZE);
+        width = cursor.getInt(INDEX_WIDTH);
+        height = cursor.getInt(INDEX_HEIGHT);
+    }
+
+    @Override
+    protected boolean updateFromCursor(Cursor cursor) {
+        UpdateHelper uh = new UpdateHelper();
+        id = uh.update(id, cursor.getInt(INDEX_ID));
+        caption = uh.update(caption, cursor.getString(INDEX_CAPTION));
+        mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE));
+        latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE));
+        longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE));
+        dateTakenInMs = uh.update(
+                dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN));
+        dateAddedInSec = uh.update(
+                dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED));
+        dateModifiedInSec = uh.update(
+                dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED));
+        filePath = uh.update(filePath, cursor.getString(INDEX_DATA));
+        rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION));
+        bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
+        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
+        width = uh.update(width, cursor.getInt(INDEX_WIDTH));
+        height = uh.update(height, cursor.getInt(INDEX_HEIGHT));
+        return uh.isUpdated();
+    }
+
+    @Override
+    public Job<Bitmap> requestImage(int type) {
+        return new LocalImageRequest(mApplication, mPath, dateModifiedInSec,
+                type, filePath);
+    }
+
+    public static class LocalImageRequest extends ImageCacheRequest {
+        private String mLocalFilePath;
+
+        LocalImageRequest(GalleryApp application, Path path, long timeModified,
+                int type, String localFilePath) {
+            super(application, path, timeModified, type,
+                    MediaItem.getTargetSize(type));
+            mLocalFilePath = localFilePath;
+        }
+
+        @Override
+        public Bitmap onDecodeOriginal(JobContext jc, final int type) {
+            BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+            int targetSize = MediaItem.getTargetSize(type);
+
+            // try to decode from JPEG EXIF
+            if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+                ExifInterface exif = new ExifInterface();
+                byte[] thumbData = null;
+                try {
+                    exif.readExif(mLocalFilePath);
+                    thumbData = exif.getThumbnail();
+                } catch (FileNotFoundException e) {
+                    Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath);
+                } catch (IOException e) {
+                    Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath);
+                }
+                if (thumbData != null) {
+                    Bitmap bitmap = DecodeUtils.decodeIfBigEnough(
+                            jc, thumbData, options, targetSize);
+                    if (bitmap != null) return bitmap;
+                }
+            }
+
+            return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type);
+        }
+    }
+
+    @Override
+    public Job<BitmapRegionDecoder> requestLargeImage() {
+        return new LocalLargeImageRequest(filePath);
+    }
+
+    public static class LocalLargeImageRequest
+            implements Job<BitmapRegionDecoder> {
+        String mLocalFilePath;
+
+        public LocalLargeImageRequest(String localFilePath) {
+            mLocalFilePath = localFilePath;
+        }
+
+        @Override
+        public BitmapRegionDecoder run(JobContext jc) {
+            return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false);
+        }
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+        if (progressManager != null && progressManager.getProgress(getContentUri()) != null) {
+            return 0; // doesn't support anything while stitching!
+        }
+        int operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP
+                | SUPPORT_SETAS | SUPPORT_EDIT | SUPPORT_INFO;
+        if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) {
+            operation |= SUPPORT_FULL_IMAGE;
+        }
+
+        if (BitmapUtils.isRotationSupported(mimeType)) {
+            operation |= SUPPORT_ROTATE;
+        }
+
+        if (GalleryUtils.isValidLocation(latitude, longitude)) {
+            operation |= SUPPORT_SHOW_ON_MAP;
+        }
+        return operation;
+    }
+
+    @Override
+    public void getPanoramaSupport(PanoramaSupportCallback callback) {
+        mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
+    }
+
+    @Override
+    public void clearCachedPanoramaSupport() {
+        mPanoramaMetadata.clearCachedValues();
+    }
+
+    @Override
+    public void delete() {
+        GalleryUtils.assertNotInRenderThread();
+        Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+        ContentResolver contentResolver = mApplication.getContentResolver();
+        SaveImage.deleteAuxFiles(contentResolver, getContentUri());
+        contentResolver.delete(baseUri, "_id=?",
+                new String[]{String.valueOf(id)});
+    }
+
+    @Override
+    public void rotate(int degrees) {
+        GalleryUtils.assertNotInRenderThread();
+        Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+        ContentValues values = new ContentValues();
+        int rotation = (this.rotation + degrees) % 360;
+        if (rotation < 0) rotation += 360;
+
+        if (mimeType.equalsIgnoreCase("image/jpeg")) {
+            ExifInterface exifInterface = new ExifInterface();
+            ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.getOrientationValueForRotation(rotation));
+            if(tag != null) {
+                exifInterface.setTag(tag);
+                try {
+                    exifInterface.forceRewriteExif(filePath);
+                    fileSize = new File(filePath).length();
+                    values.put(Images.Media.SIZE, fileSize);
+                } catch (FileNotFoundException e) {
+                    Log.w(TAG, "cannot find file to set exif: " + filePath);
+                } catch (IOException e) {
+                    Log.w(TAG, "cannot set exif data: " + filePath);
+                }
+            } else {
+                Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION);
+            }
+        }
+
+        values.put(Images.Media.ORIENTATION, rotation);
+        mApplication.getContentResolver().update(baseUri, values, "_id=?",
+                new String[]{String.valueOf(id)});
+    }
+
+    @Override
+    public Uri getContentUri() {
+        Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+        return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
+    }
+
+    @Override
+    public int getMediaType() {
+        return MEDIA_TYPE_IMAGE;
+    }
+
+    @Override
+    public MediaDetails getDetails() {
+        MediaDetails details = super.getDetails();
+        details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation));
+        if (MIME_TYPE_JPEG.equals(mimeType)) {
+            // ExifInterface returns incorrect values for photos in other format.
+            // For example, the width and height of an webp images is always '0'.
+            MediaDetails.extractExifInfo(details, filePath);
+        }
+        return details;
+    }
+
+    @Override
+    public int getRotation() {
+        return rotation;
+    }
+
+    @Override
+    public int getWidth() {
+        return width;
+    }
+
+    @Override
+    public int getHeight() {
+        return height;
+    }
+
+    @Override
+    public String getFilePath() {
+        return filePath;
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java
new file mode 100644
index 0000000..7e003cd
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMediaItem.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.database.Cursor;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+//
+// LocalMediaItem is an abstract class captures those common fields
+// in LocalImage and LocalVideo.
+//
+public abstract class LocalMediaItem extends MediaItem {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "LocalMediaItem";
+
+    // database fields
+    public int id;
+    public String caption;
+    public String mimeType;
+    public long fileSize;
+    public double latitude = INVALID_LATLNG;
+    public double longitude = INVALID_LATLNG;
+    public long dateTakenInMs;
+    public long dateAddedInSec;
+    public long dateModifiedInSec;
+    public String filePath;
+    public int bucketId;
+    public int width;
+    public int height;
+
+    public LocalMediaItem(Path path, long version) {
+        super(path, version);
+    }
+
+    @Override
+    public long getDateInMs() {
+        return dateTakenInMs;
+    }
+
+    @Override
+    public String getName() {
+        return caption;
+    }
+
+    @Override
+    public void getLatLong(double[] latLong) {
+        latLong[0] = latitude;
+        latLong[1] = longitude;
+    }
+
+    abstract protected boolean updateFromCursor(Cursor cursor);
+
+    public int getBucketId() {
+        return bucketId;
+    }
+
+    protected void updateContent(Cursor cursor) {
+        if (updateFromCursor(cursor)) {
+            mDataVersion = nextVersionNumber();
+        }
+    }
+
+    @Override
+    public MediaDetails getDetails() {
+        MediaDetails details = super.getDetails();
+        details.addDetail(MediaDetails.INDEX_PATH, filePath);
+        details.addDetail(MediaDetails.INDEX_TITLE, caption);
+        DateFormat formater = DateFormat.getDateTimeInstance();
+        details.addDetail(MediaDetails.INDEX_DATETIME,
+                formater.format(new Date(dateModifiedInSec * 1000)));
+        details.addDetail(MediaDetails.INDEX_WIDTH, width);
+        details.addDetail(MediaDetails.INDEX_HEIGHT, height);
+
+        if (GalleryUtils.isValidLocation(latitude, longitude)) {
+            details.addDetail(MediaDetails.INDEX_LOCATION, new double[] {latitude, longitude});
+        }
+        if (fileSize > 0) details.addDetail(MediaDetails.INDEX_SIZE, fileSize);
+        return details;
+    }
+
+    @Override
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    @Override
+    public long getSize() {
+        return fileSize;
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocalMergeAlbum.java b/src/com/android/gallery3d/data/LocalMergeAlbum.java
new file mode 100644
index 0000000..f0b5e57
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+// MergeAlbum merges items from two or more MediaSets. It uses a Comparator to
+// determine the order of items. The items are assumed to be sorted in the input
+// media sets (with the same order that the Comparator uses).
+//
+// This only handles MediaItems, not SubMediaSets.
+public class LocalMergeAlbum extends MediaSet implements ContentListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "LocalMergeAlbum";
+    private static final int PAGE_SIZE = 64;
+
+    private final Comparator<MediaItem> mComparator;
+    private final MediaSet[] mSources;
+
+    private FetchCache[] mFetcher;
+    private int mSupportedOperation;
+    private int mBucketId;
+
+    // mIndex maps global position to the position of each underlying media sets.
+    private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>();
+
+    public LocalMergeAlbum(
+            Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId) {
+        super(path, INVALID_DATA_VERSION);
+        mComparator = comparator;
+        mSources = sources;
+        mBucketId = bucketId;
+        for (MediaSet set : mSources) {
+            set.addContentListener(this);
+        }
+        reload();
+    }
+
+    @Override
+    public boolean isCameraRoll() {
+        if (mSources.length == 0) return false;
+        for(MediaSet set : mSources) {
+            if (!set.isCameraRoll()) return false;
+        }
+        return true;
+    }
+
+    private void updateData() {
+        ArrayList<MediaSet> matches = new ArrayList<MediaSet>();
+        int supported = mSources.length == 0 ? 0 : MediaItem.SUPPORT_ALL;
+        mFetcher = new FetchCache[mSources.length];
+        for (int i = 0, n = mSources.length; i < n; ++i) {
+            mFetcher[i] = new FetchCache(mSources[i]);
+            supported &= mSources[i].getSupportedOperations();
+        }
+        mSupportedOperation = supported;
+        mIndex.clear();
+        mIndex.put(0, new int[mSources.length]);
+    }
+
+    private void invalidateCache() {
+        for (int i = 0, n = mSources.length; i < n; i++) {
+            mFetcher[i].invalidate();
+        }
+        mIndex.clear();
+        mIndex.put(0, new int[mSources.length]);
+    }
+
+    @Override
+    public Uri getContentUri() {
+        String bucketId = String.valueOf(mBucketId);
+        if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+            return MediaStore.Files.getContentUri("external").buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+                    .build();
+        } else {
+            // We don't have a single URL for a merged image before ICS
+            // So we used the image's URL as a substitute.
+            return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+                    .build();
+        }
+    }
+
+    @Override
+    public String getName() {
+        return mSources.length == 0 ? "" : mSources[0].getName();
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return getTotalMediaItemCount();
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+
+        // First find the nearest mark position <= start.
+        SortedMap<Integer, int[]> head = mIndex.headMap(start + 1);
+        int markPos = head.lastKey();
+        int[] subPos = head.get(markPos).clone();
+        MediaItem[] slot = new MediaItem[mSources.length];
+
+        int size = mSources.length;
+
+        // fill all slots
+        for (int i = 0; i < size; i++) {
+            slot[i] = mFetcher[i].getItem(subPos[i]);
+        }
+
+        ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+
+        for (int i = markPos; i < start + count; i++) {
+            int k = -1;  // k points to the best slot up to now.
+            for (int j = 0; j < size; j++) {
+                if (slot[j] != null) {
+                    if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) {
+                        k = j;
+                    }
+                }
+            }
+
+            // If we don't have anything, all streams are exhausted.
+            if (k == -1) break;
+
+            // Pick the best slot and refill it.
+            subPos[k]++;
+            if (i >= start) {
+                result.add(slot[k]);
+            }
+            slot[k] = mFetcher[k].getItem(subPos[k]);
+
+            // Periodically leave a mark in the index, so we can come back later.
+            if ((i + 1) % PAGE_SIZE == 0) {
+                mIndex.put(i + 1, subPos.clone());
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public int getTotalMediaItemCount() {
+        int count = 0;
+        for (MediaSet set : mSources) {
+            count += set.getTotalMediaItemCount();
+        }
+        return count;
+    }
+
+    @Override
+    public long reload() {
+        boolean changed = false;
+        for (int i = 0, n = mSources.length; i < n; ++i) {
+            if (mSources[i].reload() > mDataVersion) changed = true;
+        }
+        if (changed) {
+            mDataVersion = nextVersionNumber();
+            updateData();
+            invalidateCache();
+        }
+        return mDataVersion;
+    }
+
+    @Override
+    public void onContentDirty() {
+        notifyContentChanged();
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return mSupportedOperation;
+    }
+
+    @Override
+    public void delete() {
+        for (MediaSet set : mSources) {
+            set.delete();
+        }
+    }
+
+    @Override
+    public void rotate(int degrees) {
+        for (MediaSet set : mSources) {
+            set.rotate(degrees);
+        }
+    }
+
+    private static class FetchCache {
+        private MediaSet mBaseSet;
+        private SoftReference<ArrayList<MediaItem>> mCacheRef;
+        private int mStartPos;
+
+        public FetchCache(MediaSet baseSet) {
+            mBaseSet = baseSet;
+        }
+
+        public void invalidate() {
+            mCacheRef = null;
+        }
+
+        public MediaItem getItem(int index) {
+            boolean needLoading = false;
+            ArrayList<MediaItem> cache = null;
+            if (mCacheRef == null
+                    || index < mStartPos || index >= mStartPos + PAGE_SIZE) {
+                needLoading = true;
+            } else {
+                cache = mCacheRef.get();
+                if (cache == null) {
+                    needLoading = true;
+                }
+            }
+
+            if (needLoading) {
+                cache = mBaseSet.getMediaItem(index, PAGE_SIZE);
+                mCacheRef = new SoftReference<ArrayList<MediaItem>>(cache);
+                mStartPos = index;
+            }
+
+            if (index < mStartPos || index >= mStartPos + cache.size()) {
+                return null;
+            }
+
+            return cache.get(index - mStartPos);
+        }
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocalSource.java b/src/com/android/gallery3d/data/LocalSource.java
new file mode 100644
index 0000000..a2e3d14
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalSource.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentProviderClient;
+import android.content.ContentUris;
+import android.content.UriMatcher;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+class LocalSource extends MediaSource {
+
+    public static final String KEY_BUCKET_ID = "bucketId";
+
+    private GalleryApp mApplication;
+    private PathMatcher mMatcher;
+    private static final int NO_MATCH = -1;
+    private final UriMatcher mUriMatcher = new UriMatcher(NO_MATCH);
+    public static final Comparator<PathId> sIdComparator = new IdComparator();
+
+    private static final int LOCAL_IMAGE_ALBUMSET = 0;
+    private static final int LOCAL_VIDEO_ALBUMSET = 1;
+    private static final int LOCAL_IMAGE_ALBUM = 2;
+    private static final int LOCAL_VIDEO_ALBUM = 3;
+    private static final int LOCAL_IMAGE_ITEM = 4;
+    private static final int LOCAL_VIDEO_ITEM = 5;
+    private static final int LOCAL_ALL_ALBUMSET = 6;
+    private static final int LOCAL_ALL_ALBUM = 7;
+
+    private static final String TAG = "LocalSource";
+
+    private ContentProviderClient mClient;
+
+    public LocalSource(GalleryApp context) {
+        super("local");
+        mApplication = context;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/local/image", LOCAL_IMAGE_ALBUMSET);
+        mMatcher.add("/local/video", LOCAL_VIDEO_ALBUMSET);
+        mMatcher.add("/local/all", LOCAL_ALL_ALBUMSET);
+
+        mMatcher.add("/local/image/*", LOCAL_IMAGE_ALBUM);
+        mMatcher.add("/local/video/*", LOCAL_VIDEO_ALBUM);
+        mMatcher.add("/local/all/*", LOCAL_ALL_ALBUM);
+        mMatcher.add("/local/image/item/*", LOCAL_IMAGE_ITEM);
+        mMatcher.add("/local/video/item/*", LOCAL_VIDEO_ITEM);
+
+        mUriMatcher.addURI(MediaStore.AUTHORITY,
+                "external/images/media/#", LOCAL_IMAGE_ITEM);
+        mUriMatcher.addURI(MediaStore.AUTHORITY,
+                "external/video/media/#", LOCAL_VIDEO_ITEM);
+        mUriMatcher.addURI(MediaStore.AUTHORITY,
+                "external/images/media", LOCAL_IMAGE_ALBUM);
+        mUriMatcher.addURI(MediaStore.AUTHORITY,
+                "external/video/media", LOCAL_VIDEO_ALBUM);
+        mUriMatcher.addURI(MediaStore.AUTHORITY,
+                "external/file", LOCAL_ALL_ALBUM);
+    }
+
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        GalleryApp app = mApplication;
+        switch (mMatcher.match(path)) {
+            case LOCAL_ALL_ALBUMSET:
+            case LOCAL_IMAGE_ALBUMSET:
+            case LOCAL_VIDEO_ALBUMSET:
+                return new LocalAlbumSet(path, mApplication);
+            case LOCAL_IMAGE_ALBUM:
+                return new LocalAlbum(path, app, mMatcher.getIntVar(0), true);
+            case LOCAL_VIDEO_ALBUM:
+                return new LocalAlbum(path, app, mMatcher.getIntVar(0), false);
+            case LOCAL_ALL_ALBUM: {
+                int bucketId = mMatcher.getIntVar(0);
+                DataManager dataManager = app.getDataManager();
+                MediaSet imageSet = (MediaSet) dataManager.getMediaObject(
+                        LocalAlbumSet.PATH_IMAGE.getChild(bucketId));
+                MediaSet videoSet = (MediaSet) dataManager.getMediaObject(
+                        LocalAlbumSet.PATH_VIDEO.getChild(bucketId));
+                Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
+                return new LocalMergeAlbum(
+                        path, comp, new MediaSet[] {imageSet, videoSet}, bucketId);
+            }
+            case LOCAL_IMAGE_ITEM:
+                return new LocalImage(path, mApplication, mMatcher.getIntVar(0));
+            case LOCAL_VIDEO_ITEM:
+                return new LocalVideo(path, mApplication, mMatcher.getIntVar(0));
+            default:
+                throw new RuntimeException("bad path: " + path);
+        }
+    }
+
+    private static int getMediaType(String type, int defaultType) {
+        if (type == null) return defaultType;
+        try {
+            int value = Integer.parseInt(type);
+            if ((value & (MEDIA_TYPE_IMAGE
+                    | MEDIA_TYPE_VIDEO)) != 0) return value;
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "invalid type: " + type, e);
+        }
+        return defaultType;
+    }
+
+    // The media type bit passed by the intent
+    private static final int MEDIA_TYPE_ALL = 0;
+    private static final int MEDIA_TYPE_IMAGE = 1;
+    private static final int MEDIA_TYPE_VIDEO = 4;
+
+    private Path getAlbumPath(Uri uri, int defaultType) {
+        int mediaType = getMediaType(
+                uri.getQueryParameter(Gallery.KEY_MEDIA_TYPES),
+                defaultType);
+        String bucketId = uri.getQueryParameter(KEY_BUCKET_ID);
+        int id = 0;
+        try {
+            id = Integer.parseInt(bucketId);
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "invalid bucket id: " + bucketId, e);
+            return null;
+        }
+        switch (mediaType) {
+            case MEDIA_TYPE_IMAGE:
+                return Path.fromString("/local/image").getChild(id);
+            case MEDIA_TYPE_VIDEO:
+                return Path.fromString("/local/video").getChild(id);
+            default:
+                return Path.fromString("/local/all").getChild(id);
+        }
+    }
+
+    @Override
+    public Path findPathByUri(Uri uri, String type) {
+        try {
+            switch (mUriMatcher.match(uri)) {
+                case LOCAL_IMAGE_ITEM: {
+                    long id = ContentUris.parseId(uri);
+                    return id >= 0 ? LocalImage.ITEM_PATH.getChild(id) : null;
+                }
+                case LOCAL_VIDEO_ITEM: {
+                    long id = ContentUris.parseId(uri);
+                    return id >= 0 ? LocalVideo.ITEM_PATH.getChild(id) : null;
+                }
+                case LOCAL_IMAGE_ALBUM: {
+                    return getAlbumPath(uri, MEDIA_TYPE_IMAGE);
+                }
+                case LOCAL_VIDEO_ALBUM: {
+                    return getAlbumPath(uri, MEDIA_TYPE_VIDEO);
+                }
+                case LOCAL_ALL_ALBUM: {
+                    return getAlbumPath(uri, MEDIA_TYPE_ALL);
+                }
+            }
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "uri: " + uri.toString(), e);
+        }
+        return null;
+    }
+
+    @Override
+    public Path getDefaultSetOf(Path item) {
+        MediaObject object = mApplication.getDataManager().getMediaObject(item);
+        if (object instanceof LocalMediaItem) {
+            return Path.fromString("/local/all").getChild(
+                    String.valueOf(((LocalMediaItem) object).getBucketId()));
+        }
+        return null;
+    }
+
+    @Override
+    public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) {
+        ArrayList<PathId> imageList = new ArrayList<PathId>();
+        ArrayList<PathId> videoList = new ArrayList<PathId>();
+        int n = list.size();
+        for (int i = 0; i < n; i++) {
+            PathId pid = list.get(i);
+            // We assume the form is: "/local/{image,video}/item/#"
+            // We don't use mMatcher for efficiency's reason.
+            Path parent = pid.path.getParent();
+            if (parent == LocalImage.ITEM_PATH) {
+                imageList.add(pid);
+            } else if (parent == LocalVideo.ITEM_PATH) {
+                videoList.add(pid);
+            }
+        }
+        // TODO: use "files" table so we can merge the two cases.
+        processMapMediaItems(imageList, consumer, true);
+        processMapMediaItems(videoList, consumer, false);
+    }
+
+    private void processMapMediaItems(ArrayList<PathId> list,
+            ItemConsumer consumer, boolean isImage) {
+        // Sort path by path id
+        Collections.sort(list, sIdComparator);
+        int n = list.size();
+        for (int i = 0; i < n; ) {
+            PathId pid = list.get(i);
+
+            // Find a range of items.
+            ArrayList<Integer> ids = new ArrayList<Integer>();
+            int startId = Integer.parseInt(pid.path.getSuffix());
+            ids.add(startId);
+
+            int j;
+            for (j = i + 1; j < n; j++) {
+                PathId pid2 = list.get(j);
+                int curId = Integer.parseInt(pid2.path.getSuffix());
+                if (curId - startId >= MediaSet.MEDIAITEM_BATCH_FETCH_COUNT) {
+                    break;
+                }
+                ids.add(curId);
+            }
+
+            MediaItem[] items = LocalAlbum.getMediaItemById(
+                    mApplication, isImage, ids);
+            for(int k = i ; k < j; k++) {
+                PathId pid2 = list.get(k);
+                consumer.consume(pid2.id, items[k - i]);
+            }
+
+            i = j;
+        }
+    }
+
+    // This is a comparator which compares the suffix number in two Paths.
+    private static class IdComparator implements Comparator<PathId> {
+        @Override
+        public int compare(PathId p1, PathId p2) {
+            String s1 = p1.path.getSuffix();
+            String s2 = p2.path.getSuffix();
+            int len1 = s1.length();
+            int len2 = s2.length();
+            if (len1 < len2) {
+                return -1;
+            } else if (len1 > len2) {
+                return 1;
+            } else {
+                return s1.compareTo(s2);
+            }
+        }
+    }
+
+    @Override
+    public void resume() {
+        mClient = mApplication.getContentResolver()
+                .acquireContentProviderClient(MediaStore.AUTHORITY);
+    }
+
+    @Override
+    public void pause() {
+        mClient.release();
+        mClient = null;
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java
new file mode 100644
index 0000000..4b8774c
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
+// LocalVideo represents a video in the local storage.
+public class LocalVideo extends LocalMediaItem {
+    private static final String TAG = "LocalVideo";
+    static final Path ITEM_PATH = Path.fromString("/local/video/item");
+
+    // Must preserve order between these indices and the order of the terms in
+    // the following PROJECTION array.
+    private static final int INDEX_ID = 0;
+    private static final int INDEX_CAPTION = 1;
+    private static final int INDEX_MIME_TYPE = 2;
+    private static final int INDEX_LATITUDE = 3;
+    private static final int INDEX_LONGITUDE = 4;
+    private static final int INDEX_DATE_TAKEN = 5;
+    private static final int INDEX_DATE_ADDED = 6;
+    private static final int INDEX_DATE_MODIFIED = 7;
+    private static final int INDEX_DATA = 8;
+    private static final int INDEX_DURATION = 9;
+    private static final int INDEX_BUCKET_ID = 10;
+    private static final int INDEX_SIZE = 11;
+    private static final int INDEX_RESOLUTION = 12;
+
+    static final String[] PROJECTION = new String[] {
+            VideoColumns._ID,
+            VideoColumns.TITLE,
+            VideoColumns.MIME_TYPE,
+            VideoColumns.LATITUDE,
+            VideoColumns.LONGITUDE,
+            VideoColumns.DATE_TAKEN,
+            VideoColumns.DATE_ADDED,
+            VideoColumns.DATE_MODIFIED,
+            VideoColumns.DATA,
+            VideoColumns.DURATION,
+            VideoColumns.BUCKET_ID,
+            VideoColumns.SIZE,
+            VideoColumns.RESOLUTION,
+    };
+
+    private final GalleryApp mApplication;
+
+    public int durationInSec;
+
+    public LocalVideo(Path path, GalleryApp application, Cursor cursor) {
+        super(path, nextVersionNumber());
+        mApplication = application;
+        loadFromCursor(cursor);
+    }
+
+    public LocalVideo(Path path, GalleryApp context, int id) {
+        super(path, nextVersionNumber());
+        mApplication = context;
+        ContentResolver resolver = mApplication.getContentResolver();
+        Uri uri = Video.Media.EXTERNAL_CONTENT_URI;
+        Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id);
+        if (cursor == null) {
+            throw new RuntimeException("cannot get cursor for: " + path);
+        }
+        try {
+            if (cursor.moveToNext()) {
+                loadFromCursor(cursor);
+            } else {
+                throw new RuntimeException("cannot find data for: " + path);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void loadFromCursor(Cursor cursor) {
+        id = cursor.getInt(INDEX_ID);
+        caption = cursor.getString(INDEX_CAPTION);
+        mimeType = cursor.getString(INDEX_MIME_TYPE);
+        latitude = cursor.getDouble(INDEX_LATITUDE);
+        longitude = cursor.getDouble(INDEX_LONGITUDE);
+        dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
+        dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+        dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+        filePath = cursor.getString(INDEX_DATA);
+        durationInSec = cursor.getInt(INDEX_DURATION) / 1000;
+        bucketId = cursor.getInt(INDEX_BUCKET_ID);
+        fileSize = cursor.getLong(INDEX_SIZE);
+        parseResolution(cursor.getString(INDEX_RESOLUTION));
+    }
+
+    private void parseResolution(String resolution) {
+        if (resolution == null) return;
+        int m = resolution.indexOf('x');
+        if (m == -1) return;
+        try {
+            int w = Integer.parseInt(resolution.substring(0, m));
+            int h = Integer.parseInt(resolution.substring(m + 1));
+            width = w;
+            height = h;
+        } catch (Throwable t) {
+            Log.w(TAG, t);
+        }
+    }
+
+    @Override
+    protected boolean updateFromCursor(Cursor cursor) {
+        UpdateHelper uh = new UpdateHelper();
+        id = uh.update(id, cursor.getInt(INDEX_ID));
+        caption = uh.update(caption, cursor.getString(INDEX_CAPTION));
+        mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE));
+        latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE));
+        longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE));
+        dateTakenInMs = uh.update(
+                dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN));
+        dateAddedInSec = uh.update(
+                dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED));
+        dateModifiedInSec = uh.update(
+                dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED));
+        filePath = uh.update(filePath, cursor.getString(INDEX_DATA));
+        durationInSec = uh.update(
+                durationInSec, cursor.getInt(INDEX_DURATION) / 1000);
+        bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
+        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
+        return uh.isUpdated();
+    }
+
+    @Override
+    public Job<Bitmap> requestImage(int type) {
+        return new LocalVideoRequest(mApplication, getPath(), dateModifiedInSec,
+                type, filePath);
+    }
+
+    public static class LocalVideoRequest extends ImageCacheRequest {
+        private String mLocalFilePath;
+
+        LocalVideoRequest(GalleryApp application, Path path, long timeModified,
+                int type, String localFilePath) {
+            super(application, path, timeModified, type,
+                    MediaItem.getTargetSize(type));
+            mLocalFilePath = localFilePath;
+        }
+
+        @Override
+        public Bitmap onDecodeOriginal(JobContext jc, int type) {
+            Bitmap bitmap = BitmapUtils.createVideoThumbnail(mLocalFilePath);
+            if (bitmap == null || jc.isCancelled()) return null;
+            return bitmap;
+        }
+    }
+
+    @Override
+    public Job<BitmapRegionDecoder> requestLargeImage() {
+        throw new UnsupportedOperationException("Cannot regquest a large image"
+                + " to a local video!");
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_PLAY | SUPPORT_INFO | SUPPORT_TRIM | SUPPORT_MUTE;
+    }
+
+    @Override
+    public void delete() {
+        GalleryUtils.assertNotInRenderThread();
+        Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI;
+        mApplication.getContentResolver().delete(baseUri, "_id=?",
+                new String[]{String.valueOf(id)});
+    }
+
+    @Override
+    public void rotate(int degrees) {
+        // TODO
+    }
+
+    @Override
+    public Uri getContentUri() {
+        Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI;
+        return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
+    }
+
+    @Override
+    public Uri getPlayUri() {
+        return getContentUri();
+    }
+
+    @Override
+    public int getMediaType() {
+        return MEDIA_TYPE_VIDEO;
+    }
+
+    @Override
+    public MediaDetails getDetails() {
+        MediaDetails details = super.getDetails();
+        int s = durationInSec;
+        if (s > 0) {
+            details.addDetail(MediaDetails.INDEX_DURATION, GalleryUtils.formatDuration(
+                    mApplication.getAndroidContext(), durationInSec));
+        }
+        return details;
+    }
+
+    @Override
+    public int getWidth() {
+        return width;
+    }
+
+    @Override
+    public int getHeight() {
+        return height;
+    }
+
+    @Override
+    public String getFilePath() {
+        return filePath;
+    }
+}
diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java
new file mode 100644
index 0000000..540322a
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocationClustering.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.FloatMath;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ReverseGeocoder;
+
+import java.util.ArrayList;
+
+class LocationClustering extends Clustering {
+    @SuppressWarnings("unused")
+    private static final String TAG = "LocationClustering";
+
+    private static final int MIN_GROUPS = 1;
+    private static final int MAX_GROUPS = 20;
+    private static final int MAX_ITERATIONS = 30;
+
+    // If the total distance change is less than this ratio, stop iterating.
+    private static final float STOP_CHANGE_RATIO = 0.01f;
+    private Context mContext;
+    private ArrayList<ArrayList<SmallItem>> mClusters;
+    private ArrayList<String> mNames;
+    private String mNoLocationString;
+    private Handler mHandler;
+
+    private static class Point {
+        public Point(double lat, double lng) {
+            latRad = Math.toRadians(lat);
+            lngRad = Math.toRadians(lng);
+        }
+        public Point() {}
+        public double latRad, lngRad;
+    }
+
+    private static class SmallItem {
+        Path path;
+        double lat, lng;
+    }
+
+    public LocationClustering(Context context) {
+        mContext = context;
+        mNoLocationString = mContext.getResources().getString(R.string.no_location);
+        mHandler = new Handler(Looper.getMainLooper());
+    }
+
+    @Override
+    public void run(MediaSet baseSet) {
+        final int total = baseSet.getTotalMediaItemCount();
+        final SmallItem[] buf = new SmallItem[total];
+        // Separate items to two sets: with or without lat-long.
+        final double[] latLong = new double[2];
+        baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                if (index < 0 || index >= total) return;
+                SmallItem s = new SmallItem();
+                s.path = item.getPath();
+                item.getLatLong(latLong);
+                s.lat = latLong[0];
+                s.lng = latLong[1];
+                buf[index] = s;
+            }
+        });
+
+        final ArrayList<SmallItem> withLatLong = new ArrayList<SmallItem>();
+        final ArrayList<SmallItem> withoutLatLong = new ArrayList<SmallItem>();
+        final ArrayList<Point> points = new ArrayList<Point>();
+        for (int i = 0; i < total; i++) {
+            SmallItem s = buf[i];
+            if (s == null) continue;
+            if (GalleryUtils.isValidLocation(s.lat, s.lng)) {
+                withLatLong.add(s);
+                points.add(new Point(s.lat, s.lng));
+            } else {
+                withoutLatLong.add(s);
+            }
+        }
+
+        ArrayList<ArrayList<SmallItem>> clusters = new ArrayList<ArrayList<SmallItem>>();
+
+        int m = withLatLong.size();
+        if (m > 0) {
+            // cluster the items with lat-long
+            Point[] pointsArray = new Point[m];
+            pointsArray = points.toArray(pointsArray);
+            int[] bestK = new int[1];
+            int[] index = kMeans(pointsArray, bestK);
+
+            for (int i = 0; i < bestK[0]; i++) {
+                clusters.add(new ArrayList<SmallItem>());
+            }
+
+            for (int i = 0; i < m; i++) {
+                clusters.get(index[i]).add(withLatLong.get(i));
+            }
+        }
+
+        ReverseGeocoder geocoder = new ReverseGeocoder(mContext);
+        mNames = new ArrayList<String>();
+        boolean hasUnresolvedAddress = false;
+        mClusters = new ArrayList<ArrayList<SmallItem>>();
+        for (ArrayList<SmallItem> cluster : clusters) {
+            String name = generateName(cluster, geocoder);
+            if (name != null) {
+                mNames.add(name);
+                mClusters.add(cluster);
+            } else {
+                // move cluster-i to no location cluster
+                withoutLatLong.addAll(cluster);
+                hasUnresolvedAddress = true;
+            }
+        }
+
+        if (withoutLatLong.size() > 0) {
+            mNames.add(mNoLocationString);
+            mClusters.add(withoutLatLong);
+        }
+
+        if (hasUnresolvedAddress) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Toast.makeText(mContext, R.string.no_connectivity,
+                            Toast.LENGTH_LONG).show();
+                }
+            });
+        }
+    }
+
+    private static String generateName(ArrayList<SmallItem> items,
+            ReverseGeocoder geocoder) {
+        ReverseGeocoder.SetLatLong set = new ReverseGeocoder.SetLatLong();
+
+        int n = items.size();
+        for (int i = 0; i < n; i++) {
+            SmallItem item = items.get(i);
+            double itemLatitude = item.lat;
+            double itemLongitude = item.lng;
+
+            if (set.mMinLatLatitude > itemLatitude) {
+                set.mMinLatLatitude = itemLatitude;
+                set.mMinLatLongitude = itemLongitude;
+            }
+            if (set.mMaxLatLatitude < itemLatitude) {
+                set.mMaxLatLatitude = itemLatitude;
+                set.mMaxLatLongitude = itemLongitude;
+            }
+            if (set.mMinLonLongitude > itemLongitude) {
+                set.mMinLonLatitude = itemLatitude;
+                set.mMinLonLongitude = itemLongitude;
+            }
+            if (set.mMaxLonLongitude < itemLongitude) {
+                set.mMaxLonLatitude = itemLatitude;
+                set.mMaxLonLongitude = itemLongitude;
+            }
+        }
+
+        return geocoder.computeAddress(set);
+    }
+
+    @Override
+    public int getNumberOfClusters() {
+        return mClusters.size();
+    }
+
+    @Override
+    public ArrayList<Path> getCluster(int index) {
+        ArrayList<SmallItem> items = mClusters.get(index);
+        ArrayList<Path> result = new ArrayList<Path>(items.size());
+        for (int i = 0, n = items.size(); i < n; i++) {
+            result.add(items.get(i).path);
+        }
+        return result;
+    }
+
+    @Override
+    public String getClusterName(int index) {
+        return mNames.get(index);
+    }
+
+    // Input: n points
+    // Output: the best k is stored in bestK[0], and the return value is the
+    // an array which specifies the group that each point belongs (0 to k - 1).
+    private static int[] kMeans(Point points[], int[] bestK) {
+        int n = points.length;
+
+        // min and max number of groups wanted
+        int minK = Math.min(n, MIN_GROUPS);
+        int maxK = Math.min(n, MAX_GROUPS);
+
+        Point[] center = new Point[maxK];  // center of each group.
+        Point[] groupSum = new Point[maxK];  // sum of points in each group.
+        int[] groupCount = new int[maxK];  // number of points in each group.
+        int[] grouping = new int[n]; // The group assignment for each point.
+
+        for (int i = 0; i < maxK; i++) {
+            center[i] = new Point();
+            groupSum[i] = new Point();
+        }
+
+        // The score we want to minimize is:
+        //   (sum of distance from each point to its group center) * sqrt(k).
+        float bestScore = Float.MAX_VALUE;
+        // The best group assignment up to now.
+        int[] bestGrouping = new int[n];
+        // The best K up to now.
+        bestK[0] = 1;
+
+        float lastDistance = 0;
+        float totalDistance = 0;
+
+        for (int k = minK; k <= maxK; k++) {
+            // step 1: (arbitrarily) pick k points as the initial centers.
+            int delta = n / k;
+            for (int i = 0; i < k; i++) {
+                Point p = points[i * delta];
+                center[i].latRad = p.latRad;
+                center[i].lngRad = p.lngRad;
+            }
+
+            for (int iter = 0; iter < MAX_ITERATIONS; iter++) {
+                // step 2: assign each point to the nearest center.
+                for (int i = 0; i < k; i++) {
+                    groupSum[i].latRad = 0;
+                    groupSum[i].lngRad = 0;
+                    groupCount[i] = 0;
+                }
+                totalDistance = 0;
+
+                for (int i = 0; i < n; i++) {
+                    Point p = points[i];
+                    float bestDistance = Float.MAX_VALUE;
+                    int bestIndex = 0;
+                    for (int j = 0; j < k; j++) {
+                        float distance = (float) GalleryUtils.fastDistanceMeters(
+                                p.latRad, p.lngRad, center[j].latRad, center[j].lngRad);
+                        // We may have small non-zero distance introduced by
+                        // floating point calculation, so zero out small
+                        // distances less than 1 meter.
+                        if (distance < 1) {
+                            distance = 0;
+                        }
+                        if (distance < bestDistance) {
+                            bestDistance = distance;
+                            bestIndex = j;
+                        }
+                    }
+                    grouping[i] = bestIndex;
+                    groupCount[bestIndex]++;
+                    groupSum[bestIndex].latRad += p.latRad;
+                    groupSum[bestIndex].lngRad += p.lngRad;
+                    totalDistance += bestDistance;
+                }
+
+                // step 3: calculate new centers
+                for (int i = 0; i < k; i++) {
+                    if (groupCount[i] > 0) {
+                        center[i].latRad = groupSum[i].latRad / groupCount[i];
+                        center[i].lngRad = groupSum[i].lngRad / groupCount[i];
+                    }
+                }
+
+                if (totalDistance == 0 || (Math.abs(lastDistance - totalDistance)
+                        / totalDistance) < STOP_CHANGE_RATIO) {
+                    break;
+                }
+                lastDistance = totalDistance;
+            }
+
+            // step 4: remove empty groups and reassign group number
+            int reassign[] = new int[k];
+            int realK = 0;
+            for (int i = 0; i < k; i++) {
+                if (groupCount[i] > 0) {
+                    reassign[i] = realK++;
+                }
+            }
+
+            // step 5: calculate the final score
+            float score = totalDistance * FloatMath.sqrt(realK);
+
+            if (score < bestScore) {
+                bestScore = score;
+                bestK[0] = realK;
+                for (int i = 0; i < n; i++) {
+                    bestGrouping[i] = reassign[grouping[i]];
+                }
+                if (score == 0) {
+                    break;
+                }
+            }
+        }
+        return bestGrouping;
+    }
+}
diff --git a/src/com/android/gallery3d/data/Log.java b/src/com/android/gallery3d/data/Log.java
new file mode 100644
index 0000000..3384eb6
--- /dev/null
+++ b/src/com/android/gallery3d/data/Log.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+public class Log {
+    public static int v(String tag, String msg) {
+        return android.util.Log.v(tag, msg);
+    }
+    public static int v(String tag, String msg, Throwable tr) {
+        return android.util.Log.v(tag, msg, tr);
+    }
+    public static int d(String tag, String msg) {
+        return android.util.Log.d(tag, msg);
+    }
+    public static int d(String tag, String msg, Throwable tr) {
+        return android.util.Log.d(tag, msg, tr);
+    }
+    public static int i(String tag, String msg) {
+        return android.util.Log.i(tag, msg);
+    }
+    public static int i(String tag, String msg, Throwable tr) {
+        return android.util.Log.i(tag, msg, tr);
+    }
+    public static int w(String tag, String msg) {
+        return android.util.Log.w(tag, msg);
+    }
+    public static int w(String tag, String msg, Throwable tr) {
+        return android.util.Log.w(tag, msg, tr);
+    }
+    public static int w(String tag, Throwable tr) {
+        return android.util.Log.w(tag, tr);
+    }
+    public static int e(String tag, String msg) {
+        return android.util.Log.e(tag, msg);
+    }
+    public static int e(String tag, String msg, Throwable tr) {
+        return android.util.Log.e(tag, msg, tr);
+    }
+}
diff --git a/src/com/android/gallery3d/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java
new file mode 100644
index 0000000..cac524b
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaDetails.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+public class MediaDetails implements Iterable<Entry<Integer, Object>> {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MediaDetails";
+
+    private TreeMap<Integer, Object> mDetails = new TreeMap<Integer, Object>();
+    private HashMap<Integer, Integer> mUnits = new HashMap<Integer, Integer>();
+
+    public static final int INDEX_TITLE = 1;
+    public static final int INDEX_DESCRIPTION = 2;
+    public static final int INDEX_DATETIME = 3;
+    public static final int INDEX_LOCATION = 4;
+    public static final int INDEX_WIDTH = 5;
+    public static final int INDEX_HEIGHT = 6;
+    public static final int INDEX_ORIENTATION = 7;
+    public static final int INDEX_DURATION = 8;
+    public static final int INDEX_MIMETYPE = 9;
+    public static final int INDEX_SIZE = 10;
+
+    // for EXIF
+    public static final int INDEX_MAKE = 100;
+    public static final int INDEX_MODEL = 101;
+    public static final int INDEX_FLASH = 102;
+    public static final int INDEX_FOCAL_LENGTH = 103;
+    public static final int INDEX_WHITE_BALANCE = 104;
+    public static final int INDEX_APERTURE = 105;
+    public static final int INDEX_SHUTTER_SPEED = 106;
+    public static final int INDEX_EXPOSURE_TIME = 107;
+    public static final int INDEX_ISO = 108;
+
+    // Put this last because it may be long.
+    public static final int INDEX_PATH = 200;
+
+    public static class FlashState {
+        private static int FLASH_FIRED_MASK = 1;
+        private static int FLASH_RETURN_MASK = 2 | 4;
+        private static int FLASH_MODE_MASK = 8 | 16;
+        private static int FLASH_FUNCTION_MASK = 32;
+        private static int FLASH_RED_EYE_MASK = 64;
+        private int mState;
+
+        public FlashState(int state) {
+            mState = state;
+        }
+
+        public boolean isFlashFired() {
+            return (mState & FLASH_FIRED_MASK) != 0;
+        }
+    }
+
+    public void addDetail(int index, Object value) {
+        mDetails.put(index, value);
+    }
+
+    public Object getDetail(int index) {
+        return mDetails.get(index);
+    }
+
+    public int size() {
+        return mDetails.size();
+    }
+
+    @Override
+    public Iterator<Entry<Integer, Object>> iterator() {
+        return mDetails.entrySet().iterator();
+    }
+
+    public void setUnit(int index, int unit) {
+        mUnits.put(index, unit);
+    }
+
+    public boolean hasUnit(int index) {
+        return mUnits.containsKey(index);
+    }
+
+    public int getUnit(int index) {
+        return mUnits.get(index);
+    }
+
+    private static void setExifData(MediaDetails details, ExifTag tag,
+            int key) {
+        if (tag != null) {
+            String value = null;
+            int type = tag.getDataType();
+            if (type == ExifTag.TYPE_UNSIGNED_RATIONAL || type == ExifTag.TYPE_RATIONAL) {
+                value = String.valueOf(tag.getValueAsRational(0).toDouble());
+            } else if (type == ExifTag.TYPE_ASCII) {
+                value = tag.getValueAsString();
+            } else {
+                value = String.valueOf(tag.forceGetValueAsLong(0));
+            }
+            if (key == MediaDetails.INDEX_FLASH) {
+                MediaDetails.FlashState state = new MediaDetails.FlashState(
+                        Integer.valueOf(value.toString()));
+                details.addDetail(key, state);
+            } else {
+                details.addDetail(key, value);
+            }
+        }
+    }
+
+    public static void extractExifInfo(MediaDetails details, String filePath) {
+
+        ExifInterface exif = new ExifInterface();
+        try {
+            exif.readExif(filePath);
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "Could not find file to read exif: " + filePath, e);
+        } catch (IOException e) {
+            Log.w(TAG, "Could not read exif from file: " + filePath, e);
+        }
+
+        setExifData(details, exif.getTag(ExifInterface.TAG_FLASH),
+                MediaDetails.INDEX_FLASH);
+        setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_WIDTH),
+                MediaDetails.INDEX_WIDTH);
+        setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_LENGTH),
+                MediaDetails.INDEX_HEIGHT);
+        setExifData(details, exif.getTag(ExifInterface.TAG_MAKE),
+                MediaDetails.INDEX_MAKE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_MODEL),
+                MediaDetails.INDEX_MODEL);
+        setExifData(details, exif.getTag(ExifInterface.TAG_APERTURE_VALUE),
+                MediaDetails.INDEX_APERTURE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_ISO_SPEED_RATINGS),
+                MediaDetails.INDEX_ISO);
+        setExifData(details, exif.getTag(ExifInterface.TAG_WHITE_BALANCE),
+                MediaDetails.INDEX_WHITE_BALANCE);
+        setExifData(details, exif.getTag(ExifInterface.TAG_EXPOSURE_TIME),
+                MediaDetails.INDEX_EXPOSURE_TIME);
+        ExifTag focalTag = exif.getTag(ExifInterface.TAG_FOCAL_LENGTH);
+        if (focalTag != null) {
+            details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH,
+                    focalTag.getValueAsRational(0).toDouble());
+            details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
new file mode 100644
index 0000000..59ea865
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+
+// MediaItem represents an image or a video item.
+public abstract class MediaItem extends MediaObject {
+    // NOTE: These type numbers are stored in the image cache, so it should not
+    // not be changed without resetting the cache.
+    public static final int TYPE_THUMBNAIL = 1;
+    public static final int TYPE_MICROTHUMBNAIL = 2;
+
+    public static final int CACHED_IMAGE_QUALITY = 95;
+
+    public static final int IMAGE_READY = 0;
+    public static final int IMAGE_WAIT = 1;
+    public static final int IMAGE_ERROR = -1;
+
+    public static final String MIME_TYPE_JPEG = "image/jpeg";
+
+    private static final int BYTESBUFFE_POOL_SIZE = 4;
+    private static final int BYTESBUFFER_SIZE = 200 * 1024;
+
+    private static int sMicrothumbnailTargetSize = 200;
+    private static final BytesBufferPool sMicroThumbBufferPool =
+            new BytesBufferPool(BYTESBUFFE_POOL_SIZE, BYTESBUFFER_SIZE);
+
+    private static int sThumbnailTargetSize = 640;
+
+    // TODO: fix default value for latlng and change this.
+    public static final double INVALID_LATLNG = 0f;
+
+    public abstract Job<Bitmap> requestImage(int type);
+    public abstract Job<BitmapRegionDecoder> requestLargeImage();
+
+    public MediaItem(Path path, long version) {
+        super(path, version);
+    }
+
+    public long getDateInMs() {
+        return 0;
+    }
+
+    public String getName() {
+        return null;
+    }
+
+    public void getLatLong(double[] latLong) {
+        latLong[0] = INVALID_LATLNG;
+        latLong[1] = INVALID_LATLNG;
+    }
+
+    public String[] getTags() {
+        return null;
+    }
+
+    public Face[] getFaces() {
+        return null;
+    }
+
+    // The rotation of the full-resolution image. By default, it returns the value of
+    // getRotation().
+    public int getFullImageRotation() {
+        return getRotation();
+    }
+
+    public int getRotation() {
+        return 0;
+    }
+
+    public long getSize() {
+        return 0;
+    }
+
+    public abstract String getMimeType();
+
+    public String getFilePath() {
+        return "";
+    }
+
+    // Returns width and height of the media item.
+    // Returns 0, 0 if the information is not available.
+    public abstract int getWidth();
+    public abstract int getHeight();
+
+    // This is an alternative for requestImage() in PhotoPage. If this
+    // is implemented, you don't need to implement requestImage().
+    public ScreenNail getScreenNail() {
+        return null;
+    }
+
+    public static int getTargetSize(int type) {
+        switch (type) {
+            case TYPE_THUMBNAIL:
+                return sThumbnailTargetSize;
+            case TYPE_MICROTHUMBNAIL:
+                return sMicrothumbnailTargetSize;
+            default:
+                throw new RuntimeException(
+                    "should only request thumb/microthumb from cache");
+        }
+    }
+
+    public static BytesBufferPool getBytesBufferPool() {
+        return sMicroThumbBufferPool;
+    }
+
+    public static void setThumbnailSizes(int size, int microSize) {
+        sThumbnailTargetSize = size;
+        if (sMicrothumbnailTargetSize != microSize) {
+            sMicrothumbnailTargetSize = microSize;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java
new file mode 100644
index 0000000..270d4cf
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaObject.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.net.Uri;
+
+public abstract class MediaObject {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MediaObject";
+    public static final long INVALID_DATA_VERSION = -1;
+
+    // These are the bits returned from getSupportedOperations():
+    public static final int SUPPORT_DELETE = 1 << 0;
+    public static final int SUPPORT_ROTATE = 1 << 1;
+    public static final int SUPPORT_SHARE = 1 << 2;
+    public static final int SUPPORT_CROP = 1 << 3;
+    public static final int SUPPORT_SHOW_ON_MAP = 1 << 4;
+    public static final int SUPPORT_SETAS = 1 << 5;
+    public static final int SUPPORT_FULL_IMAGE = 1 << 6;
+    public static final int SUPPORT_PLAY = 1 << 7;
+    public static final int SUPPORT_CACHE = 1 << 8;
+    public static final int SUPPORT_EDIT = 1 << 9;
+    public static final int SUPPORT_INFO = 1 << 10;
+    public static final int SUPPORT_TRIM = 1 << 11;
+    public static final int SUPPORT_UNLOCK = 1 << 12;
+    public static final int SUPPORT_BACK = 1 << 13;
+    public static final int SUPPORT_ACTION = 1 << 14;
+    public static final int SUPPORT_CAMERA_SHORTCUT = 1 << 15;
+    public static final int SUPPORT_MUTE = 1 << 16;
+    public static final int SUPPORT_ALL = 0xffffffff;
+
+    // These are the bits returned from getMediaType():
+    public static final int MEDIA_TYPE_UNKNOWN = 1;
+    public static final int MEDIA_TYPE_IMAGE = 2;
+    public static final int MEDIA_TYPE_VIDEO = 4;
+    public static final int MEDIA_TYPE_ALL = MEDIA_TYPE_IMAGE | MEDIA_TYPE_VIDEO;
+
+    public static final String MEDIA_TYPE_IMAGE_STRING = "image";
+    public static final String MEDIA_TYPE_VIDEO_STRING = "video";
+    public static final String MEDIA_TYPE_ALL_STRING = "all";
+
+    // These are flags for cache() and return values for getCacheFlag():
+    public static final int CACHE_FLAG_NO = 0;
+    public static final int CACHE_FLAG_SCREENNAIL = 1;
+    public static final int CACHE_FLAG_FULL = 2;
+
+    // These are return values for getCacheStatus():
+    public static final int CACHE_STATUS_NOT_CACHED = 0;
+    public static final int CACHE_STATUS_CACHING = 1;
+    public static final int CACHE_STATUS_CACHED_SCREENNAIL = 2;
+    public static final int CACHE_STATUS_CACHED_FULL = 3;
+
+    private static long sVersionSerial = 0;
+
+    protected long mDataVersion;
+
+    protected final Path mPath;
+
+    public interface PanoramaSupportCallback {
+        void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360);
+    }
+
+    public MediaObject(Path path, long version) {
+        path.setObject(this);
+        mPath = path;
+        mDataVersion = version;
+    }
+
+    public Path getPath() {
+        return mPath;
+    }
+
+    public int getSupportedOperations() {
+        return 0;
+    }
+
+    public void getPanoramaSupport(PanoramaSupportCallback callback) {
+        callback.panoramaInfoAvailable(this, false, false);
+    }
+
+    public void clearCachedPanoramaSupport() {
+    }
+
+    public void delete() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void rotate(int degrees) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Uri getContentUri() {
+        String className = getClass().getName();
+        Log.e(TAG, "Class " + className + "should implement getContentUri.");
+        Log.e(TAG, "The object was created from path: " + getPath());
+        throw new UnsupportedOperationException();
+    }
+
+    public Uri getPlayUri() {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getMediaType() {
+        return MEDIA_TYPE_UNKNOWN;
+    }
+
+    public MediaDetails getDetails() {
+        MediaDetails details = new MediaDetails();
+        return details;
+    }
+
+    public long getDataVersion() {
+        return mDataVersion;
+    }
+
+    public int getCacheFlag() {
+        return CACHE_FLAG_NO;
+    }
+
+    public int getCacheStatus() {
+        throw new UnsupportedOperationException();
+    }
+
+    public long getCacheSize() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void cache(int flag) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static synchronized long nextVersionNumber() {
+        return ++MediaObject.sVersionSerial;
+    }
+
+    public static int getTypeFromString(String s) {
+        if (MEDIA_TYPE_ALL_STRING.equals(s)) return MediaObject.MEDIA_TYPE_ALL;
+        if (MEDIA_TYPE_IMAGE_STRING.equals(s)) return MediaObject.MEDIA_TYPE_IMAGE;
+        if (MEDIA_TYPE_VIDEO_STRING.equals(s)) return MediaObject.MEDIA_TYPE_VIDEO;
+        throw new IllegalArgumentException(s);
+    }
+
+    public static String getTypeString(int type) {
+        switch (type) {
+            case MEDIA_TYPE_IMAGE: return MEDIA_TYPE_IMAGE_STRING;
+            case MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO_STRING;
+            case MEDIA_TYPE_ALL: return MEDIA_TYPE_ALL_STRING;
+        }
+        throw new IllegalArgumentException();
+    }
+}
diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java
new file mode 100644
index 0000000..683aa6b
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSet.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.Future;
+
+import java.util.ArrayList;
+import java.util.WeakHashMap;
+
+// MediaSet is a directory-like data structure.
+// It contains MediaItems and sub-MediaSets.
+//
+// The primary interface are:
+// getMediaItemCount(), getMediaItem() and
+// getSubMediaSetCount(), getSubMediaSet().
+//
+// getTotalMediaItemCount() returns the number of all MediaItems, including
+// those in sub-MediaSets.
+public abstract class MediaSet extends MediaObject {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MediaSet";
+
+    public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500;
+    public static final int INDEX_NOT_FOUND = -1;
+
+    public static final int SYNC_RESULT_SUCCESS = 0;
+    public static final int SYNC_RESULT_CANCELLED = 1;
+    public static final int SYNC_RESULT_ERROR = 2;
+
+    /** Listener to be used with requestSync(SyncListener). */
+    public static interface SyncListener {
+        /**
+         * Called when the sync task completed. Completion may be due to normal termination,
+         * an exception, or cancellation.
+         *
+         * @param mediaSet the MediaSet that's done with sync
+         * @param resultCode one of the SYNC_RESULT_* constants
+         */
+        void onSyncDone(MediaSet mediaSet, int resultCode);
+    }
+
+    public MediaSet(Path path, long version) {
+        super(path, version);
+    }
+
+    public int getMediaItemCount() {
+        return 0;
+    }
+
+    // Returns the media items in the range [start, start + count).
+    //
+    // The number of media items returned may be less than the specified count
+    // if there are not enough media items available. The number of
+    // media items available may not be consistent with the return value of
+    // getMediaItemCount() because the contents of database may have already
+    // changed.
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        return new ArrayList<MediaItem>();
+    }
+
+    public MediaItem getCoverMediaItem() {
+        ArrayList<MediaItem> items = getMediaItem(0, 1);
+        if (items.size() > 0) return items.get(0);
+        for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
+            MediaItem cover = getSubMediaSet(i).getCoverMediaItem();
+            if (cover != null) return cover;
+        }
+        return null;
+    }
+
+    public int getSubMediaSetCount() {
+        return 0;
+    }
+
+    public MediaSet getSubMediaSet(int index) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    public boolean isLeafAlbum() {
+        return false;
+    }
+
+    public boolean isCameraRoll() {
+        return false;
+    }
+
+    /**
+     * Method {@link #reload()} may process the loading task in background, this method tells
+     * its client whether the loading is still in process or not.
+     */
+    public boolean isLoading() {
+        return false;
+    }
+
+    public int getTotalMediaItemCount() {
+        int total = getMediaItemCount();
+        for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
+            total += getSubMediaSet(i).getTotalMediaItemCount();
+        }
+        return total;
+    }
+
+    // TODO: we should have better implementation of sub classes
+    public int getIndexOfItem(Path path, int hint) {
+        // hint < 0 is handled below
+        // first, try to find it around the hint
+        int start = Math.max(0,
+                hint - MEDIAITEM_BATCH_FETCH_COUNT / 2);
+        ArrayList<MediaItem> list = getMediaItem(
+                start, MEDIAITEM_BATCH_FETCH_COUNT);
+        int index = getIndexOf(path, list);
+        if (index != INDEX_NOT_FOUND) return start + index;
+
+        // try to find it globally
+        start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0;
+        list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
+        while (true) {
+            index = getIndexOf(path, list);
+            if (index != INDEX_NOT_FOUND) return start + index;
+            if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND;
+            start += MEDIAITEM_BATCH_FETCH_COUNT;
+            list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
+        }
+    }
+
+    protected int getIndexOf(Path path, ArrayList<MediaItem> list) {
+        for (int i = 0, n = list.size(); i < n; ++i) {
+            // item could be null only in ClusterAlbum
+            MediaObject item = list.get(i);
+            if (item != null && item.mPath == path) return i;
+        }
+        return INDEX_NOT_FOUND;
+    }
+
+    public abstract String getName();
+
+    private WeakHashMap<ContentListener, Object> mListeners =
+            new WeakHashMap<ContentListener, Object>();
+
+    // NOTE: The MediaSet only keeps a weak reference to the listener. The
+    // listener is automatically removed when there is no other reference to
+    // the listener.
+    public void addContentListener(ContentListener listener) {
+        mListeners.put(listener, null);
+    }
+
+    public void removeContentListener(ContentListener listener) {
+        mListeners.remove(listener);
+    }
+
+    // This should be called by subclasses when the content is changed.
+    public void notifyContentChanged() {
+        for (ContentListener listener : mListeners.keySet()) {
+            listener.onContentDirty();
+        }
+    }
+
+    // Reload the content. Return the current data version. reload() should be called
+    // in the same thread as getMediaItem(int, int) and getSubMediaSet(int).
+    public abstract long reload();
+
+    @Override
+    public MediaDetails getDetails() {
+        MediaDetails details = super.getDetails();
+        details.addDetail(MediaDetails.INDEX_TITLE, getName());
+        return details;
+    }
+
+    // Enumerate all media items in this media set (including the ones in sub
+    // media sets), in an efficient order. ItemConsumer.consumer() will be
+    // called for each media item with its index.
+    public void enumerateMediaItems(ItemConsumer consumer) {
+        enumerateMediaItems(consumer, 0);
+    }
+
+    public void enumerateTotalMediaItems(ItemConsumer consumer) {
+        enumerateTotalMediaItems(consumer, 0);
+    }
+
+    public static interface ItemConsumer {
+        void consume(int index, MediaItem item);
+    }
+
+    // The default implementation uses getMediaItem() for enumerateMediaItems().
+    // Subclasses may override this and use more efficient implementations.
+    // Returns the number of items enumerated.
+    protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) {
+        int total = getMediaItemCount();
+        int start = 0;
+        while (start < total) {
+            int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start);
+            ArrayList<MediaItem> items = getMediaItem(start, count);
+            for (int i = 0, n = items.size(); i < n; i++) {
+                MediaItem item = items.get(i);
+                consumer.consume(startIndex + start + i, item);
+            }
+            start += count;
+        }
+        return total;
+    }
+
+    // Recursively enumerate all media items under this set.
+    // Returns the number of items enumerated.
+    protected int enumerateTotalMediaItems(
+            ItemConsumer consumer, int startIndex) {
+        int start = 0;
+        start += enumerateMediaItems(consumer, startIndex);
+        int m = getSubMediaSetCount();
+        for (int i = 0; i < m; i++) {
+            start += getSubMediaSet(i).enumerateTotalMediaItems(
+                    consumer, startIndex + start);
+        }
+        return start;
+    }
+
+    /**
+     * Requests sync on this MediaSet. It returns a Future object that can be used by the caller
+     * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants
+     * defined in this class and can be obtained by Future.get().
+     *
+     * Subclasses should perform sync on a different thread.
+     *
+     * The default implementation here returns a Future stub that does nothing and returns
+     * SYNC_RESULT_SUCCESS by get().
+     */
+    public Future<Integer> requestSync(SyncListener listener) {
+        listener.onSyncDone(this, SYNC_RESULT_SUCCESS);
+        return FUTURE_STUB;
+    }
+
+    private static final Future<Integer> FUTURE_STUB = new Future<Integer>() {
+        @Override
+        public void cancel() {}
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public boolean isDone() {
+            return true;
+        }
+
+        @Override
+        public Integer get() {
+            return SYNC_RESULT_SUCCESS;
+        }
+
+        @Override
+        public void waitDone() {}
+    };
+
+    protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) {
+        return new MultiSetSyncFuture(sets, listener);
+    }
+
+    private class MultiSetSyncFuture implements Future<Integer>, SyncListener {
+        @SuppressWarnings("hiding")
+        private static final String TAG = "Gallery.MultiSetSync";
+
+        private final SyncListener mListener;
+        private final Future<Integer> mFutures[];
+
+        private boolean mIsCancelled = false;
+        private int mResult = -1;
+        private int mPendingCount;
+
+        @SuppressWarnings("unchecked")
+        MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) {
+            mListener = listener;
+            mPendingCount = sets.length;
+            mFutures = new Future[sets.length];
+
+            synchronized (this) {
+                for (int i = 0, n = sets.length; i < n; ++i) {
+                    mFutures[i] = sets[i].requestSync(this);
+                    Log.d(TAG, "  request sync: " + Utils.maskDebugInfo(sets[i].getName()));
+                }
+            }
+        }
+
+        @Override
+        public synchronized void cancel() {
+            if (mIsCancelled) return;
+            mIsCancelled = true;
+            for (Future<Integer> future : mFutures) future.cancel();
+            if (mResult < 0) mResult = SYNC_RESULT_CANCELLED;
+        }
+
+        @Override
+        public synchronized boolean isCancelled() {
+            return mIsCancelled;
+        }
+
+        @Override
+        public synchronized boolean isDone() {
+            return mPendingCount == 0;
+        }
+
+        @Override
+        public synchronized Integer get() {
+            waitDone();
+            return mResult;
+        }
+
+        @Override
+        public synchronized void waitDone() {
+            try {
+                while (!isDone()) wait();
+            } catch (InterruptedException e) {
+                Log.d(TAG, "waitDone() interrupted");
+            }
+        }
+
+        // SyncListener callback
+        @Override
+        public void onSyncDone(MediaSet mediaSet, int resultCode) {
+            SyncListener listener = null;
+            synchronized (this) {
+                if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR;
+                --mPendingCount;
+                if (mPendingCount == 0) {
+                    listener = mListener;
+                    notifyAll();
+                }
+                Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName())
+                        + " #pending=" + mPendingCount);
+            }
+            if (listener != null) listener.onSyncDone(MediaSet.this, mResult);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java
new file mode 100644
index 0000000..9590128
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSource.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.net.Uri;
+
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
+import java.util.ArrayList;
+
+public abstract class MediaSource {
+    private static final String TAG = "MediaSource";
+    private String mPrefix;
+
+    protected MediaSource(String prefix) {
+        mPrefix = prefix;
+    }
+
+    public String getPrefix() {
+        return mPrefix;
+    }
+
+    public Path findPathByUri(Uri uri, String type) {
+        return null;
+    }
+
+    public abstract MediaObject createMediaObject(Path path);
+
+    public void pause() {
+    }
+
+    public void resume() {
+    }
+
+    public Path getDefaultSetOf(Path item) {
+        return null;
+    }
+
+    public long getTotalUsedCacheSize() {
+        return 0;
+    }
+
+    public long getTotalTargetCacheSize() {
+        return 0;
+    }
+
+    public static class PathId {
+        public PathId(Path path, int id) {
+            this.path = path;
+            this.id = id;
+        }
+        public Path path;
+        public int id;
+    }
+
+    // Maps a list of Paths (all belong to this MediaSource) to MediaItems,
+    // and invoke consumer.consume() for each MediaItem with the given id.
+    //
+    // This default implementation uses getMediaObject for each Path. Subclasses
+    // may override this and provide more efficient implementation (like
+    // batching the database query).
+    public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) {
+        int n = list.size();
+        for (int i = 0; i < n; i++) {
+            PathId pid = list.get(i);
+            MediaObject obj;
+            synchronized (DataManager.LOCK) {
+                obj = pid.path.getObject();
+                if (obj == null) {
+                    try {
+                        obj = createMediaObject(pid.path);
+                    } catch (Throwable th) {
+                        Log.w(TAG, "cannot create media object: " + pid.path, th);
+                    }
+                }
+            }
+            if (obj != null) {
+                consumer.consume(pid.id, (MediaItem) obj);
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/MtpClient.java b/src/com/android/gallery3d/data/MtpClient.java
new file mode 100644
index 0000000..737b5b6
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpClient.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.mtp.MtpStorageInfo;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This class helps an application manage a list of connected MTP or PTP devices.
+ * It listens for MTP devices being attached and removed from the USB host bus
+ * and notifies the application when the MTP device list changes.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB_MR1)
+public class MtpClient {
+
+    private static final String TAG = "MtpClient";
+
+    private static final String ACTION_USB_PERMISSION =
+            "android.mtp.MtpClient.action.USB_PERMISSION";
+
+    private final Context mContext;
+    private final UsbManager mUsbManager;
+    private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
+    // mDevices contains all MtpDevices that have been seen by our client,
+    // so we can inform when the device has been detached.
+    // mDevices is also used for synchronization in this class.
+    private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>();
+    // List of MTP devices we should not try to open for which we are currently
+    // asking for permission to open.
+    private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>();
+    // List of MTP devices we should not try to open.
+    // We add devices to this list if the user canceled a permission request or we were
+    // unable to open the device.
+    private final ArrayList<String> mIgnoredDevices = new ArrayList<String>();
+
+    private final PendingIntent mPermissionIntent;
+
+    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            UsbDevice usbDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+            String deviceName = usbDevice.getDeviceName();
+
+            synchronized (mDevices) {
+                MtpDevice mtpDevice = mDevices.get(deviceName);
+
+                if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
+                    if (mtpDevice == null) {
+                        mtpDevice = openDeviceLocked(usbDevice);
+                    }
+                    if (mtpDevice != null) {
+                        for (Listener listener : mListeners) {
+                            listener.deviceAdded(mtpDevice);
+                        }
+                    }
+                } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
+                    if (mtpDevice != null) {
+                        mDevices.remove(deviceName);
+                        mRequestPermissionDevices.remove(deviceName);
+                        mIgnoredDevices.remove(deviceName);
+                        for (Listener listener : mListeners) {
+                            listener.deviceRemoved(mtpDevice);
+                        }
+                    }
+                } else if (ACTION_USB_PERMISSION.equals(action)) {
+                    mRequestPermissionDevices.remove(deviceName);
+                    boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
+                            false);
+                    Log.d(TAG, "ACTION_USB_PERMISSION: " + permission);
+                    if (permission) {
+                        if (mtpDevice == null) {
+                            mtpDevice = openDeviceLocked(usbDevice);
+                        }
+                        if (mtpDevice != null) {
+                            for (Listener listener : mListeners) {
+                                listener.deviceAdded(mtpDevice);
+                            }
+                        }
+                    } else {
+                        // so we don't ask for permission again
+                        mIgnoredDevices.add(deviceName);
+                    }
+                }
+            }
+        }
+    };
+
+    /**
+     * An interface for being notified when MTP or PTP devices are attached
+     * or removed.  In the current implementation, only PTP devices are supported.
+     */
+    public interface Listener {
+        /**
+         * Called when a new device has been added
+         *
+         * @param device the new device that was added
+         */
+        public void deviceAdded(MtpDevice device);
+
+        /**
+         * Called when a new device has been removed
+         *
+         * @param device the device that was removed
+         */
+        public void deviceRemoved(MtpDevice device);
+    }
+
+    /**
+     * Tests to see if a {@link android.hardware.usb.UsbDevice}
+     * supports the PTP protocol (typically used by digital cameras)
+     *
+     * @param device the device to test
+     * @return true if the device is a PTP device.
+     */
+    static public boolean isCamera(UsbDevice device) {
+        int count = device.getInterfaceCount();
+        for (int i = 0; i < count; i++) {
+            UsbInterface intf = device.getInterface(i);
+            if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE &&
+                    intf.getInterfaceSubclass() == 1 &&
+                    intf.getInterfaceProtocol() == 1) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * MtpClient constructor
+     *
+     * @param context the {@link android.content.Context} to use for the MtpClient
+     */
+    public MtpClient(Context context) {
+        mContext = context;
+        mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
+        mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+        filter.addAction(ACTION_USB_PERMISSION);
+        context.registerReceiver(mUsbReceiver, filter);
+    }
+
+    /**
+     * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP
+     * device and return an {@link android.mtp.MtpDevice} for it.
+     *
+     * @param usbDevice the device to open
+     * @return an MtpDevice for the device.
+     */
+    private MtpDevice openDeviceLocked(UsbDevice usbDevice) {
+        String deviceName = usbDevice.getDeviceName();
+
+        // don't try to open devices that we have decided to ignore
+        // or are currently asking permission for
+        if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName)
+                && !mRequestPermissionDevices.contains(deviceName)) {
+            if (!mUsbManager.hasPermission(usbDevice)) {
+                mUsbManager.requestPermission(usbDevice, mPermissionIntent);
+                mRequestPermissionDevices.add(deviceName);
+            } else {
+                UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice);
+                if (connection != null) {
+                    MtpDevice mtpDevice = new MtpDevice(usbDevice);
+                    if (mtpDevice.open(connection)) {
+                        mDevices.put(usbDevice.getDeviceName(), mtpDevice);
+                        return mtpDevice;
+                    } else {
+                        // so we don't try to open it again
+                        mIgnoredDevices.add(deviceName);
+                    }
+                } else {
+                    // so we don't try to open it again
+                    mIgnoredDevices.add(deviceName);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Closes all resources related to the MtpClient object
+     */
+    public void close() {
+        mContext.unregisterReceiver(mUsbReceiver);
+    }
+
+    /**
+     * Registers a {@link com.android.gallery3d.data.MtpClient.Listener} interface to receive
+     * notifications when MTP or PTP devices are added or removed.
+     *
+     * @param listener the listener to register
+     */
+    public void addListener(Listener listener) {
+        synchronized (mDevices) {
+            if (!mListeners.contains(listener)) {
+                mListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Unregisters a {@link com.android.gallery3d.data.MtpClient.Listener} interface.
+     *
+     * @param listener the listener to unregister
+     */
+    public void removeListener(Listener listener) {
+        synchronized (mDevices) {
+            mListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
+     * with the given name.
+     *
+     * @param deviceName the name of the USB device
+     * @return the MtpDevice, or null if it does not exist
+     */
+    public MtpDevice getDevice(String deviceName) {
+        synchronized (mDevices) {
+            return mDevices.get(deviceName);
+        }
+    }
+
+    /**
+     * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
+     * with the given ID.
+     *
+     * @param id the ID of the USB device
+     * @return the MtpDevice, or null if it does not exist
+     */
+    public MtpDevice getDevice(int id) {
+        synchronized (mDevices) {
+            return mDevices.get(UsbDevice.getDeviceName(id));
+        }
+    }
+
+    /**
+     * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}.
+     *
+     * @return the list of MtpDevices
+     */
+    public List<MtpDevice> getDeviceList() {
+        synchronized (mDevices) {
+            // Query the USB manager since devices might have attached
+            // before we added our listener.
+            for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
+                if (mDevices.get(usbDevice.getDeviceName()) == null) {
+                    openDeviceLocked(usbDevice);
+                }
+            }
+
+            return new ArrayList<MtpDevice>(mDevices.values());
+        }
+    }
+
+    /**
+     * Retrieves a list of all {@link android.mtp.MtpStorageInfo}
+     * for the MTP or PTP device with the given USB device name
+     *
+     * @param deviceName the name of the USB device
+     * @return the list of MtpStorageInfo
+     */
+    public List<MtpStorageInfo> getStorageList(String deviceName) {
+        MtpDevice device = getDevice(deviceName);
+        if (device == null) {
+            return null;
+        }
+        int[] storageIds = device.getStorageIds();
+        if (storageIds == null) {
+            return null;
+        }
+
+        int length = storageIds.length;
+        ArrayList<MtpStorageInfo> storageList = new ArrayList<MtpStorageInfo>(length);
+        for (int i = 0; i < length; i++) {
+            MtpStorageInfo info = device.getStorageInfo(storageIds[i]);
+            if (info == null) {
+                Log.w(TAG, "getStorageInfo failed");
+            } else {
+                storageList.add(info);
+            }
+        }
+        return storageList;
+    }
+
+    /**
+     * Retrieves the {@link android.mtp.MtpObjectInfo} for an object on
+     * the MTP or PTP device with the given USB device name with the given
+     * object handle
+     *
+     * @param deviceName the name of the USB device
+     * @param objectHandle handle of the object to query
+     * @return the MtpObjectInfo
+     */
+    public MtpObjectInfo getObjectInfo(String deviceName, int objectHandle) {
+        MtpDevice device = getDevice(deviceName);
+        if (device == null) {
+            return null;
+        }
+        return device.getObjectInfo(objectHandle);
+    }
+
+    /**
+     * Deletes an object on the MTP or PTP device with the given USB device name.
+     *
+     * @param deviceName the name of the USB device
+     * @param objectHandle handle of the object to delete
+     * @return true if the deletion succeeds
+     */
+    public boolean deleteObject(String deviceName, int objectHandle) {
+        MtpDevice device = getDevice(deviceName);
+        if (device == null) {
+            return false;
+        }
+        return device.deleteObject(objectHandle);
+    }
+
+    /**
+     * Retrieves a list of {@link android.mtp.MtpObjectInfo} for all objects
+     * on the MTP or PTP device with the given USB device name and given storage ID
+     * and/or object handle.
+     * If the object handle is zero, then all objects in the root of the storage unit
+     * will be returned. Otherwise, all immediate children of the object will be returned.
+     * If the storage ID is also zero, then all objects on all storage units will be returned.
+     *
+     * @param deviceName the name of the USB device
+     * @param storageId the ID of the storage unit to query, or zero for all
+     * @param objectHandle the handle of the parent object to query, or zero for the storage root
+     * @return the list of MtpObjectInfo
+     */
+    public List<MtpObjectInfo> getObjectList(String deviceName, int storageId, int objectHandle) {
+        MtpDevice device = getDevice(deviceName);
+        if (device == null) {
+            return null;
+        }
+        if (objectHandle == 0) {
+            // all objects in root of storage
+            objectHandle = 0xFFFFFFFF;
+        }
+        int[] handles = device.getObjectHandles(storageId, 0, objectHandle);
+        if (handles == null) {
+            return null;
+        }
+
+        int length = handles.length;
+        ArrayList<MtpObjectInfo> objectList = new ArrayList<MtpObjectInfo>(length);
+        for (int i = 0; i < length; i++) {
+            MtpObjectInfo info = device.getObjectInfo(handles[i]);
+            if (info == null) {
+                Log.w(TAG, "getObjectInfo failed");
+            } else {
+                objectList.add(info);
+            }
+        }
+        return objectList;
+    }
+
+    /**
+     * Returns the data for an object as a byte array.
+     *
+     * @param deviceName the name of the USB device containing the object
+     * @param objectHandle handle of the object to read
+     * @param objectSize the size of the object (this should match
+     *      {@link android.mtp.MtpObjectInfo#getCompressedSize}
+     * @return the object's data, or null if reading fails
+     */
+    public byte[] getObject(String deviceName, int objectHandle, int objectSize) {
+        MtpDevice device = getDevice(deviceName);
+        if (device == null) {
+            return null;
+        }
+        return device.getObject(objectHandle, objectSize);
+    }
+
+    /**
+     * Returns the thumbnail data for an object as a byte array.
+     *
+     * @param deviceName the name of the USB device containing the object
+     * @param objectHandle handle of the object to read
+     * @return the object's thumbnail, or null if reading fails
+     */
+    public byte[] getThumbnail(String deviceName, int objectHandle) {
+        MtpDevice device = getDevice(deviceName);
+        if (device == null) {
+            return null;
+        }
+        return device.getThumbnail(objectHandle);
+    }
+
+    /**
+     * Copies the data for an object to a file in external storage.
+     *
+     * @param deviceName the name of the USB device containing the object
+     * @param objectHandle handle of the object to read
+     * @param destPath path to destination for the file transfer.
+     *      This path should be in the external storage as defined by
+     *      {@link android.os.Environment#getExternalStorageDirectory}
+     * @return true if the file transfer succeeds
+     */
+    public boolean importFile(String deviceName, int objectHandle, String destPath) {
+        MtpDevice device = getDevice(deviceName);
+        if (device == null) {
+            return false;
+        }
+        return device.importFile(objectHandle, destPath);
+    }
+}
diff --git a/src/com/android/gallery3d/data/PanoramaMetadataJob.java b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
new file mode 100644
index 0000000..ab99d6a
--- /dev/null
+++ b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class PanoramaMetadataJob implements Job<PanoramaMetadata> {
+    Context mContext;
+    Uri mUri;
+
+    public PanoramaMetadataJob(Context context, Uri uri) {
+        mContext = context;
+        mUri = uri;
+    }
+
+    @Override
+    public PanoramaMetadata run(JobContext jc) {
+        return LightCycleHelper.getPanoramaMetadata(mContext, mUri);
+    }
+}
diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java
new file mode 100644
index 0000000..fcae65e
--- /dev/null
+++ b/src/com/android/gallery3d/data/Path.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.IdentityCache;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class Path {
+    private static final String TAG = "Path";
+    private static Path sRoot = new Path(null, "ROOT");
+
+    private final Path mParent;
+    private final String mSegment;
+    private WeakReference<MediaObject> mObject;
+    private IdentityCache<String, Path> mChildren;
+
+    private Path(Path parent, String segment) {
+        mParent = parent;
+        mSegment = segment;
+    }
+
+    public Path getChild(String segment) {
+        synchronized (Path.class) {
+            if (mChildren == null) {
+                mChildren = new IdentityCache<String, Path>();
+            } else {
+                Path p = mChildren.get(segment);
+                if (p != null) return p;
+            }
+
+            Path p = new Path(this, segment);
+            mChildren.put(segment, p);
+            return p;
+        }
+    }
+
+    public Path getParent() {
+        synchronized (Path.class) {
+            return mParent;
+        }
+    }
+
+    public Path getChild(int segment) {
+        return getChild(String.valueOf(segment));
+    }
+
+    public Path getChild(long segment) {
+        return getChild(String.valueOf(segment));
+    }
+
+    public void setObject(MediaObject object) {
+        synchronized (Path.class) {
+            Utils.assertTrue(mObject == null || mObject.get() == null);
+            mObject = new WeakReference<MediaObject>(object);
+        }
+    }
+
+    MediaObject getObject() {
+        synchronized (Path.class) {
+            return (mObject == null) ? null : mObject.get();
+        }
+    }
+
+    @Override
+    // TODO: toString() should be more efficient, will fix it later
+    public String toString() {
+        synchronized (Path.class) {
+            StringBuilder sb = new StringBuilder();
+            String[] segments = split();
+            for (int i = 0; i < segments.length; i++) {
+                sb.append("/");
+                sb.append(segments[i]);
+            }
+            return sb.toString();
+        }
+    }
+
+    public boolean equalsIgnoreCase (String p) {
+        String path = toString();
+        return path.equalsIgnoreCase(p);
+    }
+
+    public static Path fromString(String s) {
+        synchronized (Path.class) {
+            String[] segments = split(s);
+            Path current = sRoot;
+            for (int i = 0; i < segments.length; i++) {
+                current = current.getChild(segments[i]);
+            }
+            return current;
+        }
+    }
+
+    public String[] split() {
+        synchronized (Path.class) {
+            int n = 0;
+            for (Path p = this; p != sRoot; p = p.mParent) {
+                n++;
+            }
+            String[] segments = new String[n];
+            int i = n - 1;
+            for (Path p = this; p != sRoot; p = p.mParent) {
+                segments[i--] = p.mSegment;
+            }
+            return segments;
+        }
+    }
+
+    public static String[] split(String s) {
+        int n = s.length();
+        if (n == 0) return new String[0];
+        if (s.charAt(0) != '/') {
+            throw new RuntimeException("malformed path:" + s);
+        }
+        ArrayList<String> segments = new ArrayList<String>();
+        int i = 1;
+        while (i < n) {
+            int brace = 0;
+            int j;
+            for (j = i; j < n; j++) {
+                char c = s.charAt(j);
+                if (c == '{') ++brace;
+                else if (c == '}') --brace;
+                else if (brace == 0 && c == '/') break;
+            }
+            if (brace != 0) {
+                throw new RuntimeException("unbalanced brace in path:" + s);
+            }
+            segments.add(s.substring(i, j));
+            i = j + 1;
+        }
+        String[] result = new String[segments.size()];
+        segments.toArray(result);
+        return result;
+    }
+
+    // Splits a string to an array of strings.
+    // For example, "{foo,bar,baz}" -> {"foo","bar","baz"}.
+    public static String[] splitSequence(String s) {
+        int n = s.length();
+        if (s.charAt(0) != '{' || s.charAt(n-1) != '}') {
+            throw new RuntimeException("bad sequence: " + s);
+        }
+        ArrayList<String> segments = new ArrayList<String>();
+        int i = 1;
+        while (i < n - 1) {
+            int brace = 0;
+            int j;
+            for (j = i; j < n - 1; j++) {
+                char c = s.charAt(j);
+                if (c == '{') ++brace;
+                else if (c == '}') --brace;
+                else if (brace == 0 && c == ',') break;
+            }
+            if (brace != 0) {
+                throw new RuntimeException("unbalanced brace in path:" + s);
+            }
+            segments.add(s.substring(i, j));
+            i = j + 1;
+        }
+        String[] result = new String[segments.size()];
+        segments.toArray(result);
+        return result;
+    }
+
+    public String getPrefix() {
+        if (this == sRoot) return "";
+        return getPrefixPath().mSegment;
+    }
+
+    public Path getPrefixPath() {
+        synchronized (Path.class) {
+            Path current = this;
+            if (current == sRoot) {
+                throw new IllegalStateException();
+            }
+            while (current.mParent != sRoot) {
+                current = current.mParent;
+            }
+            return current;
+        }
+    }
+
+    public String getSuffix() {
+        // We don't need lock because mSegment is final.
+        return mSegment;
+    }
+
+    // Below are for testing/debugging only
+    static void clearAll() {
+        synchronized (Path.class) {
+            sRoot = new Path(null, "");
+        }
+    }
+
+    static void dumpAll() {
+        dumpAll(sRoot, "", "");
+    }
+
+    static void dumpAll(Path p, String prefix1, String prefix2) {
+        synchronized (Path.class) {
+            MediaObject obj = p.getObject();
+            Log.d(TAG, prefix1 + p.mSegment + ":"
+                    + (obj == null ? "null" : obj.getClass().getSimpleName()));
+            if (p.mChildren != null) {
+                ArrayList<String> childrenKeys = p.mChildren.keys();
+                int i = 0, n = childrenKeys.size();
+                for (String key : childrenKeys) {
+                    Path child = p.mChildren.get(key);
+                    if (child == null) {
+                        ++i;
+                        continue;
+                    }
+                    Log.d(TAG, prefix2 + "|");
+                    if (++i < n) {
+                        dumpAll(child, prefix2 + "+-- ", prefix2 + "|   ");
+                    } else {
+                        dumpAll(child, prefix2 + "+-- ", prefix2 + "    ");
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/PathMatcher.java b/src/com/android/gallery3d/data/PathMatcher.java
new file mode 100644
index 0000000..9c6b840
--- /dev/null
+++ b/src/com/android/gallery3d/data/PathMatcher.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class PathMatcher {
+    public static final int NOT_FOUND = -1;
+
+    private ArrayList<String> mVariables = new ArrayList<String>();
+    private Node mRoot = new Node();
+
+    public PathMatcher() {
+        mRoot = new Node();
+    }
+
+    public void add(String pattern, int kind) {
+        String[] segments = Path.split(pattern);
+        Node current = mRoot;
+        for (int i = 0; i < segments.length; i++) {
+            current = current.addChild(segments[i]);
+        }
+        current.setKind(kind);
+    }
+
+    public int match(Path path) {
+        String[] segments = path.split();
+        mVariables.clear();
+        Node current = mRoot;
+        for (int i = 0; i < segments.length; i++) {
+            Node next = current.getChild(segments[i]);
+            if (next == null) {
+                next = current.getChild("*");
+                if (next != null) {
+                    mVariables.add(segments[i]);
+                } else {
+                    return NOT_FOUND;
+                }
+            }
+            current = next;
+        }
+        return current.getKind();
+    }
+
+    public String getVar(int index) {
+        return mVariables.get(index);
+    }
+
+    public int getIntVar(int index) {
+        return Integer.parseInt(mVariables.get(index));
+    }
+
+    public long getLongVar(int index) {
+        return Long.parseLong(mVariables.get(index));
+    }
+
+    private static class Node {
+        private HashMap<String, Node> mMap;
+        private int mKind = NOT_FOUND;
+
+        Node addChild(String segment) {
+            if (mMap == null) {
+                mMap = new HashMap<String, Node>();
+            } else {
+                Node node = mMap.get(segment);
+                if (node != null) return node;
+            }
+
+            Node n = new Node();
+            mMap.put(segment, n);
+            return n;
+        }
+
+        Node getChild(String segment) {
+            if (mMap == null) return null;
+            return mMap.get(segment);
+        }
+
+        void setKind(int kind) {
+            mKind = kind;
+        }
+
+        int getKind() {
+            return mKind;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/SecureAlbum.java b/src/com/android/gallery3d/data/SecureAlbum.java
new file mode 100644
index 0000000..204f848
--- /dev/null
+++ b/src/com/android/gallery3d/data/SecureAlbum.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.MediaColumns;
+import android.provider.MediaStore.Video;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import java.util.ArrayList;
+
+// This class lists all media items added by the client.
+public class SecureAlbum extends MediaSet implements StitchingChangeListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SecureAlbum";
+    private static final String[] PROJECTION = {MediaColumns._ID};
+    private int mMinImageId = Integer.MAX_VALUE; // the smallest id of images
+    private int mMaxImageId = Integer.MIN_VALUE; // the biggest id in images
+    private int mMinVideoId = Integer.MAX_VALUE; // the smallest id of videos
+    private int mMaxVideoId = Integer.MIN_VALUE; // the biggest id of videos
+    // All the media items added by the client.
+    private ArrayList<Path> mAllItems = new ArrayList<Path>();
+    // The types of items in mAllItems. True is video and false is image.
+    private ArrayList<Boolean> mAllItemTypes = new ArrayList<Boolean>();
+    private ArrayList<Path> mExistingItems = new ArrayList<Path>();
+    private Context mContext;
+    private DataManager mDataManager;
+    private static final Uri[] mWatchUris =
+        {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
+    private final ChangeNotifier mNotifier;
+    // A placeholder image in the end of secure album. When it is tapped, it
+    // will take the user to the lock screen.
+    private MediaItem mUnlockItem;
+    private boolean mShowUnlockItem;
+
+    public SecureAlbum(Path path, GalleryApp application, MediaItem unlock) {
+        super(path, nextVersionNumber());
+        mContext = application.getAndroidContext();
+        mDataManager = application.getDataManager();
+        mNotifier = new ChangeNotifier(this, mWatchUris, application);
+        mUnlockItem = unlock;
+        mShowUnlockItem = (!isCameraBucketEmpty(Images.Media.EXTERNAL_CONTENT_URI)
+                || !isCameraBucketEmpty(Video.Media.EXTERNAL_CONTENT_URI));
+    }
+
+    public void addMediaItem(boolean isVideo, int id) {
+        Path pathBase;
+        if (isVideo) {
+            pathBase = LocalVideo.ITEM_PATH;
+            mMinVideoId = Math.min(mMinVideoId, id);
+            mMaxVideoId = Math.max(mMaxVideoId, id);
+        } else {
+            pathBase = LocalImage.ITEM_PATH;
+            mMinImageId = Math.min(mMinImageId, id);
+            mMaxImageId = Math.max(mMaxImageId, id);
+        }
+        Path path = pathBase.getChild(id);
+        if (!mAllItems.contains(path)) {
+            mAllItems.add(path);
+            mAllItemTypes.add(isVideo);
+            mNotifier.fakeChange();
+        }
+    }
+
+    // The sequence is stitching items, local media items, and unlock image.
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        int existingCount = mExistingItems.size();
+        if (start >= existingCount + 1) {
+            return new ArrayList<MediaItem>();
+        }
+
+        // Add paths of requested stitching items.
+        int end = Math.min(start + count, existingCount);
+        ArrayList<Path> subset = new ArrayList<Path>(mExistingItems.subList(start, end));
+
+        // Convert paths to media items.
+        final MediaItem[] buf = new MediaItem[end - start];
+        ItemConsumer consumer = new ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                buf[index] = item;
+            }
+        };
+        mDataManager.mapMediaItems(subset, consumer, 0);
+        ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start);
+        for (int i = 0; i < buf.length; i++) {
+            result.add(buf[i]);
+        }
+        if (mShowUnlockItem) result.add(mUnlockItem);
+        return result;
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return (mExistingItems.size() + (mShowUnlockItem ? 1 : 0));
+    }
+
+    @Override
+    public String getName() {
+        return "secure";
+    }
+
+    @Override
+    public long reload() {
+        if (mNotifier.isDirty()) {
+            mDataVersion = nextVersionNumber();
+            updateExistingItems();
+        }
+        return mDataVersion;
+    }
+
+    private ArrayList<Integer> queryExistingIds(Uri uri, int minId, int maxId) {
+        ArrayList<Integer> ids = new ArrayList<Integer>();
+        if (minId == Integer.MAX_VALUE || maxId == Integer.MIN_VALUE) return ids;
+
+        String[] selectionArgs = {String.valueOf(minId), String.valueOf(maxId)};
+        Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
+                "_id BETWEEN ? AND ?", selectionArgs, null);
+        if (cursor == null) return ids;
+        try {
+            while (cursor.moveToNext()) {
+                ids.add(cursor.getInt(0));
+            }
+        } finally {
+            cursor.close();
+        }
+        return ids;
+    }
+
+    private boolean isCameraBucketEmpty(Uri baseUri) {
+        Uri uri = baseUri.buildUpon()
+                .appendQueryParameter("limit", "1").build();
+        String[] selection = {String.valueOf(MediaSetUtils.CAMERA_BUCKET_ID)};
+        Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
+                "bucket_id = ?", selection, null);
+        if (cursor == null) return true;
+        try {
+            return (cursor.getCount() == 0);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void updateExistingItems() {
+        if (mAllItems.size() == 0) return;
+
+        // Query existing ids.
+        ArrayList<Integer> imageIds = queryExistingIds(
+                Images.Media.EXTERNAL_CONTENT_URI, mMinImageId, mMaxImageId);
+        ArrayList<Integer> videoIds = queryExistingIds(
+                Video.Media.EXTERNAL_CONTENT_URI, mMinVideoId, mMaxVideoId);
+
+        // Construct the existing items list.
+        mExistingItems.clear();
+        for (int i = mAllItems.size() - 1; i >= 0; i--) {
+            Path path = mAllItems.get(i);
+            boolean isVideo = mAllItemTypes.get(i);
+            int id = Integer.parseInt(path.getSuffix());
+            if (isVideo) {
+                if (videoIds.contains(id)) mExistingItems.add(path);
+            } else {
+                if (imageIds.contains(id)) mExistingItems.add(path);
+            }
+        }
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+
+    @Override
+    public void onStitchingQueued(Uri uri) {
+        int id = Integer.parseInt(uri.getLastPathSegment());
+        addMediaItem(false, id);
+    }
+
+    @Override
+    public void onStitchingResult(Uri uri) {
+    }
+
+    @Override
+    public void onStitchingProgress(Uri uri, final int progress) {
+    }
+}
diff --git a/src/com/android/gallery3d/data/SecureSource.java b/src/com/android/gallery3d/data/SecureSource.java
new file mode 100644
index 0000000..6bc8cc2
--- /dev/null
+++ b/src/com/android/gallery3d/data/SecureSource.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+public class SecureSource extends MediaSource {
+    private GalleryApp mApplication;
+    private static PathMatcher mMatcher = new PathMatcher();
+    private static final int SECURE_ALBUM = 0;
+    private static final int SECURE_UNLOCK = 1;
+
+    static {
+        mMatcher.add("/secure/all/*", SECURE_ALBUM);
+        mMatcher.add("/secure/unlock", SECURE_UNLOCK);
+    }
+
+    public SecureSource(GalleryApp context) {
+        super("secure");
+        mApplication = context;
+    }
+
+    public static boolean isSecurePath(String path) {
+        return (SECURE_ALBUM == mMatcher.match(Path.fromString(path)));
+    }
+
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        switch (mMatcher.match(path)) {
+            case SECURE_ALBUM: {
+                DataManager dataManager = mApplication.getDataManager();
+                MediaItem unlock = (MediaItem) dataManager.getMediaObject(
+                        "/secure/unlock");
+                return new SecureAlbum(path, mApplication, unlock);
+            }
+            case SECURE_UNLOCK:
+                return new UnlockImage(path, mApplication);
+            default:
+                throw new RuntimeException("bad path: " + path);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/SingleItemAlbum.java b/src/com/android/gallery3d/data/SingleItemAlbum.java
new file mode 100644
index 0000000..a0093e0
--- /dev/null
+++ b/src/com/android/gallery3d/data/SingleItemAlbum.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import java.util.ArrayList;
+
+public class SingleItemAlbum extends MediaSet {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SingleItemAlbum";
+    private final MediaItem mItem;
+    private final String mName;
+
+    public SingleItemAlbum(Path path, MediaItem item) {
+        super(path, nextVersionNumber());
+        mItem =  item;
+        mName = "SingleItemAlbum("+mItem.getClass().getSimpleName()+")";
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return 1;
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+
+        // If [start, start+count) contains the index 0, return the item.
+        if (start <= 0 && start + count > 0) {
+            result.add(mItem);
+        }
+
+        return result;
+    }
+
+    public MediaItem getItem() {
+        return mItem;
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public long reload() {
+        return mDataVersion;
+    }
+}
diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java
new file mode 100644
index 0000000..b809c84
--- /dev/null
+++ b/src/com/android/gallery3d/data/SizeClustering.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class SizeClustering extends Clustering {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SizeClustering";
+
+    private Context mContext;
+    private ArrayList<Path>[] mClusters;
+    private String[] mNames;
+    private long mMinSizes[];
+
+    private static final long MEGA_BYTES = 1024L*1024;
+    private static final long GIGA_BYTES = 1024L*1024*1024;
+
+    private static final long[] SIZE_LEVELS = {
+        0,
+        1 * MEGA_BYTES,
+        10 * MEGA_BYTES,
+        100 * MEGA_BYTES,
+        1 * GIGA_BYTES,
+        2 * GIGA_BYTES,
+        4 * GIGA_BYTES,
+    };
+
+    public SizeClustering(Context context) {
+        mContext = context;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void run(MediaSet baseSet) {
+        @SuppressWarnings("unchecked")
+        final ArrayList<Path>[] group = new ArrayList[SIZE_LEVELS.length];
+        baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                // Find the cluster this item belongs to.
+                long size = item.getSize();
+                int i;
+                for (i = 0; i < SIZE_LEVELS.length - 1; i++) {
+                    if (size < SIZE_LEVELS[i + 1]) {
+                        break;
+                    }
+                }
+
+                ArrayList<Path> list = group[i];
+                if (list == null) {
+                    list = new ArrayList<Path>();
+                    group[i] = list;
+                }
+                list.add(item.getPath());
+            }
+        });
+
+        int count = 0;
+        for (int i = 0; i < group.length; i++) {
+            if (group[i] != null) {
+                count++;
+            }
+        }
+
+        mClusters = new ArrayList[count];
+        mNames = new String[count];
+        mMinSizes = new long[count];
+
+        Resources res = mContext.getResources();
+        int k = 0;
+        // Go through group in the reverse order, so the group with the largest
+        // size will show first.
+        for (int i = group.length - 1; i >= 0; i--) {
+            if (group[i] == null) continue;
+
+            mClusters[k] = group[i];
+            if (i == 0) {
+                mNames[k] = String.format(
+                        res.getString(R.string.size_below), getSizeString(i + 1));
+            } else if (i == group.length - 1) {
+                mNames[k] = String.format(
+                        res.getString(R.string.size_above), getSizeString(i));
+            } else {
+                String minSize = getSizeString(i);
+                String maxSize = getSizeString(i + 1);
+                mNames[k] = String.format(
+                        res.getString(R.string.size_between), minSize, maxSize);
+            }
+            mMinSizes[k] = SIZE_LEVELS[i];
+            k++;
+        }
+    }
+
+    private String getSizeString(int index) {
+        long bytes = SIZE_LEVELS[index];
+        if (bytes >= GIGA_BYTES) {
+            return (bytes / GIGA_BYTES) + "GB";
+        } else {
+            return (bytes / MEGA_BYTES) + "MB";
+        }
+    }
+
+    @Override
+    public int getNumberOfClusters() {
+        return mClusters.length;
+    }
+
+    @Override
+    public ArrayList<Path> getCluster(int index) {
+        return mClusters[index];
+    }
+
+    @Override
+    public String getClusterName(int index) {
+        return mNames[index];
+    }
+
+    public long getMinSize(int index) {
+        return mMinSizes[index];
+    }
+}
diff --git a/src/com/android/gallery3d/data/SnailAlbum.java b/src/com/android/gallery3d/data/SnailAlbum.java
new file mode 100644
index 0000000..7bce7a6
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailAlbum.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// This is a simple MediaSet which contains only one MediaItem -- a SnailItem.
+public class SnailAlbum extends SingleItemAlbum {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SnailAlbum";
+    private AtomicBoolean mDirty = new AtomicBoolean(false);
+
+    public SnailAlbum(Path path, SnailItem item) {
+        super(path, item);
+    }
+
+    @Override
+    public long reload() {
+        if (mDirty.compareAndSet(true, false)) {
+            ((SnailItem) getItem()).updateVersion();
+            mDataVersion = nextVersionNumber();
+        }
+        return mDataVersion;
+    }
+
+    public void notifyChange() {
+        mDirty.set(true);
+        notifyContentChanged();
+    }
+}
diff --git a/src/com/android/gallery3d/data/SnailItem.java b/src/com/android/gallery3d/data/SnailItem.java
new file mode 100644
index 0000000..3586d2c
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailItem.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+// SnailItem is a MediaItem which can provide a ScreenNail. This is
+// used so we can show an foreign component (like an
+// android.view.View) instead of a Bitmap.
+public class SnailItem extends MediaItem {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SnailItem";
+    private ScreenNail mScreenNail;
+
+    public SnailItem(Path path) {
+        super(path, nextVersionNumber());
+    }
+
+    @Override
+    public Job<Bitmap> requestImage(int type) {
+        // nothing to return
+        return new Job<Bitmap>() {
+            @Override
+            public Bitmap run(JobContext jc) {
+                return null;
+            }
+        };
+    }
+
+    @Override
+    public Job<BitmapRegionDecoder> requestLargeImage() {
+        // nothing to return
+        return new Job<BitmapRegionDecoder>() {
+            @Override
+            public BitmapRegionDecoder run(JobContext jc) {
+                return null;
+            }
+        };
+    }
+
+    // We do not provide requestImage or requestLargeImage, instead we
+    // provide a ScreenNail.
+    @Override
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
+    }
+
+    @Override
+    public String getMimeType() {
+        return "";
+    }
+
+    // Returns width and height of the media item.
+    // Returns 0, 0 if the information is not available.
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  Extra methods for SnailItem
+    //////////////////////////////////////////////////////////////////////////
+
+    public void setScreenNail(ScreenNail screenNail) {
+        mScreenNail = screenNail;
+    }
+
+    public void updateVersion() {
+        mDataVersion = nextVersionNumber();
+    }
+}
diff --git a/src/com/android/gallery3d/data/SnailSource.java b/src/com/android/gallery3d/data/SnailSource.java
new file mode 100644
index 0000000..5c690cc
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailSource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+public class SnailSource extends MediaSource {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SnailSource";
+    private static final int SNAIL_ALBUM = 0;
+    private static final int SNAIL_ITEM = 1;
+
+    private GalleryApp mApplication;
+    private PathMatcher mMatcher;
+    private static int sNextId;
+
+    public SnailSource(GalleryApp application) {
+        super("snail");
+        mApplication = application;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/snail/set/*", SNAIL_ALBUM);
+        mMatcher.add("/snail/item/*", SNAIL_ITEM);
+    }
+
+    // The only path we accept is "/snail/set/id" and "/snail/item/id"
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        DataManager dataManager = mApplication.getDataManager();
+        switch (mMatcher.match(path)) {
+            case SNAIL_ALBUM:
+                String itemPath = "/snail/item/" + mMatcher.getVar(0);
+                SnailItem item =
+                        (SnailItem) dataManager.getMediaObject(itemPath);
+                return new SnailAlbum(path, item);
+            case SNAIL_ITEM: {
+                int id = mMatcher.getIntVar(0);
+                return new SnailItem(path);
+            }
+        }
+        return null;
+    }
+
+    // Registers a new SnailAlbum containing a SnailItem and returns the id of
+    // them. You can obtain the Path of the SnailAlbum and SnailItem associated
+    // with the id by getSetPath and getItemPath().
+    public static synchronized int newId() {
+        return sNextId++;
+    }
+
+    public static Path getSetPath(int id) {
+        return Path.fromString("/snail/set").getChild(id);
+    }
+
+    public static Path getItemPath(int id) {
+        return Path.fromString("/snail/item").getChild(id);
+    }
+}
diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java
new file mode 100644
index 0000000..407ca84
--- /dev/null
+++ b/src/com/android/gallery3d/data/TagClustering.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.Context;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class TagClustering extends Clustering {
+    @SuppressWarnings("unused")
+    private static final String TAG = "TagClustering";
+
+    private ArrayList<ArrayList<Path>> mClusters;
+    private String[] mNames;
+    private String mUntaggedString;
+
+    public TagClustering(Context context) {
+        mUntaggedString = context.getResources().getString(R.string.untagged);
+    }
+
+    @Override
+    public void run(MediaSet baseSet) {
+        final TreeMap<String, ArrayList<Path>> map =
+                new TreeMap<String, ArrayList<Path>>();
+        final ArrayList<Path> untagged = new ArrayList<Path>();
+
+        baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                Path path = item.getPath();
+
+                String[] tags = item.getTags();
+                if (tags == null || tags.length == 0) {
+                    untagged.add(path);
+                    return;
+                }
+                for (int j = 0; j < tags.length; j++) {
+                    String key = tags[j];
+                    ArrayList<Path> list = map.get(key);
+                    if (list == null) {
+                        list = new ArrayList<Path>();
+                        map.put(key, list);
+                    }
+                    list.add(path);
+                }
+            }
+        });
+
+        int m = map.size();
+        mClusters = new ArrayList<ArrayList<Path>>();
+        mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)];
+        int i = 0;
+        for (Map.Entry<String, ArrayList<Path>> entry : map.entrySet()) {
+            mNames[i++] = entry.getKey();
+            mClusters.add(entry.getValue());
+        }
+        if (untagged.size() > 0) {
+            mNames[i++] = mUntaggedString;
+            mClusters.add(untagged);
+        }
+    }
+
+    @Override
+    public int getNumberOfClusters() {
+        return mClusters.size();
+    }
+
+    @Override
+    public ArrayList<Path> getCluster(int index) {
+        return mClusters.get(index);
+    }
+
+    @Override
+    public String getClusterName(int index) {
+        return mNames[index];
+    }
+}
diff --git a/src/com/android/gallery3d/data/TimeClustering.java b/src/com/android/gallery3d/data/TimeClustering.java
new file mode 100644
index 0000000..35cbab1
--- /dev/null
+++ b/src/com/android/gallery3d/data/TimeClustering.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class TimeClustering extends Clustering {
+    @SuppressWarnings("unused")
+    private static final String TAG = "TimeClustering";
+
+    // If 2 items are greater than 25 miles apart, they will be in different
+    // clusters.
+    private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20;
+
+    // Do not want to split based on anything under 1 min.
+    private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L;
+
+    // Disregard a cluster split time of anything over 2 hours.
+    private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L;
+
+    // Try and get around 9 clusters (best-effort for the common case).
+    private static final int NUM_CLUSTERS_TARGETED = 9;
+
+    // Try and merge 2 clusters if they are both smaller than min cluster size.
+    // The min cluster size can range from 8 to 15.
+    private static final int MIN_MIN_CLUSTER_SIZE = 8;
+    private static final int MAX_MIN_CLUSTER_SIZE = 15;
+
+    // Try and split a cluster if it is bigger than max cluster size.
+    // The max cluster size can range from 20 to 50.
+    private static final int MIN_MAX_CLUSTER_SIZE = 20;
+    private static final int MAX_MAX_CLUSTER_SIZE = 50;
+
+    // Initially put 2 items in the same cluster as long as they are within
+    // 3 cluster frequencies of each other.
+    private static int CLUSTER_SPLIT_MULTIPLIER = 3;
+
+    // The minimum change factor in the time between items to consider a
+    // partition.
+    // Example: (Item 3 - Item 2) / (Item 2 - Item 1).
+    private static final int MIN_PARTITION_CHANGE_FACTOR = 2;
+
+    // Make the cluster split time of a large cluster half that of a regular
+    // cluster.
+    private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2;
+
+    private Context mContext;
+    private ArrayList<Cluster> mClusters;
+    private String[] mNames;
+    private Cluster mCurrCluster;
+
+    private long mClusterSplitTime =
+            (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2;
+    private long mLargeClusterSplitTime =
+            mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+    private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2;
+    private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2;
+
+
+    private static final Comparator<SmallItem> sDateComparator =
+            new DateComparator();
+
+    private static class DateComparator implements Comparator<SmallItem> {
+        @Override
+        public int compare(SmallItem item1, SmallItem item2) {
+            return -Utils.compare(item1.dateInMs, item2.dateInMs);
+        }
+    }
+
+    public TimeClustering(Context context) {
+        mContext = context;
+        mClusters = new ArrayList<Cluster>();
+        mCurrCluster = new Cluster();
+    }
+
+    @Override
+    public void run(MediaSet baseSet) {
+        final int total = baseSet.getTotalMediaItemCount();
+        final SmallItem[] buf = new SmallItem[total];
+        final double[] latLng = new double[2];
+
+        baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                if (index < 0 || index >= total) return;
+                SmallItem s = new SmallItem();
+                s.path = item.getPath();
+                s.dateInMs = item.getDateInMs();
+                item.getLatLong(latLng);
+                s.lat = latLng[0];
+                s.lng = latLng[1];
+                buf[index] = s;
+            }
+        });
+
+        ArrayList<SmallItem> items = new ArrayList<SmallItem>(total);
+        for (int i = 0; i < total; i++) {
+            if (buf[i] != null) {
+                items.add(buf[i]);
+            }
+        }
+
+        Collections.sort(items, sDateComparator);
+
+        int n = items.size();
+        long minTime = 0;
+        long maxTime = 0;
+        for (int i = 0; i < n; i++) {
+            long t = items.get(i).dateInMs;
+            if (t == 0) continue;
+            if (minTime == 0) {
+                minTime = maxTime = t;
+            } else {
+                minTime = Math.min(minTime, t);
+                maxTime = Math.max(maxTime, t);
+            }
+        }
+
+        setTimeRange(maxTime - minTime, n);
+
+        for (int i = 0; i < n; i++) {
+            compute(items.get(i));
+        }
+
+        compute(null);
+
+        int m = mClusters.size();
+        mNames = new String[m];
+        for (int i = 0; i < m; i++) {
+            mNames[i] = mClusters.get(i).generateCaption(mContext);
+        }
+    }
+
+    @Override
+    public int getNumberOfClusters() {
+        return mClusters.size();
+    }
+
+    @Override
+    public ArrayList<Path> getCluster(int index) {
+        ArrayList<SmallItem> items = mClusters.get(index).getItems();
+        ArrayList<Path> result = new ArrayList<Path>(items.size());
+        for (int i = 0, n = items.size(); i < n; i++) {
+            result.add(items.get(i).path);
+        }
+        return result;
+    }
+
+    @Override
+    public String getClusterName(int index) {
+        return mNames[index];
+    }
+
+    private void setTimeRange(long timeRange, int numItems) {
+        if (numItems != 0) {
+            int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED;
+            // Heuristic to get min and max cluster size - half and double the
+            // desired items per cluster.
+            mMinClusterSize = meanItemsPerCluster / 2;
+            mMaxClusterSize = meanItemsPerCluster * 2;
+            mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER;
+        }
+        mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS);
+        mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+        mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE);
+        mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE);
+    }
+
+    private void compute(SmallItem currentItem) {
+        if (currentItem != null) {
+            int numClusters = mClusters.size();
+            int numCurrClusterItems = mCurrCluster.size();
+            boolean geographicallySeparateItem = false;
+            boolean itemAddedToCurrentCluster = false;
+
+            // Determine if this item should go in the current cluster or be the
+            // start of a new cluster.
+            if (numCurrClusterItems == 0) {
+                mCurrCluster.addItem(currentItem);
+            } else {
+                SmallItem prevItem = mCurrCluster.getLastItem();
+                if (isGeographicallySeparated(prevItem, currentItem)) {
+                    mClusters.add(mCurrCluster);
+                    geographicallySeparateItem = true;
+                } else if (numCurrClusterItems > mMaxClusterSize) {
+                    splitAndAddCurrentCluster();
+                } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) {
+                    mCurrCluster.addItem(currentItem);
+                    itemAddedToCurrentCluster = true;
+                } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
+                        && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+                    mergeAndAddCurrentCluster();
+                } else {
+                    mClusters.add(mCurrCluster);
+                }
+
+                // Creating a new cluster and adding the current item to it.
+                if (!itemAddedToCurrentCluster) {
+                    mCurrCluster = new Cluster();
+                    if (geographicallySeparateItem) {
+                        mCurrCluster.mGeographicallySeparatedFromPrevCluster = true;
+                    }
+                    mCurrCluster.addItem(currentItem);
+                }
+            }
+        } else {
+            if (mCurrCluster.size() > 0) {
+                int numClusters = mClusters.size();
+                int numCurrClusterItems = mCurrCluster.size();
+
+                // The last cluster may potentially be too big or too small.
+                if (numCurrClusterItems > mMaxClusterSize) {
+                    splitAndAddCurrentCluster();
+                } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
+                        && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+                    mergeAndAddCurrentCluster();
+                } else {
+                    mClusters.add(mCurrCluster);
+                }
+                mCurrCluster = new Cluster();
+            }
+        }
+    }
+
+    private void splitAndAddCurrentCluster() {
+        ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+        int numCurrClusterItems = mCurrCluster.size();
+        int secondPartitionStartIndex = getPartitionIndexForCurrentCluster();
+        if (secondPartitionStartIndex != -1) {
+            Cluster partitionedCluster = new Cluster();
+            for (int j = 0; j < secondPartitionStartIndex; j++) {
+                partitionedCluster.addItem(currClusterItems.get(j));
+            }
+            mClusters.add(partitionedCluster);
+            partitionedCluster = new Cluster();
+            for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) {
+                partitionedCluster.addItem(currClusterItems.get(j));
+            }
+            mClusters.add(partitionedCluster);
+        } else {
+            mClusters.add(mCurrCluster);
+        }
+    }
+
+    private int getPartitionIndexForCurrentCluster() {
+        int partitionIndex = -1;
+        float largestChange = MIN_PARTITION_CHANGE_FACTOR;
+        ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+        int numCurrClusterItems = mCurrCluster.size();
+        int minClusterSize = mMinClusterSize;
+
+        // Could be slightly more efficient here but this code seems cleaner.
+        if (numCurrClusterItems > minClusterSize + 1) {
+            for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) {
+                SmallItem prevItem = currClusterItems.get(i - 1);
+                SmallItem currItem = currClusterItems.get(i);
+                SmallItem nextItem = currClusterItems.get(i + 1);
+
+                long timeNext = nextItem.dateInMs;
+                long timeCurr = currItem.dateInMs;
+                long timePrev = prevItem.dateInMs;
+
+                if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue;
+
+                long diff1 = Math.abs(timeNext - timeCurr);
+                long diff2 = Math.abs(timeCurr - timePrev);
+
+                float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f));
+                if (change > largestChange) {
+                    if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) {
+                        partitionIndex = i;
+                        largestChange = change;
+                    } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) {
+                        partitionIndex = i + 1;
+                        largestChange = change;
+                    }
+                }
+            }
+        }
+        return partitionIndex;
+    }
+
+    private void mergeAndAddCurrentCluster() {
+        int numClusters = mClusters.size();
+        Cluster prevCluster = mClusters.get(numClusters - 1);
+        ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
+        int numCurrClusterItems = mCurrCluster.size();
+        if (prevCluster.size() < mMinClusterSize) {
+            for (int i = 0; i < numCurrClusterItems; i++) {
+                prevCluster.addItem(currClusterItems.get(i));
+            }
+            mClusters.set(numClusters - 1, prevCluster);
+        } else {
+            mClusters.add(mCurrCluster);
+        }
+    }
+
+    // Returns true if a, b are sufficiently geographically separated.
+    private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) {
+        if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng)
+                || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) {
+            return false;
+        }
+
+        double distance = GalleryUtils.fastDistanceMeters(
+            Math.toRadians(itemA.lat),
+            Math.toRadians(itemA.lng),
+            Math.toRadians(itemB.lat),
+            Math.toRadians(itemB.lng));
+        return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES);
+    }
+
+    // Returns the time interval between the two items in milliseconds.
+    private static long timeDistance(SmallItem a, SmallItem b) {
+        return Math.abs(a.dateInMs - b.dateInMs);
+    }
+}
+
+class SmallItem {
+    Path path;
+    long dateInMs;
+    double lat, lng;
+}
+
+class Cluster {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Cluster";
+    private static final String MMDDYY_FORMAT = "MMddyy";
+
+    // This is for TimeClustering only.
+    public boolean mGeographicallySeparatedFromPrevCluster = false;
+
+    private ArrayList<SmallItem> mItems = new ArrayList<SmallItem>();
+
+    public Cluster() {
+    }
+
+    public void addItem(SmallItem item) {
+        mItems.add(item);
+    }
+
+    public int size() {
+        return mItems.size();
+    }
+
+    public SmallItem getLastItem() {
+        int n = mItems.size();
+        return (n == 0) ? null : mItems.get(n - 1);
+    }
+
+    public ArrayList<SmallItem> getItems() {
+        return mItems;
+    }
+
+    public String generateCaption(Context context) {
+        int n = mItems.size();
+        long minTimestamp = 0;
+        long maxTimestamp = 0;
+
+        for (int i = 0; i < n; i++) {
+            long t = mItems.get(i).dateInMs;
+            if (t == 0) continue;
+            if (minTimestamp == 0) {
+                minTimestamp = maxTimestamp = t;
+            } else {
+                minTimestamp = Math.min(minTimestamp, t);
+                maxTimestamp = Math.max(maxTimestamp, t);
+            }
+        }
+        if (minTimestamp == 0) return "";
+
+        String caption;
+        String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp)
+                .toString();
+        String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp)
+                .toString();
+
+        if (minDay.substring(4).equals(maxDay.substring(4))) {
+            // The items are from the same year - show at least as
+            // much granularity as abbrev_all allows.
+            caption = DateUtils.formatDateRange(context, minTimestamp,
+                    maxTimestamp, DateUtils.FORMAT_ABBREV_ALL);
+
+            // Get a more granular date range string if the min and
+            // max timestamp are on the same day and from the
+            // current year.
+            if (minDay.equals(maxDay)) {
+                int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+                // Contains the year only if the date does not
+                // correspond to the current year.
+                String dateRangeWithOptionalYear = DateUtils.formatDateTime(
+                        context, minTimestamp, flags);
+                String dateRangeWithYear = DateUtils.formatDateTime(
+                        context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR);
+                if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) {
+                    // This means both dates are from the same year
+                    // - show the time.
+                    // Not enough room to display the time range.
+                    // Pick the mid-point.
+                    long midTimestamp = (minTimestamp + maxTimestamp) / 2;
+                    caption = DateUtils.formatDateRange(context, midTimestamp,
+                            midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags);
+                }
+            }
+        } else {
+            // The items are not from the same year - only show
+            // month and year.
+            int flags = DateUtils.FORMAT_NO_MONTH_DAY
+                    | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+            caption = DateUtils.formatDateRange(context, minTimestamp,
+                    maxTimestamp, flags);
+        }
+
+        return caption;
+    }
+}
diff --git a/src/com/android/gallery3d/data/UnlockImage.java b/src/com/android/gallery3d/data/UnlockImage.java
new file mode 100644
index 0000000..ed3b485
--- /dev/null
+++ b/src/com/android/gallery3d/data/UnlockImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.data;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+
+public class UnlockImage extends ActionImage {
+    @SuppressWarnings("unused")
+    private static final String TAG = "UnlockImage";
+
+    public UnlockImage(Path path, GalleryApp application) {
+        super(path, application, R.drawable.placeholder_locked);
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        return super.getSupportedOperations() | SUPPORT_UNLOCK;
+    }
+}
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
new file mode 100644
index 0000000..e8875b5
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+
+public class UriImage extends MediaItem {
+    private static final String TAG = "UriImage";
+
+    private static final int STATE_INIT = 0;
+    private static final int STATE_DOWNLOADING = 1;
+    private static final int STATE_DOWNLOADED = 2;
+    private static final int STATE_ERROR = -1;
+
+    private final Uri mUri;
+    private final String mContentType;
+
+    private DownloadCache.Entry mCacheEntry;
+    private ParcelFileDescriptor mFileDescriptor;
+    private int mState = STATE_INIT;
+    private int mWidth;
+    private int mHeight;
+    private int mRotation;
+    private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
+
+    private GalleryApp mApplication;
+
+    public UriImage(GalleryApp application, Path path, Uri uri, String contentType) {
+        super(path, nextVersionNumber());
+        mUri = uri;
+        mApplication = Utils.checkNotNull(application);
+        mContentType = contentType;
+    }
+
+    @Override
+    public Job<Bitmap> requestImage(int type) {
+        return new BitmapJob(type);
+    }
+
+    @Override
+    public Job<BitmapRegionDecoder> requestLargeImage() {
+        return new RegionDecoderJob();
+    }
+
+    private void openFileOrDownloadTempFile(JobContext jc) {
+        int state = openOrDownloadInner(jc);
+        synchronized (this) {
+            mState = state;
+            if (mState != STATE_DOWNLOADED) {
+                if (mFileDescriptor != null) {
+                    Utils.closeSilently(mFileDescriptor);
+                    mFileDescriptor = null;
+                }
+            }
+            notifyAll();
+        }
+    }
+
+    private int openOrDownloadInner(JobContext jc) {
+        String scheme = mUri.getScheme();
+        if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+                || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
+                || ContentResolver.SCHEME_FILE.equals(scheme)) {
+            try {
+                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
+                    InputStream is = mApplication.getContentResolver()
+                            .openInputStream(mUri);
+                    mRotation = Exif.getOrientation(is);
+                    Utils.closeSilently(is);
+                }
+                mFileDescriptor = mApplication.getContentResolver()
+                        .openFileDescriptor(mUri, "r");
+                if (jc.isCancelled()) return STATE_INIT;
+                return STATE_DOWNLOADED;
+            } catch (FileNotFoundException e) {
+                Log.w(TAG, "fail to open: " + mUri, e);
+                return STATE_ERROR;
+            }
+        } else {
+            try {
+                URL url = new URI(mUri.toString()).toURL();
+                mCacheEntry = mApplication.getDownloadCache().download(jc, url);
+                if (jc.isCancelled()) return STATE_INIT;
+                if (mCacheEntry == null) {
+                    Log.w(TAG, "download failed " + url);
+                    return STATE_ERROR;
+                }
+                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
+                    InputStream is = new FileInputStream(mCacheEntry.cacheFile);
+                    mRotation = Exif.getOrientation(is);
+                    Utils.closeSilently(is);
+                }
+                mFileDescriptor = ParcelFileDescriptor.open(
+                        mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY);
+                return STATE_DOWNLOADED;
+            } catch (Throwable t) {
+                Log.w(TAG, "download error", t);
+                return STATE_ERROR;
+            }
+        }
+    }
+
+    private boolean prepareInputFile(JobContext jc) {
+        jc.setCancelListener(new CancelListener() {
+            @Override
+            public void onCancel() {
+                synchronized (this) {
+                    notifyAll();
+                }
+            }
+        });
+
+        while (true) {
+            synchronized (this) {
+                if (jc.isCancelled()) return false;
+                if (mState == STATE_INIT) {
+                    mState = STATE_DOWNLOADING;
+                    // Then leave the synchronized block and continue.
+                } else if (mState == STATE_ERROR) {
+                    return false;
+                } else if (mState == STATE_DOWNLOADED) {
+                    return true;
+                } else /* if (mState == STATE_DOWNLOADING) */ {
+                    try {
+                        wait();
+                    } catch (InterruptedException ex) {
+                        // ignored.
+                    }
+                    continue;
+                }
+            }
+            // This is only reached for STATE_INIT->STATE_DOWNLOADING
+            openFileOrDownloadTempFile(jc);
+        }
+    }
+
+    private class RegionDecoderJob implements Job<BitmapRegionDecoder> {
+        @Override
+        public BitmapRegionDecoder run(JobContext jc) {
+            if (!prepareInputFile(jc)) return null;
+            BitmapRegionDecoder decoder = DecodeUtils.createBitmapRegionDecoder(
+                    jc, mFileDescriptor.getFileDescriptor(), false);
+            mWidth = decoder.getWidth();
+            mHeight = decoder.getHeight();
+            return decoder;
+        }
+    }
+
+    private class BitmapJob implements Job<Bitmap> {
+        private int mType;
+
+        protected BitmapJob(int type) {
+            mType = type;
+        }
+
+        @Override
+        public Bitmap run(JobContext jc) {
+            if (!prepareInputFile(jc)) return null;
+            int targetSize = MediaItem.getTargetSize(mType);
+            Options options = new Options();
+            options.inPreferredConfig = Config.ARGB_8888;
+            Bitmap bitmap = DecodeUtils.decodeThumbnail(jc,
+                    mFileDescriptor.getFileDescriptor(), options, targetSize, mType);
+
+            if (jc.isCancelled() || bitmap == null) {
+                return null;
+            }
+
+            if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+                bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
+            } else {
+                bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
+            }
+            return bitmap;
+        }
+    }
+
+    @Override
+    public int getSupportedOperations() {
+        int supported = SUPPORT_EDIT | SUPPORT_SETAS;
+        if (isSharable()) supported |= SUPPORT_SHARE;
+        if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) {
+            supported |= SUPPORT_FULL_IMAGE;
+        }
+        return supported;
+    }
+
+    @Override
+    public void getPanoramaSupport(PanoramaSupportCallback callback) {
+        mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
+    }
+
+    @Override
+    public void clearCachedPanoramaSupport() {
+        mPanoramaMetadata.clearCachedValues();
+    }
+
+    private boolean isSharable() {
+        // We cannot grant read permission to the receiver since we put
+        // the data URI in EXTRA_STREAM instead of the data part of an intent
+        // And there are issues in MediaUploader and Bluetooth file sender to
+        // share a general image data. So, we only share for local file.
+        return ContentResolver.SCHEME_FILE.equals(mUri.getScheme());
+    }
+
+    @Override
+    public int getMediaType() {
+        return MEDIA_TYPE_IMAGE;
+    }
+
+    @Override
+    public Uri getContentUri() {
+        return mUri;
+    }
+
+    @Override
+    public MediaDetails getDetails() {
+        MediaDetails details = super.getDetails();
+        if (mWidth != 0 && mHeight != 0) {
+            details.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
+            details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
+        }
+        if (mContentType != null) {
+            details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType);
+        }
+        if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) {
+            String filePath = mUri.getPath();
+            details.addDetail(MediaDetails.INDEX_PATH, filePath);
+            MediaDetails.extractExifInfo(details, filePath);
+        }
+        return details;
+    }
+
+    @Override
+    public String getMimeType() {
+        return mContentType;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mFileDescriptor != null) {
+                Utils.closeSilently(mFileDescriptor);
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
+
+    @Override
+    public int getRotation() {
+        return mRotation;
+    }
+}
diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java
new file mode 100644
index 0000000..f66bacd
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriSource.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+
+class UriSource extends MediaSource {
+    @SuppressWarnings("unused")
+    private static final String TAG = "UriSource";
+    private static final String IMAGE_TYPE_PREFIX = "image/";
+    private static final String IMAGE_TYPE_ANY = "image/*";
+    private static final String CHARSET_UTF_8 = "utf-8";
+
+    private GalleryApp mApplication;
+
+    public UriSource(GalleryApp context) {
+        super("uri");
+        mApplication = context;
+    }
+
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        String segment[] = path.split();
+        if (segment.length != 3) {
+            throw new RuntimeException("bad path: " + path);
+        }
+        try {
+            String uri = URLDecoder.decode(segment[1], CHARSET_UTF_8);
+            String type = URLDecoder.decode(segment[2], CHARSET_UTF_8);
+            return new UriImage(mApplication, path, Uri.parse(uri), type);
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private String getMimeType(Uri uri) {
+        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+            String extension =
+                    MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+            String type = MimeTypeMap.getSingleton()
+                    .getMimeTypeFromExtension(extension.toLowerCase());
+            if (type != null) return type;
+        }
+        // Assume the type is image if the type cannot be resolved
+        // This could happen for "http" URI.
+        String type = mApplication.getContentResolver().getType(uri);
+        if (type == null) type = "image/*";
+        return type;
+    }
+
+    @Override
+    public Path findPathByUri(Uri uri, String type) {
+        String mimeType = getMimeType(uri);
+
+        // Try to find a most specific type but it has to be started with "image/"
+        if ((type == null) || (IMAGE_TYPE_ANY.equals(type)
+                && mimeType.startsWith(IMAGE_TYPE_PREFIX))) {
+            type = mimeType;
+        }
+
+        if (type.startsWith(IMAGE_TYPE_PREFIX)) {
+            try {
+                return Path.fromString("/uri/"
+                        + URLEncoder.encode(uri.toString(), CHARSET_UTF_8)
+                        + "/" +URLEncoder.encode(type, CHARSET_UTF_8));
+            } catch (UnsupportedEncodingException e) {
+                throw new AssertionError(e);
+            }
+        }
+        // We have no clues that it is an image
+        return null;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java b/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java
new file mode 100644
index 0000000..bc9342d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+
+public class CenteredLinearLayout extends LinearLayout {
+    private final int mMaxWidth;
+
+    public CenteredLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CenteredLinearLayout);
+        mMaxWidth = a.getDimensionPixelSize(R.styleable.CenteredLinearLayout_max_width, 0);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
+        Resources r = getContext().getResources();
+        float value = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, parentWidth,
+                r.getDisplayMetrics());
+        if (mMaxWidth > 0 && parentWidth > mMaxWidth) {
+            int measureMode = MeasureSpec.getMode(widthMeasureSpec);
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java
new file mode 100644
index 0000000..95abce1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java
@@ -0,0 +1,82 @@
+package com.android.gallery3d.filtershow;
+
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+
+import java.util.HashMap;
+import java.util.Vector;
+
+public class EditorPlaceHolder {
+    private static final String LOGTAG = "EditorPlaceHolder";
+
+    private FilterShowActivity mActivity = null;
+    private FrameLayout mContainer = null;
+    private HashMap<Integer, Editor> mEditors = new HashMap<Integer, Editor>();
+    private Vector<ImageShow> mOldViews = new Vector<ImageShow>();
+
+    public EditorPlaceHolder(FilterShowActivity activity) {
+        mActivity = activity;
+    }
+
+    public void setContainer(FrameLayout container) {
+        mContainer = container;
+    }
+
+    public void addEditor(Editor c) {
+        mEditors.put(c.getID(), c);
+    }
+
+    public boolean contains(int type) {
+        if (mEditors.get(type) != null) {
+            return true;
+        }
+        return false;
+    }
+
+    public Editor showEditor(int type) {
+        Editor editor = mEditors.get(type);
+        if (editor == null) {
+            return null;
+        }
+
+        editor.createEditor(mActivity, mContainer);
+        editor.getImageShow().bindAsImageLoadListener();
+        mContainer.setVisibility(View.VISIBLE);
+        mContainer.removeAllViews();
+        View eview = editor.getTopLevelView();
+        ViewParent parent = eview.getParent();
+
+        if (parent != null && parent instanceof FrameLayout) {
+            ((FrameLayout) parent).removeAllViews();
+        }
+
+        mContainer.addView(eview);
+        hideOldViews();
+        editor.setVisibility(View.VISIBLE);
+        return editor;
+    }
+
+    public void setOldViews(Vector<ImageShow> views) {
+        mOldViews = views;
+    }
+
+    public void hide() {
+        mContainer.setVisibility(View.GONE);
+    }
+
+    public void hideOldViews() {
+        for (View view : mOldViews) {
+            view.setVisibility(View.GONE);
+        }
+    }
+
+    public Editor getEditor(int editorId) {
+        return mEditors.get(editorId);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
new file mode 100644
index 0000000..4700fcc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -0,0 +1,1121 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewPropertyAnimator;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.FrameLayout;
+import android.widget.ShareActionProvider;
+import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.category.Action;
+import com.android.gallery3d.filtershow.category.CategoryAdapter;
+import com.android.gallery3d.filtershow.category.MainPanel;
+import com.android.gallery3d.filtershow.data.UserPresetsManager;
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.editors.EditorChanSat;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.editors.EditorDraw;
+import com.android.gallery3d.filtershow.editors.EditorGrad;
+import com.android.gallery3d.filtershow.editors.EditorManager;
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+import com.android.gallery3d.filtershow.editors.EditorPanel;
+import com.android.gallery3d.filtershow.editors.EditorRedEye;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.history.HistoryItem;
+import com.android.gallery3d.filtershow.history.HistoryManager;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.imageshow.Spline;
+import com.android.gallery3d.filtershow.pipeline.CachingPipeline;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
+import com.android.gallery3d.filtershow.presets.PresetManagementDialog;
+import com.android.gallery3d.filtershow.presets.UserPresetsAdapter;
+import com.android.gallery3d.filtershow.provider.SharedImageProvider;
+import com.android.gallery3d.filtershow.state.StateAdapter;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+import com.android.gallery3d.filtershow.tools.XmpPresets;
+import com.android.gallery3d.filtershow.tools.XmpPresets.XMresults;
+import com.android.gallery3d.filtershow.ui.ExportDialog;
+import com.android.gallery3d.filtershow.ui.FramedTextButton;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.UsageStatistics;
+import com.android.photos.data.GalleryBitmapPool;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Vector;
+
+public class FilterShowActivity extends FragmentActivity implements OnItemClickListener,
+        OnShareTargetSelectedListener {
+
+    private String mAction = "";
+    MasterImage mMasterImage = null;
+
+    private static final long LIMIT_SUPPORTS_HIGHRES = 134217728; // 128Mb
+
+    public static final String TINY_PLANET_ACTION = "com.android.camera.action.TINY_PLANET";
+    public static final String LAUNCH_FULLSCREEN = "launch-fullscreen";
+    private ImageShow mImageShow = null;
+
+    private View mSaveButton = null;
+
+    private EditorPlaceHolder mEditorPlaceHolder = new EditorPlaceHolder(this);
+
+    private static final int SELECT_PICTURE = 1;
+    private static final String LOGTAG = "FilterShowActivity";
+
+    private boolean mShowingTinyPlanet = false;
+    private boolean mShowingImageStatePanel = false;
+
+    private final Vector<ImageShow> mImageViews = new Vector<ImageShow>();
+
+    private ShareActionProvider mShareActionProvider;
+    private File mSharedOutputFile = null;
+
+    private boolean mSharingImage = false;
+
+    private WeakReference<ProgressDialog> mSavingProgressDialog;
+
+    private LoadBitmapTask mLoadBitmapTask;
+
+    private Uri mOriginalImageUri = null;
+    private ImagePreset mOriginalPreset = null;
+
+    private Uri mSelectedImageUri = null;
+
+    private UserPresetsManager mUserPresetsManager = null;
+    private UserPresetsAdapter mUserPresetsAdapter = null;
+    private CategoryAdapter mCategoryLooksAdapter = null;
+    private CategoryAdapter mCategoryBordersAdapter = null;
+    private CategoryAdapter mCategoryGeometryAdapter = null;
+    private CategoryAdapter mCategoryFiltersAdapter = null;
+    private int mCurrentPanel = MainPanel.LOOKS;
+
+    private ProcessingService mBoundService;
+    private boolean mIsBound = false;
+
+    public ProcessingService getProcessingService() {
+        return mBoundService;
+    }
+
+    public boolean isSimpleEditAction() {
+        return !PhotoPage.ACTION_NEXTGEN_EDIT.equalsIgnoreCase(mAction);
+    }
+
+    private ServiceConnection mConnection = new ServiceConnection() {
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            /*
+             * This is called when the connection with the service has been
+             * established, giving us the service object we can use to
+             * interact with the service.  Because we have bound to a explicit
+             * service that we know is running in our own process, we can
+             * cast its IBinder to a concrete class and directly access it.
+             */
+            mBoundService = ((ProcessingService.LocalBinder)service).getService();
+            mBoundService.setFiltershowActivity(FilterShowActivity.this);
+            mBoundService.onStart();
+        }
+
+        public void onServiceDisconnected(ComponentName className) {
+            /*
+             * This is called when the connection with the service has been
+             * unexpectedly disconnected -- that is, its process crashed.
+             * Because it is running in our same process, we should never
+             * see this happen.
+             */
+            mBoundService = null;
+        }
+    };
+
+    void doBindService() {
+        /*
+         * Establish a connection with the service.  We use an explicit
+         * class name because we want a specific service implementation that
+         * we know will be running in our own process (and thus won't be
+         * supporting component replacement by other applications).
+         */
+        bindService(new Intent(FilterShowActivity.this, ProcessingService.class),
+                mConnection, Context.BIND_AUTO_CREATE);
+        mIsBound = true;
+    }
+
+    void doUnbindService() {
+        if (mIsBound) {
+            // Detach our existing connection.
+            unbindService(mConnection);
+            mIsBound = false;
+        }
+    }
+
+    private void setupPipeline() {
+        doBindService();
+        ImageFilter.setActivityForMemoryToasts(this);
+        mUserPresetsManager = new UserPresetsManager(this);
+        mUserPresetsAdapter = new UserPresetsAdapter(this);
+        mCategoryLooksAdapter = new CategoryAdapter(this);
+    }
+
+    public void updateUIAfterServiceStarted() {
+        fillCategories();
+        loadMainPanel();
+        setDefaultPreset();
+        extractXMPData();
+        processIntent();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        boolean onlyUsePortrait = getResources().getBoolean(R.bool.only_use_portrait);
+        if (onlyUsePortrait) {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        }
+        MasterImage.setMaster(mMasterImage);
+
+        clearGalleryBitmapPool();
+        setupPipeline();
+
+        setupMasterImage();
+        setDefaultValues();
+        fillEditors();
+
+        loadXML();
+        UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_EDITOR, "Main");
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                UsageStatistics.CATEGORY_LIFECYCLE, UsageStatistics.LIFECYCLE_START);
+    }
+
+    public boolean isShowingImageStatePanel() {
+        return mShowingImageStatePanel;
+    }
+
+    public void loadMainPanel() {
+        if (findViewById(R.id.main_panel_container) == null) {
+            return;
+        }
+        MainPanel panel = new MainPanel();
+        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+        transaction.replace(R.id.main_panel_container, panel, MainPanel.FRAGMENT_TAG);
+        transaction.commit();
+    }
+
+    public void loadEditorPanel(FilterRepresentation representation,
+                                final Editor currentEditor) {
+        if (representation.getEditorId() == ImageOnlyEditor.ID) {
+            currentEditor.reflectCurrentFilter();
+            return;
+        }
+        final int currentId = currentEditor.getID();
+        Runnable showEditor = new Runnable() {
+            @Override
+            public void run() {
+                EditorPanel panel = new EditorPanel();
+                panel.setEditor(currentId);
+                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+                transaction.remove(getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG));
+                transaction.replace(R.id.main_panel_container, panel, MainPanel.FRAGMENT_TAG);
+                transaction.commit();
+            }
+        };
+        Fragment main = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+        boolean doAnimation = false;
+        if (mShowingImageStatePanel
+                && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+            doAnimation = true;
+        }
+        if (doAnimation && main != null && main instanceof MainPanel) {
+            MainPanel mainPanel = (MainPanel) main;
+            View container = mainPanel.getView().findViewById(R.id.category_panel_container);
+            View bottom = mainPanel.getView().findViewById(R.id.bottom_panel);
+            int panelHeight = container.getHeight() + bottom.getHeight();
+            ViewPropertyAnimator anim = mainPanel.getView().animate();
+            anim.translationY(panelHeight).start();
+            final Handler handler = new Handler();
+            handler.postDelayed(showEditor, anim.getDuration());
+        } else {
+            showEditor.run();
+        }
+    }
+
+    private void loadXML() {
+        setContentView(R.layout.filtershow_activity);
+
+        ActionBar actionBar = getActionBar();
+        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+        actionBar.setCustomView(R.layout.filtershow_actionbar);
+
+        mSaveButton = actionBar.getCustomView();
+        mSaveButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                saveImage();
+            }
+        });
+
+        mImageShow = (ImageShow) findViewById(R.id.imageShow);
+        mImageViews.add(mImageShow);
+
+        setupEditors();
+
+        mEditorPlaceHolder.hide();
+        mImageShow.bindAsImageLoadListener();
+
+        setupStatePanel();
+    }
+
+    public void fillCategories() {
+        fillLooks();
+        loadUserPresets();
+        fillBorders();
+        fillTools();
+        fillEffects();
+    }
+
+    public void setupStatePanel() {
+        MasterImage.getImage().setHistoryManager(mMasterImage.getHistory());
+    }
+
+    private void fillEffects() {
+        FiltersManager filtersManager = FiltersManager.getManager();
+        ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getEffects();
+        mCategoryFiltersAdapter = new CategoryAdapter(this);
+        for (FilterRepresentation representation : filtersRepresentations) {
+            if (representation.getTextId() != 0) {
+                representation.setName(getString(representation.getTextId()));
+            }
+            mCategoryFiltersAdapter.add(new Action(this, representation));
+        }
+    }
+
+    private void fillTools() {
+        FiltersManager filtersManager = FiltersManager.getManager();
+        ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getTools();
+        mCategoryGeometryAdapter = new CategoryAdapter(this);
+        for (FilterRepresentation representation : filtersRepresentations) {
+            mCategoryGeometryAdapter.add(new Action(this, representation));
+        }
+    }
+
+    private void processIntent() {
+        Intent intent = getIntent();
+        if (intent.getBooleanExtra(LAUNCH_FULLSCREEN, false)) {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        }
+
+        mAction = intent.getAction();
+        mSelectedImageUri = intent.getData();
+        Uri loadUri = mSelectedImageUri;
+        if (mOriginalImageUri != null) {
+            loadUri = mOriginalImageUri;
+        }
+        if (loadUri != null) {
+            startLoadBitmap(loadUri);
+        } else {
+            pickImage();
+        }
+    }
+
+    private void setupEditors() {
+        mEditorPlaceHolder.setContainer((FrameLayout) findViewById(R.id.editorContainer));
+        EditorManager.addEditors(mEditorPlaceHolder);
+        mEditorPlaceHolder.setOldViews(mImageViews);
+    }
+
+    private void fillEditors() {
+        mEditorPlaceHolder.addEditor(new EditorChanSat());
+        mEditorPlaceHolder.addEditor(new EditorGrad());
+        mEditorPlaceHolder.addEditor(new EditorDraw());
+        mEditorPlaceHolder.addEditor(new BasicEditor());
+        mEditorPlaceHolder.addEditor(new ImageOnlyEditor());
+        mEditorPlaceHolder.addEditor(new EditorTinyPlanet());
+        mEditorPlaceHolder.addEditor(new EditorRedEye());
+        mEditorPlaceHolder.addEditor(new EditorCrop());
+        mEditorPlaceHolder.addEditor(new EditorMirror());
+        mEditorPlaceHolder.addEditor(new EditorRotate());
+        mEditorPlaceHolder.addEditor(new EditorStraighten());
+    }
+
+    private void setDefaultValues() {
+        Resources res = getResources();
+
+        // TODO: get those values from XML.
+        FramedTextButton.setTextSize((int) getPixelsFromDip(14));
+        FramedTextButton.setTrianglePadding((int) getPixelsFromDip(4));
+        FramedTextButton.setTriangleSize((int) getPixelsFromDip(10));
+
+        Drawable curveHandle = res.getDrawable(R.drawable.camera_crop);
+        int curveHandleSize = (int) res.getDimension(R.dimen.crop_indicator_size);
+        Spline.setCurveHandle(curveHandle, curveHandleSize);
+        Spline.setCurveWidth((int) getPixelsFromDip(3));
+    }
+
+    private void startLoadBitmap(Uri uri) {
+        final View loading = findViewById(R.id.loading);
+        final View imageShow = findViewById(R.id.imageShow);
+        imageShow.setVisibility(View.INVISIBLE);
+        loading.setVisibility(View.VISIBLE);
+        mShowingTinyPlanet = false;
+        mLoadBitmapTask = new LoadBitmapTask();
+        mLoadBitmapTask.execute(uri);
+    }
+
+    private void fillBorders() {
+        FiltersManager filtersManager = FiltersManager.getManager();
+        ArrayList<FilterRepresentation> borders = filtersManager.getBorders();
+
+        for (int i = 0; i < borders.size(); i++) {
+            FilterRepresentation filter = borders.get(i);
+            filter.setName(getString(R.string.borders));
+            if (i == 0) {
+                filter.setName(getString(R.string.none));
+            }
+        }
+
+        mCategoryBordersAdapter = new CategoryAdapter(this);
+        for (FilterRepresentation representation : borders) {
+            if (representation.getTextId() != 0) {
+                representation.setName(getString(representation.getTextId()));
+            }
+            mCategoryBordersAdapter.add(new Action(this, representation, Action.FULL_VIEW));
+        }
+    }
+
+    public UserPresetsAdapter getUserPresetsAdapter() {
+        return mUserPresetsAdapter;
+    }
+
+    public CategoryAdapter getCategoryLooksAdapter() {
+        return mCategoryLooksAdapter;
+    }
+
+    public CategoryAdapter getCategoryBordersAdapter() {
+        return mCategoryBordersAdapter;
+    }
+
+    public CategoryAdapter getCategoryGeometryAdapter() {
+        return mCategoryGeometryAdapter;
+    }
+
+    public CategoryAdapter getCategoryFiltersAdapter() {
+        return mCategoryFiltersAdapter;
+    }
+
+    public void removeFilterRepresentation(FilterRepresentation filterRepresentation) {
+        if (filterRepresentation == null) {
+            return;
+        }
+        ImagePreset oldPreset = MasterImage.getImage().getPreset();
+        ImagePreset copy = new ImagePreset(oldPreset);
+        copy.removeFilter(filterRepresentation);
+        MasterImage.getImage().setPreset(copy, copy.getLastRepresentation(), true);
+        if (MasterImage.getImage().getCurrentFilterRepresentation() == filterRepresentation) {
+            FilterRepresentation lastRepresentation = copy.getLastRepresentation();
+            MasterImage.getImage().setCurrentFilterRepresentation(lastRepresentation);
+        }
+    }
+
+    public void useFilterRepresentation(FilterRepresentation filterRepresentation) {
+        if (filterRepresentation == null) {
+            return;
+        }
+        if (MasterImage.getImage().getCurrentFilterRepresentation() == filterRepresentation) {
+            return;
+        }
+        ImagePreset oldPreset = MasterImage.getImage().getPreset();
+        ImagePreset copy = new ImagePreset(oldPreset);
+        FilterRepresentation representation = copy.getRepresentation(filterRepresentation);
+        if (representation == null) {
+            copy.addFilter(filterRepresentation);
+        } else if (filterRepresentation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+            filterRepresentation = representation;
+        } else {
+            if (filterRepresentation.allowsSingleInstanceOnly()) {
+                // Don't just update the filter representation. Centralize the
+                // logic in the addFilter(), such that we can keep "None" as
+                // null.
+                copy.removeFilter(representation);
+                copy.addFilter(filterRepresentation);
+            }
+        }
+        MasterImage.getImage().setPreset(copy, filterRepresentation, true);
+        MasterImage.getImage().setCurrentFilterRepresentation(filterRepresentation);
+    }
+
+    public void showRepresentation(FilterRepresentation representation) {
+        if (representation == null) {
+            return;
+        }
+
+        useFilterRepresentation(representation);
+
+        // show representation
+        Editor mCurrentEditor = mEditorPlaceHolder.showEditor(representation.getEditorId());
+        loadEditorPanel(representation, mCurrentEditor);
+    }
+
+    public Editor getEditor(int editorID) {
+        return mEditorPlaceHolder.getEditor(editorID);
+    }
+
+    public void setCurrentPanel(int currentPanel) {
+        mCurrentPanel = currentPanel;
+    }
+
+    public int getCurrentPanel() {
+        return mCurrentPanel;
+    }
+
+    public void updateCategories() {
+        ImagePreset preset = mMasterImage.getPreset();
+        mCategoryLooksAdapter.reflectImagePreset(preset);
+        mCategoryBordersAdapter.reflectImagePreset(preset);
+    }
+
+    private class LoadHighresBitmapTask extends AsyncTask<Void, Void, Boolean> {
+        @Override
+        protected Boolean doInBackground(Void... params) {
+            MasterImage master = MasterImage.getImage();
+            Rect originalBounds = master.getOriginalBounds();
+            if (master.supportsHighRes()) {
+                int highresPreviewSize = master.getOriginalBitmapLarge().getWidth() * 2;
+                if (highresPreviewSize > originalBounds.width()) {
+                    highresPreviewSize = originalBounds.width();
+                }
+                Rect bounds = new Rect();
+                Bitmap originalHires = ImageLoader.loadOrientedConstrainedBitmap(master.getUri(),
+                        master.getActivity(), highresPreviewSize,
+                        master.getOrientation(), bounds);
+                master.setOriginalBounds(bounds);
+                master.setOriginalBitmapHighres(originalHires);
+                mBoundService.setOriginalBitmapHighres(originalHires);
+                master.warnListeners();
+            }
+            return true;
+        }
+
+        @Override
+        protected void onPostExecute(Boolean result) {
+            Bitmap highresBitmap = MasterImage.getImage().getOriginalBitmapHighres();
+            if (highresBitmap != null) {
+                float highResPreviewScale = (float) highresBitmap.getWidth()
+                        / (float) MasterImage.getImage().getOriginalBounds().width();
+                mBoundService.setHighresPreviewScaleFactor(highResPreviewScale);
+            }
+        }
+    }
+
+    private class LoadBitmapTask extends AsyncTask<Uri, Boolean, Boolean> {
+        int mBitmapSize;
+
+        public LoadBitmapTask() {
+            mBitmapSize = getScreenImageSize();
+        }
+
+        @Override
+        protected Boolean doInBackground(Uri... params) {
+            if (!MasterImage.getImage().loadBitmap(params[0], mBitmapSize)) {
+                return false;
+            }
+            publishProgress(ImageLoader.queryLightCycle360(MasterImage.getImage().getActivity()));
+            return true;
+        }
+
+        @Override
+        protected void onProgressUpdate(Boolean... values) {
+            super.onProgressUpdate(values);
+            if (isCancelled()) {
+                return;
+            }
+            if (values[0]) {
+                mShowingTinyPlanet = true;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Boolean result) {
+            MasterImage.setMaster(mMasterImage);
+            if (isCancelled()) {
+                return;
+            }
+
+            if (!result) {
+                cannotLoadImage();
+            }
+
+            if (null == CachingPipeline.getRenderScriptContext()){
+                Log.v(LOGTAG,"RenderScript context destroyed during load");
+                return;
+            }
+            final View loading = findViewById(R.id.loading);
+            loading.setVisibility(View.GONE);
+            final View imageShow = findViewById(R.id.imageShow);
+            imageShow.setVisibility(View.VISIBLE);
+
+            Bitmap largeBitmap = MasterImage.getImage().getOriginalBitmapLarge();
+            mBoundService.setOriginalBitmap(largeBitmap);
+
+            float previewScale = (float) largeBitmap.getWidth()
+                    / (float) MasterImage.getImage().getOriginalBounds().width();
+            mBoundService.setPreviewScaleFactor(previewScale);
+            if (!mShowingTinyPlanet) {
+                mCategoryFiltersAdapter.removeTinyPlanet();
+            }
+            mCategoryLooksAdapter.imageLoaded();
+            mCategoryBordersAdapter.imageLoaded();
+            mCategoryGeometryAdapter.imageLoaded();
+            mCategoryFiltersAdapter.imageLoaded();
+            mLoadBitmapTask = null;
+
+            if (mOriginalPreset != null) {
+                MasterImage.getImage().setLoadedPreset(mOriginalPreset);
+                MasterImage.getImage().setPreset(mOriginalPreset,
+                        mOriginalPreset.getLastRepresentation(), true);
+                mOriginalPreset = null;
+            }
+
+            if (mAction == TINY_PLANET_ACTION) {
+                showRepresentation(mCategoryFiltersAdapter.getTinyPlanet());
+            }
+            LoadHighresBitmapTask highresLoad = new LoadHighresBitmapTask();
+            highresLoad.execute();
+            super.onPostExecute(result);
+        }
+
+    }
+
+    private void clearGalleryBitmapPool() {
+        (new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                // Free memory held in Gallery's Bitmap pool.  May be O(n) for n bitmaps.
+                GalleryBitmapPool.getInstance().clear();
+                return null;
+            }
+        }).execute();
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mLoadBitmapTask != null) {
+            mLoadBitmapTask.cancel(false);
+        }
+        mUserPresetsManager.close();
+        doUnbindService();
+        super.onDestroy();
+    }
+
+    // TODO: find a more robust way of handling image size selection
+    // for high screen densities.
+    private int getScreenImageSize() {
+        DisplayMetrics outMetrics = new DisplayMetrics();
+        getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
+        return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
+    }
+
+    private void showSavingProgress(String albumName) {
+        ProgressDialog progress;
+        if (mSavingProgressDialog != null) {
+            progress = mSavingProgressDialog.get();
+            if (progress != null) {
+                progress.show();
+                return;
+            }
+        }
+        // TODO: Allow cancellation of the saving process
+        String progressText;
+        if (albumName == null) {
+            progressText = getString(R.string.saving_image);
+        } else {
+            progressText = getString(R.string.filtershow_saving_image, albumName);
+        }
+        progress = ProgressDialog.show(this, "", progressText, true, false);
+        mSavingProgressDialog = new WeakReference<ProgressDialog>(progress);
+    }
+
+    private void hideSavingProgress() {
+        if (mSavingProgressDialog != null) {
+            ProgressDialog progress = mSavingProgressDialog.get();
+            if (progress != null)
+                progress.dismiss();
+        }
+    }
+
+    public void completeSaveImage(Uri saveUri) {
+        if (mSharingImage && mSharedOutputFile != null) {
+            // Image saved, we unblock the content provider
+            Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI,
+                    Uri.encode(mSharedOutputFile.getAbsolutePath()));
+            ContentValues values = new ContentValues();
+            values.put(SharedImageProvider.PREPARE, false);
+            getContentResolver().insert(uri, values);
+        }
+        setResult(RESULT_OK, new Intent().setData(saveUri));
+        hideSavingProgress();
+        finish();
+    }
+
+    @Override
+    public boolean onShareTargetSelected(ShareActionProvider arg0, Intent arg1) {
+        // First, let's tell the SharedImageProvider that it will need to wait
+        // for the image
+        Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI,
+                Uri.encode(mSharedOutputFile.getAbsolutePath()));
+        ContentValues values = new ContentValues();
+        values.put(SharedImageProvider.PREPARE, true);
+        getContentResolver().insert(uri, values);
+        mSharingImage = true;
+
+        // Process and save the image in the background.
+        showSavingProgress(null);
+        mImageShow.saveImage(this, mSharedOutputFile);
+        return true;
+    }
+
+    private Intent getDefaultShareIntent() {
+        Intent intent = new Intent(Intent.ACTION_SEND);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        intent.setType(SharedImageProvider.MIME_TYPE);
+        mSharedOutputFile = SaveImage.getNewFile(this, MasterImage.getImage().getUri());
+        Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI,
+                Uri.encode(mSharedOutputFile.getAbsolutePath()));
+        intent.putExtra(Intent.EXTRA_STREAM, uri);
+        return intent;
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.filtershow_activity_menu, menu);
+        MenuItem showState = menu.findItem(R.id.showImageStateButton);
+        if (mShowingImageStatePanel) {
+            showState.setTitle(R.string.hide_imagestate_panel);
+        } else {
+            showState.setTitle(R.string.show_imagestate_panel);
+        }
+        mShareActionProvider = (ShareActionProvider) menu.findItem(R.id.menu_share)
+                .getActionProvider();
+        mShareActionProvider.setShareIntent(getDefaultShareIntent());
+        mShareActionProvider.setOnShareTargetSelectedListener(this);
+
+        MenuItem undoItem = menu.findItem(R.id.undoButton);
+        MenuItem redoItem = menu.findItem(R.id.redoButton);
+        MenuItem resetItem = menu.findItem(R.id.resetHistoryButton);
+        mMasterImage.getHistory().setMenuItems(undoItem, redoItem, resetItem);
+        return true;
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (mShareActionProvider != null) {
+            mShareActionProvider.setOnShareTargetSelectedListener(null);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (mShareActionProvider != null) {
+            mShareActionProvider.setOnShareTargetSelectedListener(this);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.undoButton: {
+                HistoryManager adapter = mMasterImage.getHistory();
+                int position = adapter.undo();
+                mMasterImage.onHistoryItemClick(position);
+                backToMain();
+                invalidateViews();
+                UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                        UsageStatistics.CATEGORY_BUTTON_PRESS, "Undo");
+                return true;
+            }
+            case R.id.redoButton: {
+                HistoryManager adapter = mMasterImage.getHistory();
+                int position = adapter.redo();
+                mMasterImage.onHistoryItemClick(position);
+                invalidateViews();
+                UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                        UsageStatistics.CATEGORY_BUTTON_PRESS, "Redo");
+                return true;
+            }
+            case R.id.resetHistoryButton: {
+                resetHistory();
+                UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                        UsageStatistics.CATEGORY_BUTTON_PRESS, "ResetHistory");
+                return true;
+            }
+            case R.id.showImageStateButton: {
+                toggleImageStatePanel();
+                UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                        UsageStatistics.CATEGORY_BUTTON_PRESS,
+                        mShowingImageStatePanel ? "ShowPanel" : "HidePanel");
+                return true;
+            }
+            case R.id.exportFlattenButton: {
+                showExportOptionsDialog();
+                return true;
+            }
+            case android.R.id.home: {
+                saveImage();
+                return true;
+            }
+            case R.id.manageUserPresets: {
+                manageUserPresets();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void manageUserPresets() {
+        DialogFragment dialog = new PresetManagementDialog();
+        dialog.show(getSupportFragmentManager(), "NoticeDialogFragment");
+    }
+
+    private void showExportOptionsDialog() {
+        DialogFragment dialog = new ExportDialog();
+        dialog.show(getSupportFragmentManager(), "ExportDialogFragment");
+    }
+
+    public void updateUserPresetsFromAdapter(UserPresetsAdapter adapter) {
+        ArrayList<FilterUserPresetRepresentation> representations =
+                adapter.getDeletedRepresentations();
+        for (FilterUserPresetRepresentation representation : representations) {
+            deletePreset(representation.getId());
+        }
+        ArrayList<FilterUserPresetRepresentation> changedRepresentations =
+                adapter.getChangedRepresentations();
+        for (FilterUserPresetRepresentation representation : changedRepresentations) {
+            updatePreset(representation);
+        }
+        adapter.clearDeletedRepresentations();
+        adapter.clearChangedRepresentations();
+        loadUserPresets();
+    }
+
+    public void loadUserPresets() {
+        mUserPresetsManager.load();
+    }
+
+    public void updateUserPresetsFromManager() {
+        ArrayList<FilterUserPresetRepresentation> presets = mUserPresetsManager.getRepresentations();
+        if (presets == null) {
+            return;
+        }
+        if (mCategoryLooksAdapter != null) {
+            fillLooks();
+        }
+        mUserPresetsAdapter.clear();
+        for (int i = 0; i < presets.size(); i++) {
+            FilterUserPresetRepresentation representation = presets.get(i);
+            mCategoryLooksAdapter.add(
+                    new Action(this, representation, Action.FULL_VIEW));
+            mUserPresetsAdapter.add(new Action(this, representation, Action.FULL_VIEW));
+        }
+        mCategoryLooksAdapter.notifyDataSetInvalidated();
+
+    }
+
+    public void saveCurrentImagePreset() {
+        mUserPresetsManager.save(MasterImage.getImage().getPreset());
+    }
+
+    private void deletePreset(int id) {
+        mUserPresetsManager.delete(id);
+    }
+
+    private void updatePreset(FilterUserPresetRepresentation representation) {
+        mUserPresetsManager.update(representation);
+    }
+
+    public void enableSave(boolean enable) {
+        if (mSaveButton != null) {
+            mSaveButton.setEnabled(enable);
+        }
+    }
+
+    private void fillLooks() {
+        FiltersManager filtersManager = FiltersManager.getManager();
+        ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getLooks();
+
+        mCategoryLooksAdapter.clear();
+        int verticalItemHeight = (int) getResources().getDimension(R.dimen.action_item_height);
+        mCategoryLooksAdapter.setItemHeight(verticalItemHeight);
+        for (FilterRepresentation representation : filtersRepresentations) {
+            mCategoryLooksAdapter.add(new Action(this, representation, Action.FULL_VIEW));
+        }
+    }
+
+    public void setDefaultPreset() {
+        // Default preset (original)
+        ImagePreset preset = new ImagePreset(); // empty
+        mMasterImage.setPreset(preset, preset.getLastRepresentation(), true);
+    }
+
+    // //////////////////////////////////////////////////////////////////////////////
+    // Some utility functions
+    // TODO: finish the cleanup.
+
+    public void invalidateViews() {
+        for (ImageShow views : mImageViews) {
+            views.updateImage();
+        }
+    }
+
+    public void hideImageViews() {
+        for (View view : mImageViews) {
+            view.setVisibility(View.GONE);
+        }
+        mEditorPlaceHolder.hide();
+    }
+
+    // //////////////////////////////////////////////////////////////////////////////
+    // imageState panel...
+
+    public void toggleImageStatePanel() {
+        invalidateOptionsMenu();
+        mShowingImageStatePanel = !mShowingImageStatePanel;
+        Fragment panel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+        if (panel != null) {
+            if (panel instanceof EditorPanel) {
+                EditorPanel editorPanel = (EditorPanel) panel;
+                editorPanel.showImageStatePanel(mShowingImageStatePanel);
+            } else if (panel instanceof MainPanel) {
+                MainPanel mainPanel = (MainPanel) panel;
+                mainPanel.showImageStatePanel(mShowingImageStatePanel);
+            }
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig)
+    {
+        super.onConfigurationChanged(newConfig);
+        setDefaultValues();
+        loadXML();
+        fillCategories();
+        loadMainPanel();
+
+        // mLoadBitmapTask==null implies you have looked at the intent
+        if (!mShowingTinyPlanet && (mLoadBitmapTask == null)) {
+            mCategoryFiltersAdapter.removeTinyPlanet();
+        }
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.GONE);
+    }
+
+    public void setupMasterImage() {
+
+        HistoryManager historyManager = new HistoryManager();
+        StateAdapter imageStateAdapter = new StateAdapter(this, 0);
+        MasterImage.reset();
+        mMasterImage = MasterImage.getImage();
+        mMasterImage.setHistoryManager(historyManager);
+        mMasterImage.setStateAdapter(imageStateAdapter);
+        mMasterImage.setActivity(this);
+
+        if (Runtime.getRuntime().maxMemory() > LIMIT_SUPPORTS_HIGHRES) {
+            mMasterImage.setSupportsHighRes(true);
+        } else {
+            mMasterImage.setSupportsHighRes(false);
+        }
+    }
+
+    void resetHistory() {
+        HistoryManager adapter = mMasterImage.getHistory();
+        adapter.reset();
+        HistoryItem historyItem = adapter.getItem(0);
+        ImagePreset original = new ImagePreset(historyItem.getImagePreset());
+        mMasterImage.setPreset(original, historyItem.getFilterRepresentation(), true);
+        invalidateViews();
+        backToMain();
+    }
+
+    public void showDefaultImageView() {
+        mEditorPlaceHolder.hide();
+        mImageShow.setVisibility(View.VISIBLE);
+        MasterImage.getImage().setCurrentFilter(null);
+        MasterImage.getImage().setCurrentFilterRepresentation(null);
+    }
+
+    public void backToMain() {
+        Fragment currentPanel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+        if (currentPanel instanceof MainPanel) {
+            return;
+        }
+        loadMainPanel();
+        showDefaultImageView();
+    }
+
+    @Override
+    public void onBackPressed() {
+        Fragment currentPanel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+        if (currentPanel instanceof MainPanel) {
+            if (!mImageShow.hasModifications()) {
+                done();
+            } else {
+                AlertDialog.Builder builder = new AlertDialog.Builder(this);
+                builder.setMessage(R.string.unsaved).setTitle(R.string.save_before_exit);
+                builder.setPositiveButton(R.string.save_and_exit, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int id) {
+                        saveImage();
+                    }
+                });
+                builder.setNegativeButton(R.string.exit, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int id) {
+                        done();
+                    }
+                });
+                builder.show();
+            }
+        } else {
+            backToMain();
+        }
+    }
+
+    public void cannotLoadImage() {
+        Toast.makeText(this, R.string.cannot_load_image, Toast.LENGTH_SHORT).show();
+        finish();
+    }
+
+    // //////////////////////////////////////////////////////////////////////////////
+
+    public float getPixelsFromDip(float value) {
+        Resources r = getResources();
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
+                r.getDisplayMetrics());
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position,
+            long id) {
+        mMasterImage.onHistoryItemClick(position);
+        invalidateViews();
+    }
+
+    public void pickImage() {
+        Intent intent = new Intent();
+        intent.setType("image/*");
+        intent.setAction(Intent.ACTION_GET_CONTENT);
+        startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
+                SELECT_PICTURE);
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode == RESULT_OK) {
+            if (requestCode == SELECT_PICTURE) {
+                Uri selectedImageUri = data.getData();
+                startLoadBitmap(selectedImageUri);
+            }
+        }
+    }
+
+
+    public void saveImage() {
+        if (mImageShow.hasModifications()) {
+            // Get the name of the album, to which the image will be saved
+            File saveDir = SaveImage.getFinalSaveDirectory(this, mSelectedImageUri);
+            int bucketId = GalleryUtils.getBucketId(saveDir.getPath());
+            String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null);
+            showSavingProgress(albumName);
+            mImageShow.saveImage(this, null);
+        } else {
+            done();
+        }
+    }
+
+
+    public void done() {
+        hideSavingProgress();
+        if (mLoadBitmapTask != null) {
+            mLoadBitmapTask.cancel(false);
+        }
+        finish();
+    }
+
+    private void extractXMPData() {
+        XMresults res = XmpPresets.extractXMPData(
+                getBaseContext(), mMasterImage, getIntent().getData());
+        if (res == null)
+            return;
+
+        mOriginalImageUri = res.originalimage;
+        mOriginalPreset = res.preset;
+    }
+
+    public Uri getSelectedImageUri() {
+        return mSelectedImageUri;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
new file mode 100644
index 0000000..b6c72fd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.cache;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public final class ImageLoader {
+
+    private static final String LOGTAG = "ImageLoader";
+
+    public static final String JPEG_MIME_TYPE = "image/jpeg";
+    public static final int DEFAULT_COMPRESS_QUALITY = 95;
+
+    public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
+    public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
+    public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
+    public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
+    public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
+    public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
+    public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
+    public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
+
+    private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
+
+    private ImageLoader() {}
+
+    /**
+     * Returns the Mime type for a Url.  Safe to use with Urls that do not
+     * come from Gallery's content provider.
+     */
+    public static String getMimeType(Uri src) {
+        String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString());
+        String ret = null;
+        if (postfix != null) {
+            ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix);
+        }
+        return ret;
+    }
+
+    /**
+     * Returns the image's orientation flag.  Defaults to ORI_NORMAL if no valid
+     * orientation was found.
+     */
+    public static int getMetadataOrientation(Context context, Uri uri) {
+        if (uri == null || context == null) {
+            throw new IllegalArgumentException("bad argument to getOrientation");
+        }
+
+        // First try to find orientation data in Gallery's ContentProvider.
+        Cursor cursor = null;
+        try {
+            cursor = context.getContentResolver().query(uri,
+                    new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
+                    null, null, null);
+            if (cursor != null && cursor.moveToNext()) {
+                int ori = cursor.getInt(0);
+                switch (ori) {
+                    case 90:
+                        return ORI_ROTATE_90;
+                    case 270:
+                        return ORI_ROTATE_270;
+                    case 180:
+                        return ORI_ROTATE_180;
+                    default:
+                        return ORI_NORMAL;
+                }
+            }
+        } catch (SQLiteException e) {
+            // Do nothing
+        } catch (IllegalArgumentException e) {
+            // Do nothing
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+
+        // Fall back to checking EXIF tags in file.
+        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+            String mimeType = getMimeType(uri);
+            if (!JPEG_MIME_TYPE.equals(mimeType)) {
+                return ORI_NORMAL;
+            }
+            String path = uri.getPath();
+            ExifInterface exif = new ExifInterface();
+            try {
+                exif.readExif(path);
+                Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+                if (tagval != null) {
+                    int orientation = tagval;
+                    switch(orientation) {
+                        case ORI_NORMAL:
+                        case ORI_ROTATE_90:
+                        case ORI_ROTATE_180:
+                        case ORI_ROTATE_270:
+                        case ORI_FLIP_HOR:
+                        case ORI_FLIP_VERT:
+                        case ORI_TRANSPOSE:
+                        case ORI_TRANSVERSE:
+                            return orientation;
+                        default:
+                            return ORI_NORMAL;
+                    }
+                }
+            } catch (IOException e) {
+                Log.w(LOGTAG, "Failed to read EXIF orientation", e);
+            }
+        }
+        return ORI_NORMAL;
+    }
+
+    /**
+     * Returns the rotation of image at the given URI as one of 0, 90, 180,
+     * 270.  Defaults to 0.
+     */
+    public static int getMetadataRotation(Context context, Uri uri) {
+        int orientation = getMetadataOrientation(context, uri);
+        switch(orientation) {
+            case ORI_ROTATE_90:
+                return 90;
+            case ORI_ROTATE_180:
+                return 180;
+            case ORI_ROTATE_270:
+                return 270;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * Takes an orientation and a bitmap, and returns the bitmap transformed
+     * to that orientation.
+     */
+    public static Bitmap orientBitmap(Bitmap bitmap, int ori) {
+        Matrix matrix = new Matrix();
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        if (ori == ORI_ROTATE_90 ||
+                ori == ORI_ROTATE_270 ||
+                ori == ORI_TRANSPOSE ||
+                ori == ORI_TRANSVERSE) {
+            int tmp = w;
+            w = h;
+            h = tmp;
+        }
+        switch (ori) {
+            case ORI_ROTATE_90:
+                matrix.setRotate(90, w / 2f, h / 2f);
+                break;
+            case ORI_ROTATE_180:
+                matrix.setRotate(180, w / 2f, h / 2f);
+                break;
+            case ORI_ROTATE_270:
+                matrix.setRotate(270, w / 2f, h / 2f);
+                break;
+            case ORI_FLIP_HOR:
+                matrix.preScale(-1, 1);
+                break;
+            case ORI_FLIP_VERT:
+                matrix.preScale(1, -1);
+                break;
+            case ORI_TRANSPOSE:
+                matrix.setRotate(90, w / 2f, h / 2f);
+                matrix.preScale(1, -1);
+                break;
+            case ORI_TRANSVERSE:
+                matrix.setRotate(270, w / 2f, h / 2f);
+                matrix.preScale(1, -1);
+                break;
+            case ORI_NORMAL:
+            default:
+                return bitmap;
+        }
+        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
+                bitmap.getHeight(), matrix, true);
+    }
+
+    /**
+     * Returns the bitmap for the rectangular region given by "bounds"
+     * if it is a subset of the bitmap stored at uri.  Otherwise returns
+     * null.
+     */
+    public static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options,
+            Rect bounds) {
+        InputStream is = null;
+        try {
+            is = context.getContentResolver().openInputStream(uri);
+            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+            Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
+            // return null if bounds are not entirely within the bitmap
+            if (!r.contains(bounds)) {
+                return null;
+            }
+            return decoder.decodeRegion(bounds, options);
+        } catch (FileNotFoundException e) {
+            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+        } catch (IOException e) {
+            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+        } finally {
+            Utils.closeSilently(is);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the bounds of the bitmap stored at a given Url.
+     */
+    public static Rect loadBitmapBounds(Context context, Uri uri) {
+        BitmapFactory.Options o = new BitmapFactory.Options();
+        loadBitmap(context, uri, o);
+        return new Rect(0, 0, o.outWidth, o.outHeight);
+    }
+
+    /**
+     * Loads a bitmap that has been downsampled using sampleSize from a given url.
+     */
+    public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) {
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inMutable = true;
+        options.inSampleSize = sampleSize;
+        return loadBitmap(context, uri, options);
+    }
+
+
+    /**
+     * Returns the bitmap from the given uri loaded using the given options.
+     * Returns null on failure.
+     */
+    public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) {
+        if (uri == null || context == null) {
+            throw new IllegalArgumentException("bad argument to loadBitmap");
+        }
+        InputStream is = null;
+        try {
+            is = context.getContentResolver().openInputStream(uri);
+            return BitmapFactory.decodeStream(is, null, o);
+        } catch (FileNotFoundException e) {
+            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+        } finally {
+            Utils.closeSilently(is);
+        }
+        return null;
+    }
+
+    /**
+     * Loads a bitmap at a given URI that is downsampled so that both sides are
+     * smaller than maxSideLength. The Bitmap's original dimensions are stored
+     * in the rect originalBounds.
+     *
+     * @param uri URI of image to open.
+     * @param context context whose ContentResolver to use.
+     * @param maxSideLength max side length of returned bitmap.
+     * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
+     * @param useMin use min or max side of the original image
+     * @return downsampled bitmap or null if this operation failed.
+     */
+    public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
+            Rect originalBounds, boolean useMin) {
+        if (maxSideLength <= 0 || uri == null || context == null) {
+            throw new IllegalArgumentException("bad argument to getScaledBitmap");
+        }
+        // Get width and height of stored bitmap
+        Rect storedBounds = loadBitmapBounds(context, uri);
+        if (originalBounds != null) {
+            originalBounds.set(storedBounds);
+        }
+        int w = storedBounds.width();
+        int h = storedBounds.height();
+
+        // If bitmap cannot be decoded, return null
+        if (w <= 0 || h <= 0) {
+            return null;
+        }
+
+        // Find best downsampling size
+        int imageSide = 0;
+        if (useMin) {
+            imageSide = Math.min(w, h);
+        } else {
+            imageSide = Math.max(w, h);
+        }
+        int sampleSize = 1;
+        while (imageSide > maxSideLength) {
+            imageSide >>>= 1;
+            sampleSize <<= 1;
+        }
+
+        // Make sure sample size is reasonable
+        if (sampleSize <= 0 ||
+                0 >= (int) (Math.min(w, h) / sampleSize)) {
+            return null;
+        }
+        return loadDownsampledBitmap(context, uri, sampleSize);
+    }
+
+    /**
+     * Loads a bitmap at a given URI that is downsampled so that both sides are
+     * smaller than maxSideLength. The Bitmap's original dimensions are stored
+     * in the rect originalBounds.  The output is also transformed to the given
+     * orientation.
+     *
+     * @param uri URI of image to open.
+     * @param context context whose ContentResolver to use.
+     * @param maxSideLength max side length of returned bitmap.
+     * @param orientation  the orientation to transform the bitmap to.
+     * @param originalBounds set to the actual bounds of the stored bitmap.
+     * @return downsampled bitmap or null if this operation failed.
+     */
+    public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
+            int orientation, Rect originalBounds) {
+        Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
+        if (bmap != null) {
+            bmap = orientBitmap(bmap, orientation);
+        }
+        return bmap;
+    }
+
+    public static Bitmap getScaleOneImageForPreset(Context context, Uri uri, Rect bounds,
+            Rect destination) {
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inMutable = true;
+        if (destination != null) {
+            if (bounds.width() > destination.width()) {
+                int sampleSize = 1;
+                int w = bounds.width();
+                while (w > destination.width()) {
+                    sampleSize *= 2;
+                    w /= sampleSize;
+                }
+                options.inSampleSize = sampleSize;
+            }
+        }
+        Bitmap bmp = loadRegionBitmap(context, uri, options, bounds);
+        return bmp;
+    }
+
+    /**
+     * Loads a bitmap that is downsampled by at least the input sample size. In
+     * low-memory situations, the bitmap may be downsampled further.
+     */
+    public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) {
+        boolean noBitmap = true;
+        int num_tries = 0;
+        if (sampleSize <= 0) {
+            sampleSize = 1;
+        }
+        Bitmap bmap = null;
+        while (noBitmap) {
+            try {
+                // Try to decode, downsample if low-memory.
+                bmap = loadDownsampledBitmap(context, sourceUri, sampleSize);
+                noBitmap = false;
+            } catch (java.lang.OutOfMemoryError e) {
+                // Try with more downsampling before failing for good.
+                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+                    throw e;
+                }
+                bmap = null;
+                System.gc();
+                sampleSize *= 2;
+            }
+        }
+        return bmap;
+    }
+
+    /**
+     * Loads an oriented bitmap that is downsampled by at least the input sample
+     * size. In low-memory situations, the bitmap may be downsampled further.
+     */
+    public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri,
+            int sampleSize) {
+        Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize);
+        if (bitmap == null) {
+            return null;
+        }
+        int orientation = getMetadataOrientation(context, sourceUri);
+        bitmap = orientBitmap(bitmap, orientation);
+        return bitmap;
+    }
+
+    /**
+     * Loads bitmap from a resource that may be downsampled in low-memory situations.
+     */
+    public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
+            int id) {
+        boolean noBitmap = true;
+        int num_tries = 0;
+        if (options.inSampleSize < 1) {
+            options.inSampleSize = 1;
+        }
+        // Stopgap fix for low-memory devices.
+        Bitmap bmap = null;
+        while (noBitmap) {
+            try {
+                // Try to decode, downsample if low-memory.
+                bmap = BitmapFactory.decodeResource(
+                        res, id, options);
+                noBitmap = false;
+            } catch (java.lang.OutOfMemoryError e) {
+                // Retry before failing for good.
+                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+                    throw e;
+                }
+                bmap = null;
+                System.gc();
+                options.inSampleSize *= 2;
+            }
+        }
+        return bmap;
+    }
+
+    public static XMPMeta getXmpObject(Context context) {
+        try {
+            InputStream is = context.getContentResolver().openInputStream(
+                    MasterImage.getImage().getUri());
+            return XmpUtilHelper.extractXMPMeta(is);
+        } catch (FileNotFoundException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Determine if this is a light cycle 360 image
+     *
+     * @return true if it is a light Cycle image that is full 360
+     */
+    public static boolean queryLightCycle360(Context context) {
+        InputStream is = null;
+        try {
+            is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri());
+            XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
+            if (meta == null) {
+                return false;
+            }
+            String namespace = "http://ns.google.com/photos/1.0/panorama/";
+            String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
+            String fullWidthName = "GPano:FullPanoWidthPixels";
+
+            if (!meta.doesPropertyExist(namespace, cropWidthName)) {
+                return false;
+            }
+            if (!meta.doesPropertyExist(namespace, fullWidthName)) {
+                return false;
+            }
+
+            Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
+            Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
+
+            // Definition of a 360:
+            // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
+            if (cropValue != null && fullValue != null) {
+                return cropValue.equals(fullValue);
+            }
+
+            return false;
+        } catch (FileNotFoundException e) {
+            return false;
+        } catch (XMPException e) {
+            return false;
+        } finally {
+            Utils.closeSilently(is);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/Action.java b/src/com/android/gallery3d/filtershow/category/Action.java
new file mode 100644
index 0000000..332ca18
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/Action.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.category;
+
+import android.content.Context;
+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.RectF;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class Action implements RenderingRequestCaller {
+
+    private static final String LOGTAG = "Action";
+    private FilterRepresentation mRepresentation;
+    private String mName;
+    private Rect mImageFrame;
+    private Bitmap mImage;
+    private ArrayAdapter mAdapter;
+    public static final int FULL_VIEW = 0;
+    public static final int CROP_VIEW = 1;
+    private int mType = CROP_VIEW;
+    private Bitmap mPortraitImage;
+    private Bitmap mOverlayBitmap;
+    private Context mContext;
+
+    public Action(Context context, FilterRepresentation representation, int type) {
+        mContext = context;
+        setRepresentation(representation);
+        setType(type);
+    }
+
+    public Action(Context context, FilterRepresentation representation) {
+        this(context, representation, CROP_VIEW);
+    }
+
+    public FilterRepresentation getRepresentation() {
+        return mRepresentation;
+    }
+
+    public void setRepresentation(FilterRepresentation representation) {
+        mRepresentation = representation;
+        mName = representation.getName();
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
+
+    public void setImageFrame(Rect imageFrame, int orientation) {
+        if (mImageFrame != null && mImageFrame.equals(imageFrame)) {
+            return;
+        }
+        Bitmap bitmap = MasterImage.getImage().getLargeThumbnailBitmap();
+        if (bitmap != null) {
+            mImageFrame = imageFrame;
+            int w = mImageFrame.width();
+            int h = mImageFrame.height();
+            if (orientation == CategoryView.VERTICAL
+                && mType == CROP_VIEW) {
+                w /= 2;
+            }
+            Bitmap bitmapCrop = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+            drawCenteredImage(bitmap, bitmapCrop, true);
+
+            postNewIconRenderRequest(bitmapCrop);
+        }
+    }
+
+    public Bitmap getImage() {
+        return mImage;
+    }
+
+    public void setImage(Bitmap image) {
+        mImage = image;
+    }
+
+    public void setAdapter(ArrayAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    public void setType(int type) {
+        mType = type;
+    }
+
+    private void postNewIconRenderRequest(Bitmap bitmap) {
+        if (bitmap != null && mRepresentation != null) {
+            ImagePreset preset = new ImagePreset();
+            preset.addFilter(mRepresentation);
+            RenderingRequest.post(mContext, bitmap,
+                    preset, RenderingRequest.ICON_RENDERING, this);
+        }
+    }
+
+    private void drawCenteredImage(Bitmap source, Bitmap destination, boolean scale) {
+        RectF image = new RectF(0, 0, source.getWidth(), source.getHeight());
+        int border = 0;
+        if (!scale) {
+            border = destination.getWidth() - destination.getHeight();
+            if (border < 0) {
+                border = 0;
+            }
+        }
+        RectF frame = new RectF(border, 0,
+                destination.getWidth() - border,
+                destination.getHeight());
+        Matrix m = new Matrix();
+        m.setRectToRect(frame, image, Matrix.ScaleToFit.CENTER);
+        image.set(frame);
+        m.mapRect(image);
+        m.setRectToRect(image, frame, Matrix.ScaleToFit.FILL);
+        Canvas canvas = new Canvas(destination);
+        canvas.drawBitmap(source, m, new Paint(Paint.FILTER_BITMAP_FLAG));
+    }
+
+    @Override
+    public void available(RenderingRequest request) {
+        mImage = request.getBitmap();
+        if (mImage == null) {
+            return;
+        }
+        if (mRepresentation.getOverlayId() != 0 && mOverlayBitmap == null) {
+            mOverlayBitmap = BitmapFactory.decodeResource(
+                    mContext.getResources(),
+                    mRepresentation.getOverlayId());
+        }
+        if (mOverlayBitmap != null) {
+            if (getRepresentation().getFilterType() == FilterRepresentation.TYPE_BORDER) {
+                Canvas canvas = new Canvas(mImage);
+                canvas.drawBitmap(mOverlayBitmap, new Rect(0, 0, mOverlayBitmap.getWidth(), mOverlayBitmap.getHeight()),
+                        new Rect(0, 0, mImage.getWidth(), mImage.getHeight()), new Paint());
+            } else {
+                Canvas canvas = new Canvas(mImage);
+                canvas.drawARGB(128, 0, 0, 0);
+                drawCenteredImage(mOverlayBitmap, mImage, false);
+            }
+        }
+        if (mAdapter != null) {
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    public void setPortraitImage(Bitmap portraitImage) {
+        mPortraitImage = portraitImage;
+    }
+
+    public Bitmap getPortraitImage() {
+        return mPortraitImage;
+    }
+
+    public Bitmap getOverlayBitmap() {
+        return mOverlayBitmap;
+    }
+
+    public void setOverlayBitmap(Bitmap overlayBitmap) {
+        mOverlayBitmap = overlayBitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java
new file mode 100644
index 0000000..6451c39
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.category;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class CategoryAdapter extends ArrayAdapter<Action> {
+
+    private static final String LOGTAG = "CategoryAdapter";
+    private int mItemHeight;
+    private View mContainer;
+    private int mItemWidth = ListView.LayoutParams.MATCH_PARENT;
+    private int mSelectedPosition;
+    int mCategory;
+    private int mOrientation;
+
+    public CategoryAdapter(Context context, int textViewResourceId) {
+        super(context, textViewResourceId);
+        mItemHeight = (int) (context.getResources().getDisplayMetrics().density * 100);
+    }
+
+    public CategoryAdapter(Context context) {
+        this(context, 0);
+    }
+
+    public void setItemHeight(int height) {
+        mItemHeight = height;
+    }
+
+    public void setItemWidth(int width) {
+        mItemWidth = width;
+    }
+
+    @Override
+    public void add(Action action) {
+        super.add(action);
+        action.setAdapter(this);
+    }
+
+    public void initializeSelection(int category) {
+        mCategory = category;
+        mSelectedPosition = -1;
+        if (category == MainPanel.LOOKS) {
+            mSelectedPosition = 0;
+        }
+        if (category == MainPanel.BORDERS) {
+            mSelectedPosition = 0;
+        }
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+            convertView = new CategoryView(getContext());
+        }
+        CategoryView view = (CategoryView) convertView;
+        view.setOrientation(mOrientation);
+        view.setAction(getItem(position), this);
+        view.setLayoutParams(
+                new ListView.LayoutParams(mItemWidth, mItemHeight));
+        view.setTag(position);
+        view.invalidate();
+        return view;
+    }
+
+    public void setSelected(View v) {
+        int old = mSelectedPosition;
+        mSelectedPosition = (Integer) v.getTag();
+        if (old != -1) {
+            invalidateView(old);
+        }
+        invalidateView(mSelectedPosition);
+    }
+
+    public boolean isSelected(View v) {
+        return (Integer) v.getTag() == mSelectedPosition;
+    }
+
+    private void invalidateView(int position) {
+        View child = null;
+        if (mContainer instanceof ListView) {
+            ListView lv = (ListView) mContainer;
+            child = lv.getChildAt(position - lv.getFirstVisiblePosition());
+        } else {
+            CategoryTrack ct = (CategoryTrack) mContainer;
+            child = ct.getChildAt(position);
+        }
+        if (child != null) {
+            child.invalidate();
+        }
+    }
+
+    public void setContainer(View container) {
+        mContainer = container;
+    }
+
+    public void imageLoaded() {
+        notifyDataSetChanged();
+    }
+
+    public FilterRepresentation getTinyPlanet() {
+        for (int i = 0; i < getCount(); i++) {
+            Action action = getItem(i);
+            if (action.getRepresentation() != null
+                    && action.getRepresentation()
+                    instanceof FilterTinyPlanetRepresentation) {
+                return action.getRepresentation();
+            }
+        }
+        return null;
+    }
+
+    public void removeTinyPlanet() {
+        for (int i = 0; i < getCount(); i++) {
+            Action action = getItem(i);
+            if (action.getRepresentation() != null
+                    && action.getRepresentation()
+                    instanceof FilterTinyPlanetRepresentation) {
+                remove(action);
+                return;
+            }
+        }
+    }
+
+    public void setOrientation(int orientation) {
+        mOrientation = orientation;
+    }
+
+    public void reflectImagePreset(ImagePreset preset) {
+        if (preset == null) {
+            return;
+        }
+        int selected = 0; // if nothing found, select "none" (first element)
+        FilterRepresentation rep = null;
+        if (mCategory == MainPanel.LOOKS) {
+            int pos = preset.getPositionForType(FilterRepresentation.TYPE_FX);
+            if (pos != -1) {
+                rep = preset.getFilterRepresentation(pos);
+            }
+        } else if (mCategory == MainPanel.BORDERS) {
+            int pos = preset.getPositionForType(FilterRepresentation.TYPE_BORDER);
+            if (pos != -1) {
+                rep = preset.getFilterRepresentation(pos);
+            }
+        }
+        if (rep != null) {
+            for (int i = 0; i < getCount(); i++) {
+                if (rep.getName().equalsIgnoreCase(
+                        getItem(i).getRepresentation().getName())) {
+                    selected = i;
+                    break;
+                }
+            }
+        }
+        if (mSelectedPosition != selected) {
+            mSelectedPosition = selected;
+            this.notifyDataSetChanged();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryPanel.java b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java
new file mode 100644
index 0000000..de2481f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.category;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+
+public class CategoryPanel extends Fragment {
+
+    public static final String FRAGMENT_TAG = "CategoryPanel";
+    private static final String PARAMETER_TAG = "currentPanel";
+
+    private int mCurrentAdapter = MainPanel.LOOKS;
+    private CategoryAdapter mAdapter;
+
+    public void setAdapter(int value) {
+        mCurrentAdapter = value;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        loadAdapter(mCurrentAdapter);
+    }
+
+    private void loadAdapter(int adapter) {
+        FilterShowActivity activity = (FilterShowActivity) getActivity();
+        switch (adapter) {
+            case MainPanel.LOOKS: {
+                mAdapter = activity.getCategoryLooksAdapter();
+                mAdapter.initializeSelection(MainPanel.LOOKS);
+                activity.updateCategories();
+                break;
+            }
+            case MainPanel.BORDERS: {
+                mAdapter = activity.getCategoryBordersAdapter();
+                mAdapter.initializeSelection(MainPanel.BORDERS);
+                activity.updateCategories();
+                break;
+            }
+            case MainPanel.GEOMETRY: {
+                mAdapter = activity.getCategoryGeometryAdapter();
+                mAdapter.initializeSelection(MainPanel.GEOMETRY);
+                break;
+            }
+            case MainPanel.FILTERS: {
+                mAdapter = activity.getCategoryFiltersAdapter();
+                mAdapter.initializeSelection(MainPanel.FILTERS);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle state) {
+        super.onSaveInstanceState(state);
+        state.putInt(PARAMETER_TAG, mCurrentAdapter);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        LinearLayout main = (LinearLayout) inflater.inflate(
+                R.layout.filtershow_category_panel_new, container,
+                false);
+
+        if (savedInstanceState != null) {
+            int selectedPanel = savedInstanceState.getInt(PARAMETER_TAG);
+            loadAdapter(selectedPanel);
+        }
+
+        View panelView = main.findViewById(R.id.listItems);
+        if (panelView instanceof CategoryTrack) {
+            CategoryTrack panel = (CategoryTrack) panelView;
+            mAdapter.setOrientation(CategoryView.HORIZONTAL);
+            panel.setAdapter(mAdapter);
+            mAdapter.setContainer(panel);
+        } else {
+            ListView panel = (ListView) main.findViewById(R.id.listItems);
+            panel.setAdapter(mAdapter);
+            mAdapter.setContainer(panel);
+        }
+        return main;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryTrack.java b/src/com/android/gallery3d/filtershow/category/CategoryTrack.java
new file mode 100644
index 0000000..ac8245a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryTrack.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.category;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+
+public class CategoryTrack extends LinearLayout {
+
+    private CategoryAdapter mAdapter;
+    private int mElemSize;
+    private DataSetObserver mDataSetObserver = new DataSetObserver() {
+        @Override
+        public void onChanged() {
+            super.onChanged();
+            invalidate();
+        }
+        @Override
+        public void onInvalidated() {
+            super.onInvalidated();
+            fillContent();
+        }
+    };
+
+    public CategoryTrack(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CategoryTrack);
+        mElemSize = a.getDimensionPixelSize(R.styleable.CategoryTrack_iconSize, 0);
+    }
+
+    public void setAdapter(CategoryAdapter adapter) {
+        mAdapter = adapter;
+        mAdapter.registerDataSetObserver(mDataSetObserver);
+        fillContent();
+    }
+
+    public void fillContent() {
+        removeAllViews();
+        mAdapter.setItemWidth(mElemSize);
+        mAdapter.setItemHeight(LayoutParams.MATCH_PARENT);
+        int n = mAdapter.getCount();
+        for (int i = 0; i < n; i++) {
+            View view = mAdapter.getView(i, null, this);
+            addView(view, i);
+        }
+        requestLayout();
+    }
+
+    @Override
+    public void invalidate() {
+        for (int i = 0; i < this.getChildCount(); i++) {
+            View child = getChildAt(i);
+            child.invalidate();
+        }
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryView.java b/src/com/android/gallery3d/filtershow/category/CategoryView.java
new file mode 100644
index 0000000..c456dc2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryView.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.category;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.view.View;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.ui.SelectionRenderer;
+
+public class CategoryView extends View implements View.OnClickListener {
+
+    private static final String LOGTAG = "CategoryView";
+    public static final int VERTICAL = 0;
+    public static final int HORIZONTAL = 1;
+    private Paint mPaint = new Paint();
+    private Action mAction;
+    private Rect mTextBounds = new Rect();
+    private int mMargin = 16;
+    private int mTextSize = 32;
+    private int mTextColor;
+    private int mBackgroundColor;
+    private Paint mSelectPaint;
+    CategoryAdapter mAdapter;
+    private int mSelectionStroke;
+    private Paint mBorderPaint;
+    private int mBorderStroke;
+    private int mOrientation = VERTICAL;
+
+    public CategoryView(Context context) {
+        super(context);
+        setOnClickListener(this);
+        Resources res = getResources();
+        mBackgroundColor = res.getColor(R.color.filtershow_categoryview_background);
+        mTextColor = res.getColor(R.color.filtershow_categoryview_text);
+        mSelectionStroke = res.getDimensionPixelSize(R.dimen.thumbnail_margin);
+        mTextSize = res.getDimensionPixelSize(R.dimen.category_panel_text_size);
+        mMargin = res.getDimensionPixelOffset(R.dimen.category_panel_margin);
+        mSelectPaint = new Paint();
+        mSelectPaint.setStyle(Paint.Style.FILL);
+        mSelectPaint.setColor(res.getColor(R.color.filtershow_category_selection));
+        mBorderPaint = new Paint(mSelectPaint);
+        mBorderPaint.setColor(Color.BLACK);
+        mBorderStroke = mSelectionStroke / 3;
+    }
+
+    private void computeTextPosition(String text) {
+        if (text == null) {
+            return;
+        }
+        mPaint.setTextSize(mTextSize);
+        if (mOrientation == VERTICAL) {
+            text = text.toUpperCase();
+            // TODO: set this in xml
+            mPaint.setTypeface(Typeface.DEFAULT_BOLD);
+        }
+        mPaint.getTextBounds(text, 0, text.length(), mTextBounds);
+    }
+
+    public void drawText(Canvas canvas, String text) {
+        if (text == null) {
+            return;
+        }
+        float textWidth = mPaint.measureText(text);
+        int x = (int) (canvas.getWidth() - textWidth - mMargin);
+        if (mOrientation == HORIZONTAL) {
+            x = (int) ((canvas.getWidth() - textWidth) / 2.0f);
+        }
+        if (x < 0) {
+            // If the text takes more than the view width,
+            // justify to the left.
+            x = mMargin;
+        }
+        int y = canvas.getHeight() - mMargin;
+        canvas.drawText(text, x, y, mPaint);
+    }
+
+    @Override
+    public CharSequence getContentDescription () {
+        if (mAction != null) {
+            return mAction.getName();
+        }
+        return null;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        canvas.drawColor(mBackgroundColor);
+        if (mAction != null) {
+            mPaint.reset();
+            mPaint.setAntiAlias(true);
+            computeTextPosition(mAction.getName());
+            if (mAction.getImage() == null) {
+                mAction.setImageFrame(new Rect(0, 0, getWidth(), getHeight()), mOrientation);
+            } else {
+                Bitmap bitmap = mAction.getImage();
+                canvas.save();
+                Rect clipRect = new Rect(mSelectionStroke, mSelectionStroke,
+                        getWidth() - mSelectionStroke,
+                        getHeight() - 2* mMargin - mTextSize);
+                int offsetx = 0;
+                int offsety = 0;
+                if (mOrientation == HORIZONTAL) {
+                    canvas.clipRect(clipRect);
+                    offsetx = - (bitmap.getWidth() - clipRect.width()) / 2;
+                    offsety = - (bitmap.getHeight() - clipRect.height()) / 2;
+                }
+                canvas.drawBitmap(bitmap, offsetx, offsety, mPaint);
+                canvas.restore();
+                if (mAdapter.isSelected(this)) {
+                    if (mOrientation == HORIZONTAL) {
+                        SelectionRenderer.drawSelection(canvas, 0, 0,
+                                getWidth(), getHeight() - mMargin - mTextSize,
+                                mSelectionStroke, mSelectPaint, mBorderStroke, mBorderPaint);
+                    } else {
+                        SelectionRenderer.drawSelection(canvas, 0, 0,
+                                Math.min(bitmap.getWidth(), getWidth()),
+                                Math.min(bitmap.getHeight(), getHeight()),
+                                mSelectionStroke, mSelectPaint, mBorderStroke, mBorderPaint);
+                    }
+                }
+            }
+            mPaint.setColor(mBackgroundColor);
+            mPaint.setStyle(Paint.Style.STROKE);
+            mPaint.setStrokeWidth(3);
+            drawText(canvas, mAction.getName());
+            mPaint.setColor(mTextColor);
+            mPaint.setStyle(Paint.Style.FILL);
+            mPaint.setStrokeWidth(1);
+            drawText(canvas, mAction.getName());
+        }
+    }
+
+    public void setAction(Action action, CategoryAdapter adapter) {
+        mAction = action;
+        mAdapter = adapter;
+        invalidate();
+    }
+
+    public FilterRepresentation getRepresentation() {
+        return mAction.getRepresentation();
+    }
+
+    @Override
+    public void onClick(View view) {
+        FilterShowActivity activity = (FilterShowActivity) getContext();
+        activity.showRepresentation(mAction.getRepresentation());
+        mAdapter.setSelected(this);
+    }
+
+    public void setOrientation(int orientation) {
+        mOrientation = orientation;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/MainPanel.java b/src/com/android/gallery3d/filtershow/category/MainPanel.java
new file mode 100644
index 0000000..9a64ffb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/MainPanel.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.category;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.state.StatePanel;
+
+public class MainPanel extends Fragment {
+
+    private static final String LOGTAG = "MainPanel";
+
+    private LinearLayout mMainView;
+    private ImageButton looksButton;
+    private ImageButton bordersButton;
+    private ImageButton geometryButton;
+    private ImageButton filtersButton;
+
+    public static final String FRAGMENT_TAG = "MainPanel";
+    public static final int LOOKS = 0;
+    public static final int BORDERS = 1;
+    public static final int GEOMETRY = 2;
+    public static final int FILTERS = 3;
+
+    private int mCurrentSelected = -1;
+
+    private void selection(int position, boolean value) {
+        if (value) {
+            FilterShowActivity activity = (FilterShowActivity) getActivity();
+            activity.setCurrentPanel(position);
+        }
+        switch (position) {
+            case LOOKS: {
+                looksButton.setSelected(value);
+                break;
+            }
+            case BORDERS: {
+                bordersButton.setSelected(value);
+                break;
+            }
+            case GEOMETRY: {
+                geometryButton.setSelected(value);
+                break;
+            }
+            case FILTERS: {
+                filtersButton.setSelected(value);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        if (mMainView != null) {
+            if (mMainView.getParent() != null) {
+                ViewGroup parent = (ViewGroup) mMainView.getParent();
+                parent.removeView(mMainView);
+            }
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+
+        mMainView = (LinearLayout) inflater.inflate(
+                R.layout.filtershow_main_panel, null, false);
+
+        looksButton = (ImageButton) mMainView.findViewById(R.id.fxButton);
+        bordersButton = (ImageButton) mMainView.findViewById(R.id.borderButton);
+        geometryButton = (ImageButton) mMainView.findViewById(R.id.geometryButton);
+        filtersButton = (ImageButton) mMainView.findViewById(R.id.colorsButton);
+
+        looksButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showPanel(LOOKS);
+            }
+        });
+        bordersButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showPanel(BORDERS);
+            }
+        });
+        geometryButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showPanel(GEOMETRY);
+            }
+        });
+        filtersButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showPanel(FILTERS);
+            }
+        });
+
+        FilterShowActivity activity = (FilterShowActivity) getActivity();
+        showImageStatePanel(activity.isShowingImageStatePanel());
+        showPanel(activity.getCurrentPanel());
+        return mMainView;
+    }
+
+    private boolean isRightAnimation(int newPos) {
+        if (newPos < mCurrentSelected) {
+            return false;
+        }
+        return true;
+    }
+
+    private void setCategoryFragment(CategoryPanel category, boolean fromRight) {
+        FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+        if (fromRight) {
+            transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_right);
+        } else {
+            transaction.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_left);
+        }
+        transaction.replace(R.id.category_panel_container, category, CategoryPanel.FRAGMENT_TAG);
+        transaction.commit();
+    }
+
+    public void loadCategoryLookPanel() {
+        if (mCurrentSelected == LOOKS) {
+            return;
+        }
+        boolean fromRight = isRightAnimation(LOOKS);
+        selection(mCurrentSelected, false);
+        CategoryPanel categoryPanel = new CategoryPanel();
+        categoryPanel.setAdapter(LOOKS);
+        setCategoryFragment(categoryPanel, fromRight);
+        mCurrentSelected = LOOKS;
+        selection(mCurrentSelected, true);
+    }
+
+    public void loadCategoryBorderPanel() {
+        if (mCurrentSelected == BORDERS) {
+            return;
+        }
+        boolean fromRight = isRightAnimation(BORDERS);
+        selection(mCurrentSelected, false);
+        CategoryPanel categoryPanel = new CategoryPanel();
+        categoryPanel.setAdapter(BORDERS);
+        setCategoryFragment(categoryPanel, fromRight);
+        mCurrentSelected = BORDERS;
+        selection(mCurrentSelected, true);
+    }
+
+    public void loadCategoryGeometryPanel() {
+        if (mCurrentSelected == GEOMETRY) {
+            return;
+        }
+        boolean fromRight = isRightAnimation(GEOMETRY);
+        selection(mCurrentSelected, false);
+        CategoryPanel categoryPanel = new CategoryPanel();
+        categoryPanel.setAdapter(GEOMETRY);
+        setCategoryFragment(categoryPanel, fromRight);
+        mCurrentSelected = GEOMETRY;
+        selection(mCurrentSelected, true);
+    }
+
+    public void loadCategoryFiltersPanel() {
+        if (mCurrentSelected == FILTERS) {
+            return;
+        }
+        boolean fromRight = isRightAnimation(FILTERS);
+        selection(mCurrentSelected, false);
+        CategoryPanel categoryPanel = new CategoryPanel();
+        categoryPanel.setAdapter(FILTERS);
+        setCategoryFragment(categoryPanel, fromRight);
+        mCurrentSelected = FILTERS;
+        selection(mCurrentSelected, true);
+    }
+
+    public void showPanel(int currentPanel) {
+        switch (currentPanel) {
+            case LOOKS: {
+                loadCategoryLookPanel();
+                break;
+            }
+            case BORDERS: {
+                loadCategoryBorderPanel();
+                break;
+            }
+            case GEOMETRY: {
+                loadCategoryGeometryPanel();
+                break;
+            }
+            case FILTERS: {
+                loadCategoryFiltersPanel();
+                break;
+            }
+        }
+    }
+
+    public void showImageStatePanel(boolean show) {
+        if (mMainView.findViewById(R.id.state_panel_container) == null) {
+            return;
+        }
+        FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+        final View container = mMainView.findViewById(R.id.state_panel_container);
+        if (show) {
+            container.setVisibility(View.VISIBLE);
+            StatePanel statePanel = new StatePanel();
+            transaction.replace(R.id.state_panel_container, statePanel, StatePanel.FRAGMENT_TAG);
+        } else {
+            container.setVisibility(View.GONE);
+            Fragment statePanel = getChildFragmentManager().findFragmentByTag(StatePanel.FRAGMENT_TAG);
+            if (statePanel != null) {
+                transaction.remove(statePanel);
+            }
+        }
+        transaction.commit();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java
new file mode 100644
index 0000000..dd4df7d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.colorpicker;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorGridDialog extends Dialog {
+    RGBListener mCallback;
+    private static final String LOGTAG = "ColorGridDialog";
+
+    public ColorGridDialog(Context context, final RGBListener cl) {
+        super(context);
+        mCallback = cl;
+        setTitle(R.string.color_pick_title);
+        setContentView(R.layout.filtershow_color_gird);
+        Button sel = (Button) findViewById(R.id.filtershow_cp_custom);
+        ArrayList<Button> b = getButtons((ViewGroup) getWindow().getDecorView());
+        int k = 0;
+        float[] hsv = new float[3];
+
+        for (Button button : b) {
+            if (!button.equals(sel)){
+                hsv[0] = (k % 5) * 360 / 5;
+                hsv[1] = (k / 5) / 3.0f;
+                hsv[2] = (k < 5) ? (k / 4f) : 1;
+                final int c = (Color.HSVToColor(hsv) & 0x00FFFFFF) | 0xAA000000;
+                GradientDrawable sd = ((GradientDrawable) button.getBackground());
+                button.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                    public void onClick(View v) {
+                        mCallback.setColor(c);
+                        dismiss();
+                    }
+                });
+                sd.setColor(c);
+                k++;
+            }
+
+        }
+        sel.setOnClickListener(new View.OnClickListener() {
+                @Override
+            public void onClick(View v) {
+                showColorPicker();
+                ColorGridDialog.this.dismiss();
+            }
+        });
+    }
+
+    private ArrayList<Button> getButtons(ViewGroup vg) {
+        ArrayList<Button> list = new ArrayList<Button>();
+        for (int i = 0; i < vg.getChildCount(); i++) {
+            View v = vg.getChildAt(i);
+            if (v instanceof Button) {
+                list.add((Button) v);
+            } else if (v instanceof ViewGroup) {
+                list.addAll(getButtons((ViewGroup) v));
+            }
+        }
+        return list;
+    }
+
+    public void showColorPicker() {
+        ColorListener cl = new ColorListener() {
+                @Override
+            public void setColor(float[] hsvo) {
+                int c = Color.HSVToColor(hsvo) & 0xFFFFFF;
+                int alpha = (int) (hsvo[3] * 255);
+                c |= alpha << 24;
+                mCallback.setColor(c);
+            }
+        };
+        ColorPickerDialog cpd = new ColorPickerDialog(this.getContext(), cl);
+        cpd.show();
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java
new file mode 100644
index 0000000..5127dad
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.colorpicker;
+
+public interface ColorListener {
+    void setColor(float[] hsvo);
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java
new file mode 100644
index 0000000..2bff501
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.colorpicker;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorOpacityView extends View implements ColorListener {
+
+    private float mRadius;
+    private float mWidth;
+    private Paint mBarPaint1;
+    private Paint mLinePaint1;
+    private Paint mLinePaint2;
+    private Paint mCheckPaint;
+
+    private float mHeight;
+    private Paint mDotPaint;
+    private int mBgcolor = 0;
+
+    private float mDotRadius;
+    private float mBorder;
+
+    private float[] mHSVO = new float[4];
+    private int mSliderColor;
+    private float mDotX = mBorder;
+    private float mDotY = mBorder;
+    private final static float DOT_SIZE = ColorRectView.DOT_SIZE;
+    public final static float BORDER_SIZE = 20;;
+
+    public ColorOpacityView(Context ctx, AttributeSet attrs) {
+        super(ctx, attrs);
+        DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
+        float mDpToPix = metrics.density;
+        mDotRadius = DOT_SIZE * mDpToPix;
+        mBorder = BORDER_SIZE * mDpToPix;
+        mBarPaint1 = new Paint();
+
+        mDotPaint = new Paint();
+
+        mDotPaint.setStyle(Paint.Style.FILL);
+        mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color));
+        mSliderColor = ctx.getResources().getColor(R.color.slider_line_color);
+
+        mBarPaint1.setStyle(Paint.Style.FILL);
+
+        mLinePaint1 = new Paint();
+        mLinePaint1.setColor(Color.GRAY);
+        mLinePaint2 = new Paint();
+        mLinePaint2.setColor(mSliderColor);
+        mLinePaint2.setStrokeWidth(4);
+
+        int[] colors = new int[16 * 16];
+        for (int i = 0; i < colors.length; i++) {
+            int y = i / (16 * 8);
+            int x = (i / 8) % 2;
+            colors[i] = (x == y) ? 0xFFAAAAAA : 0xFF444444;
+        }
+        Bitmap bitmap = Bitmap.createBitmap(colors, 16, 16, Bitmap.Config.ARGB_8888);
+        BitmapShader bs = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+        mCheckPaint = new Paint();
+        mCheckPaint.setShader(bs);
+    }
+
+    public boolean onDown(MotionEvent e) {
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        float ox = mDotX;
+        float oy = mDotY;
+
+        float x = event.getX();
+        float y = event.getY();
+
+        mDotX = x;
+
+        if (mDotX < mBorder) {
+            mDotX = mBorder;
+        }
+
+        if (mDotX > mWidth - mBorder) {
+            mDotX = mWidth - mBorder;
+        }
+        mHSVO[3] = (mDotX - mBorder) / (mWidth - mBorder * 2);
+        notifyColorListeners(mHSVO);
+        setupButton();
+        invalidate((int) (ox - mDotRadius), (int) (oy - mDotRadius), (int) (ox + mDotRadius),
+                (int) (oy + mDotRadius));
+        invalidate(
+                (int) (mDotX - mDotRadius), (int) (mDotY - mDotRadius), (int) (mDotX + mDotRadius),
+                (int) (mDotY + mDotRadius));
+
+        return true;
+    }
+
+    private void setupButton() {
+        float pos = mHSVO[3] * (mWidth - mBorder * 2);
+        mDotX = pos + mBorder;
+
+        int[] colors3 = new int[] {
+        mSliderColor, mSliderColor, 0x66000000, 0 };
+        RadialGradient g = new RadialGradient(mDotX, mDotY, mDotRadius, colors3, new float[] {
+        0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+        mDotPaint.setShader(g);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mWidth = w;
+        mHeight = h;
+        mDotY = mHeight / 2;
+        updatePaint();
+        setupButton();
+    }
+
+    private void updatePaint() {
+
+        int color2 = Color.HSVToColor(mHSVO);
+        int color1 = color2 & 0xFFFFFF;
+
+        Shader sg = new LinearGradient(
+                mBorder, mBorder, mWidth - mBorder, mBorder, color1, color2, Shader.TileMode.CLAMP);
+        mBarPaint1.setShader(sg);
+
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.drawColor(mBgcolor);
+        canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mCheckPaint);
+        canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mBarPaint1);
+        canvas.drawLine(mDotX, mDotY, mWidth - mBorder, mDotY, mLinePaint1);
+        canvas.drawLine(mBorder, mDotY, mDotX, mDotY, mLinePaint2);
+        if (mDotX != Float.NaN) {
+            canvas.drawCircle(mDotX, mDotY, mDotRadius, mDotPaint);
+        }
+    }
+
+    @Override
+    public void setColor(float[] hsv) {
+        System.arraycopy(hsv, 0, mHSVO, 0, mHSVO.length);
+
+        float oy = mDotY;
+
+        updatePaint();
+        setupButton();
+        invalidate();
+    }
+
+    ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>();
+
+    public void notifyColorListeners(float[] hsvo) {
+        for (ColorListener l : mColorListeners) {
+            l.setColor(hsvo);
+        }
+    }
+
+    public void addColorListener(ColorListener l) {
+        mColorListeners.add(l);
+    }
+
+    public void removeColorListener(ColorListener l) {
+        mColorListeners.remove(l);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java
new file mode 100644
index 0000000..73a5c90
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.colorpicker;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ToggleButton;
+
+import com.android.gallery3d.R;
+
+public class ColorPickerDialog extends Dialog implements ColorListener {
+    ToggleButton mSelectedButton;
+    GradientDrawable mSelectRect;
+
+    float[] mHSVO = new float[4];
+
+    public ColorPickerDialog(Context context, final ColorListener cl) {
+        super(context);
+
+        setContentView(R.layout.filtershow_color_picker);
+        ColorValueView csv = (ColorValueView) findViewById(R.id.colorValueView);
+        ColorRectView cwv = (ColorRectView) findViewById(R.id.colorRectView);
+        ColorOpacityView cvv = (ColorOpacityView) findViewById(R.id.colorOpacityView);
+        float[] hsvo = new float[] {
+                123, .9f, 1, 1 };
+
+        mSelectRect = (GradientDrawable) getContext()
+                .getResources().getDrawable(R.drawable.filtershow_color_picker_roundrect);
+        Button selButton = (Button) findViewById(R.id.btnSelect);
+        selButton.setCompoundDrawablesWithIntrinsicBounds(null, null, mSelectRect, null);
+        Button sel = (Button) findViewById(R.id.btnSelect);
+
+        sel.setOnClickListener(new View.OnClickListener() {
+                @Override
+            public void onClick(View v) {
+                ColorPickerDialog.this.dismiss();
+                if (cl != null) {
+                    cl.setColor(mHSVO);
+                }
+            }
+        });
+
+        cwv.setColor(hsvo);
+        cvv.setColor(hsvo);
+        csv.setColor(hsvo);
+        csv.addColorListener(cwv);
+        cwv.addColorListener(csv);
+        csv.addColorListener(cvv);
+        cwv.addColorListener(cvv);
+        cvv.addColorListener(cwv);
+        cvv.addColorListener(csv);
+        cvv.addColorListener(this);
+        csv.addColorListener(this);
+        cwv.addColorListener(this);
+
+    }
+
+    void toggleClick(ToggleButton v, int[] buttons, boolean isChecked) {
+        int id = v.getId();
+        if (!isChecked) {
+            mSelectedButton = null;
+            return;
+        }
+        for (int i = 0; i < buttons.length; i++) {
+            if (id != buttons[i]) {
+                ToggleButton b = (ToggleButton) findViewById(buttons[i]);
+                b.setChecked(false);
+            }
+        }
+        mSelectedButton = v;
+
+        float[] hsv = (float[]) v.getTag();
+
+        ColorValueView csv = (ColorValueView) findViewById(R.id.colorValueView);
+        ColorRectView cwv = (ColorRectView) findViewById(R.id.colorRectView);
+        ColorOpacityView cvv = (ColorOpacityView) findViewById(R.id.colorOpacityView);
+        cwv.setColor(hsv);
+        cvv.setColor(hsv);
+        csv.setColor(hsv);
+    }
+
+    @Override
+    public void setColor(float[] hsvo) {
+        System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length);
+        int color = Color.HSVToColor(hsvo);
+        mSelectRect.setColor(color);
+        setButtonColor(mSelectedButton, hsvo);
+    }
+
+    private void setButtonColor(ToggleButton button, float[] hsv) {
+        if (button == null) {
+            return;
+        }
+        int color = Color.HSVToColor(hsv);
+        button.setBackgroundColor(color);
+        float[] fg = new float[] {
+                (hsv[0] + 180) % 360,
+                hsv[1],
+                        (hsv[2] > .5f) ? .1f : .9f
+        };
+        button.setTextColor(Color.HSVToColor(fg));
+        button.setTag(hsv);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java
new file mode 100644
index 0000000..07d7c71
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorRectView extends View implements ColorListener {
+    private float mDpToPix;
+    private float mRadius = 80;
+    private float mCtrY = 100;
+    private Paint mWheelPaint1;
+    private Paint mWheelPaint2;
+    private Paint mWheelPaint3;
+    private float mCtrX = 100;
+    private Paint mDotPaint;
+    private float mDotRadus;
+    private float mBorder;
+    private int mBgcolor = 0;
+    private float mDotX = Float.NaN;
+    private float mDotY;
+    private int mSliderColor = 0xFF33B5E5;
+    private float[] mHSVO = new float[4];
+    private int[] mColors = new int[] {
+            0xFFFF0000,// red
+            0xFFFFFF00,// yellow
+            0xFF00FF00,// green
+            0xFF00FFFF,// cyan
+            0xFF0000FF,// blue
+            0xFFFF00FF,// magenta
+            0xFFFF0000,// red
+    };
+    private int mWidth;
+    private int mHeight;
+    public final static float DOT_SIZE = 20;
+    public final static float BORDER_SIZE = 10;
+
+    public ColorRectView(Context ctx, AttributeSet attrs) {
+        super(ctx, attrs);
+
+        DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
+        mDpToPix = metrics.density;
+        mDotRadus = DOT_SIZE * mDpToPix;
+        mBorder = BORDER_SIZE * mDpToPix;
+
+        mWheelPaint1 = new Paint();
+        mWheelPaint2 = new Paint();
+        mWheelPaint3 = new Paint();
+        mDotPaint = new Paint();
+
+        mDotPaint.setStyle(Paint.Style.FILL);
+        mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color));
+        mSliderColor = ctx.getResources().getColor(R.color.slider_line_color);
+        mWheelPaint1.setStyle(Paint.Style.FILL);
+        mWheelPaint2.setStyle(Paint.Style.FILL);
+        mWheelPaint3.setStyle(Paint.Style.FILL);
+    }
+
+    public boolean onDown(MotionEvent e) {
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+
+        invalidate((int) (mDotX - mDotRadus), (int) (mDotY - mDotRadus), (int) (mDotX + mDotRadus),
+                (int) (mDotY + mDotRadus));
+        float x = event.getX();
+        float y = event.getY();
+
+        x = Math.max(Math.min(x, mWidth - mBorder), mBorder);
+        y = Math.max(Math.min(y, mHeight - mBorder), mBorder);
+        mDotX = x;
+        mDotY = y;
+        float sat = 1 - (mDotY - mBorder) / (mHeight - 2 * mBorder);
+        if (sat > 1) {
+            sat = 1;
+        }
+
+        double hue = Math.PI * 2 * (mDotX - mBorder) / (mHeight - 2 * mBorder);
+        mHSVO[0] = ((float) Math.toDegrees(hue) + 360) % 360;
+        mHSVO[1] = sat;
+        notifyColorListeners(mHSVO);
+        updateDotPaint();
+        invalidate((int) (mDotX - mDotRadus), (int) (mDotY - mDotRadus), (int) (mDotX + mDotRadus),
+                (int) (mDotY + mDotRadus));
+
+        return true;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mWidth = w;
+        mHeight = h;
+        mCtrY = h / 2f;
+        mCtrX = w / 2f;
+        mRadius = Math.min(mCtrY, mCtrX) - 2 * mBorder;
+        setUpColorPanel();
+    }
+
+    private void setUpColorPanel() {
+        float val = mHSVO[2];
+        int v = 0xFF000000 | 0x10101 * (int) (val * 0xFF);
+        int[] colors = new int[] {
+                0x0000000, v };
+        int[] colors2 = new int[] {
+                0x0000000, 0xFF000000 };
+        int[] wheelColor = new int[mColors.length];
+        float[] hsv = new float[3];
+        for (int i = 0; i < wheelColor.length; i++) {
+            Color.colorToHSV(mColors[i], hsv);
+            hsv[2] = mHSVO[2];
+            wheelColor[i] = Color.HSVToColor(hsv);
+        }
+        updateDot();
+        updateDotPaint();
+        SweepGradient sg = new SweepGradient(mCtrX, mCtrY, wheelColor, null);
+        LinearGradient lg = new LinearGradient(
+                mBorder, 0, mWidth - mBorder, 0, wheelColor, null, Shader.TileMode.CLAMP);
+
+        mWheelPaint1.setShader(lg);
+        LinearGradient rg = new LinearGradient(
+                0, mBorder, 0, mHeight - mBorder, colors, null, Shader.TileMode.CLAMP);
+        mWheelPaint2.setShader(rg);
+        LinearGradient rg2 = new LinearGradient(
+                0, mBorder, 0, mHeight - mBorder, colors2, null, Shader.TileMode.CLAMP);
+        mWheelPaint3.setShader(rg2);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.drawColor(mBgcolor);
+        RectF rect = new RectF();
+        rect.left = mBorder;
+        rect.right = mWidth - mBorder;
+        rect.top = mBorder;
+        rect.bottom = mHeight - mBorder;
+
+        canvas.drawRect(rect, mWheelPaint1);
+        canvas.drawRect(rect, mWheelPaint3);
+        canvas.drawRect(rect, mWheelPaint2);
+
+        if (mDotX != Float.NaN) {
+
+            canvas.drawCircle(mDotX, mDotY, mDotRadus, mDotPaint);
+        }
+    }
+
+    private void updateDot() {
+
+        double hue = mHSVO[0];
+        double sat = mHSVO[1];
+
+        mDotX = (float) (mBorder + (mHeight - 2 * mBorder) * Math.toRadians(hue) / (Math.PI * 2));
+        mDotY = (float) ((1 - sat) * (mHeight - 2 * mBorder) + mBorder);
+
+    }
+
+    private void updateDotPaint() {
+        int[] colors3 = new int[] {
+                mSliderColor, mSliderColor, 0x66000000, 0 };
+        RadialGradient g = new RadialGradient(mDotX, mDotY, mDotRadus, colors3, new float[] {
+                0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+        mDotPaint.setShader(g);
+
+    }
+
+    @Override
+    public void setColor(float[] hsvo) {
+        System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length);
+
+        setUpColorPanel();
+        invalidate();
+
+        updateDot();
+        updateDotPaint();
+
+    }
+
+    ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>();
+
+    public void notifyColorListeners(float[] hsv) {
+        for (ColorListener l : mColorListeners) {
+            l.setColor(hsv);
+        }
+    }
+
+    public void addColorListener(ColorListener l) {
+        mColorListeners.add(l);
+    }
+
+    public void removeColorListener(ColorListener l) {
+        mColorListeners.remove(l);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java
new file mode 100644
index 0000000..13cb44b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorValueView extends View implements ColorListener {
+
+    private float mRadius;
+    private float mWidth;
+    private Paint mBarPaint1;
+    private Paint mLinePaint1;
+    private Paint mLinePaint2;
+    private float mHeight;
+    private int mBgcolor = 0;
+    private Paint mDotPaint;
+    private float dotRadus;
+    private float mBorder;
+
+    private float[] mHSVO = new float[4];
+    private int mSliderColor;
+    private float mDotX;
+    private float mDotY = mBorder;
+    private final static float DOT_SIZE = ColorRectView.DOT_SIZE;
+    private final static float BORDER_SIZE = ColorRectView.DOT_SIZE;
+
+    public ColorValueView(Context ctx, AttributeSet attrs) {
+        super(ctx, attrs);
+        DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
+        float mDpToPix = metrics.density;
+        dotRadus = DOT_SIZE * mDpToPix;
+        mBorder = BORDER_SIZE * mDpToPix;
+
+        mBarPaint1 = new Paint();
+
+        mDotPaint = new Paint();
+
+        mDotPaint.setStyle(Paint.Style.FILL);
+        mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color));
+
+        mBarPaint1.setStyle(Paint.Style.FILL);
+
+        mLinePaint1 = new Paint();
+        mLinePaint1.setColor(Color.GRAY);
+        mLinePaint2 = new Paint();
+        mSliderColor = ctx.getResources().getColor(R.color.slider_line_color);
+        mLinePaint2.setColor(mSliderColor);
+        mLinePaint2.setStrokeWidth(4);
+    }
+
+    public boolean onDown(MotionEvent e) {
+        return true;
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        float ox = mDotX;
+        float oy = mDotY;
+
+        float x = event.getX();
+        float y = event.getY();
+
+        mDotY = y;
+
+        if (mDotY < mBorder) {
+            mDotY = mBorder;
+        }
+
+        if (mDotY > mHeight - mBorder) {
+            mDotY = mHeight - mBorder;
+        }
+        mHSVO[2] = (mDotY - mBorder) / (mHeight - mBorder * 2);
+        notifyColorListeners(mHSVO);
+        setupButton();
+        invalidate((int) (ox - dotRadus), (int) (oy - dotRadus), (int) (ox + dotRadus),
+                (int) (oy + dotRadus));
+        invalidate((int) (mDotX - dotRadus), (int) (mDotY - dotRadus), (int) (mDotX + dotRadus),
+                (int) (mDotY + dotRadus));
+
+        return true;
+    }
+
+    private void setupButton() {
+        float pos = mHSVO[2] * (mHeight - mBorder * 2);
+        mDotY = pos + mBorder;
+
+        int[] colors3 = new int[] {
+                mSliderColor, mSliderColor, 0x66000000, 0 };
+        RadialGradient g = new RadialGradient(mDotX, mDotY, dotRadus, colors3, new float[] {
+        0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+        mDotPaint.setShader(g);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mWidth = w;
+        mHeight = h;
+        mDotX = mWidth / 2;
+        updatePaint();
+        setupButton();
+    }
+
+    private void updatePaint() {
+        float[] hsv = new float[] {
+                mHSVO[0], mHSVO[1], 0f };
+        int color1 = Color.HSVToColor(hsv);
+        hsv[2] = 1;
+        int color2 = Color.HSVToColor(hsv);
+
+        Shader sg = new LinearGradient(mBorder, mBorder, mBorder, mHeight - mBorder, color1, color2,
+                Shader.TileMode.CLAMP);
+        mBarPaint1.setShader(sg);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.drawColor(mBgcolor);
+        canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mBarPaint1);
+        canvas.drawLine(mDotX, mDotY, mDotX, mHeight - mBorder, mLinePaint2);
+        canvas.drawLine(mDotX, mBorder, mDotX, mDotY, mLinePaint1);
+        if (mDotX != Float.NaN) {
+            canvas.drawCircle(mDotX, mDotY, dotRadus, mDotPaint);
+        }
+    }
+
+    @Override
+    public void setColor(float[] hsvo) {
+        System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length);
+
+        float oy = mDotY;
+        updatePaint();
+        setupButton();
+        invalidate();
+
+    }
+
+    ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>();
+
+    public void notifyColorListeners(float[] hsv) {
+        for (ColorListener l : mColorListeners) {
+            l.setColor(hsv);
+        }
+    }
+
+    public void addColorListener(ColorListener l) {
+        mColorListeners.add(l);
+    }
+
+    public void removeColorListener(ColorListener l) {
+        mColorListeners.remove(l);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java b/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java
new file mode 100644
index 0000000..147fb91
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.colorpicker;
+
+public interface RGBListener {
+    void setColor(int hsv);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ActionSlider.java b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java
new file mode 100644
index 0000000..f80a1ca
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class ActionSlider extends TitledSlider {
+    private static final String LOGTAG = "ActionSlider";
+    ImageButton mLeftButton;
+    ImageButton mRightButton;
+    public ActionSlider() {
+        mLayoutID = R.layout.filtershow_control_action_slider;
+    }
+
+    @Override
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+        super.setUp(container, parameter, editor);
+        mLeftButton = (ImageButton) mTopView.findViewById(R.id.leftActionButton);
+        mLeftButton.setOnClickListener(new OnClickListener() {
+
+            @Override
+            public void onClick(View v) {
+                ((ParameterActionAndInt) mParameter).fireLeftAction();
+            }
+        });
+
+        mRightButton = (ImageButton) mTopView.findViewById(R.id.rightActionButton);
+        mRightButton.setOnClickListener(new OnClickListener() {
+
+                @Override
+            public void onClick(View v) {
+                ((ParameterActionAndInt) mParameter).fireRightAction();
+            }
+        });
+        updateUI();
+    }
+
+    @Override
+    public void updateUI() {
+        super.updateUI();
+        if (mLeftButton != null) {
+            int iconId = ((ParameterActionAndInt) mParameter).getLeftIcon();
+            mLeftButton.setImageResource(iconId);
+        }
+        if (mRightButton != null) {
+            int iconId = ((ParameterActionAndInt) mParameter).getRightIcon();
+            mRightButton.setImageResource(iconId);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java
new file mode 100644
index 0000000..92145e9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import android.util.Log;
+
+public class BasicParameterInt implements ParameterInteger {
+    protected String mParameterName;
+    protected Control mControl;
+    protected int mMaximum = 100;
+    protected int mMinimum = 0;
+    protected int mDefaultValue;
+    protected int mValue;
+    public final int ID;
+    protected FilterView mEditor;
+    private final String LOGTAG = "BasicParameterInt";
+
+    @Override
+    public void copyFrom(Parameter src) {
+        if (!(src instanceof BasicParameterInt)) {
+            throw new IllegalArgumentException(src.getClass().getName());
+        }
+        BasicParameterInt p = (BasicParameterInt) src;
+        mMaximum = p.mMaximum;
+        mMinimum = p.mMinimum;
+        mDefaultValue = p.mDefaultValue;
+        mValue = p.mValue;
+    }
+
+    public BasicParameterInt(int id, int value) {
+        ID = id;
+        mValue = value;
+    }
+
+    public BasicParameterInt(int id, int value, int min, int max) {
+        ID = id;
+        mValue = value;
+        mMinimum = min;
+        mMaximum = max;
+    }
+
+    @Override
+    public String getParameterName() {
+        return mParameterName;
+    }
+
+    @Override
+    public String getParameterType() {
+        return sParameterType;
+    }
+
+    @Override
+    public String getValueString() {
+        return mParameterName + mValue;
+    }
+
+    @Override
+    public void setController(Control control) {
+        mControl = control;
+    }
+
+    @Override
+    public int getMaximum() {
+        return mMaximum;
+    }
+
+    @Override
+    public int getMinimum() {
+        return mMinimum;
+    }
+
+    @Override
+    public int getDefaultValue() {
+        return mDefaultValue;
+    }
+
+    @Override
+    public int getValue() {
+        return mValue;
+    }
+
+    @Override
+    public void setValue(int value) {
+        mValue = value;
+        if (mEditor != null) {
+            mEditor.commitLocalRepresentation();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return getValueString();
+    }
+
+    @Override
+    public void setFilterView(FilterView editor) {
+        mEditor = editor;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
new file mode 100644
index 0000000..fb9f95e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import android.content.Context;
+
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public class BasicParameterStyle implements ParameterStyles {
+    protected String mParameterName;
+    protected int mSelectedStyle;
+    protected int mNumberOfStyles;
+    protected int mDefaultStyle = 0;
+    protected Control mControl;
+    protected FilterView mEditor;
+    public final int ID;
+    private final String LOGTAG = "BasicParameterStyle";
+
+    @Override
+    public void copyFrom(Parameter src) {
+        if (!(src instanceof BasicParameterStyle)) {
+            throw new IllegalArgumentException(src.getClass().getName());
+        }
+        BasicParameterStyle p = (BasicParameterStyle) src;
+        mNumberOfStyles = p.mNumberOfStyles;
+        mSelectedStyle = p.mSelectedStyle;
+        mDefaultStyle = p.mDefaultStyle;
+    }
+
+    public BasicParameterStyle(int id, int numberOfStyles) {
+        ID = id;
+        mNumberOfStyles = numberOfStyles;
+    }
+
+    @Override
+    public String getParameterName() {
+        return mParameterName;
+    }
+
+    @Override
+    public String getParameterType() {
+        return sParameterType;
+    }
+
+    @Override
+    public String getValueString() {
+        return mParameterName + mSelectedStyle;
+    }
+
+    @Override
+    public void setController(Control control) {
+        mControl = control;
+    }
+
+    @Override
+    public int getNumberOfStyles() {
+        return mNumberOfStyles;
+    }
+
+    @Override
+    public int getDefaultSelected() {
+        return mDefaultStyle;
+    }
+
+    @Override
+    public int getSelected() {
+        return mSelectedStyle;
+    }
+
+    @Override
+    public void setSelected(int selectedStyle) {
+        mSelectedStyle = selectedStyle;
+        if (mEditor != null) {
+            mEditor.commitLocalRepresentation();
+        }
+    }
+
+    @Override
+    public void getIcon(int index, RenderingRequestCaller caller) {
+        mEditor.computeIcon(index, caller);
+    }
+
+    @Override
+    public String getStyleTitle(int index, Context context) {
+        return "";
+    }
+
+    @Override
+    public String toString() {
+        return getValueString();
+    }
+
+    @Override
+    public void setFilterView(FilterView editor) {
+        mEditor = editor;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
new file mode 100644
index 0000000..9d8278d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class BasicSlider implements Control {
+    private SeekBar mSeekBar;
+    private ParameterInteger mParameter;
+    Editor mEditor;
+
+    @Override
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+        container.removeAllViews();
+        mEditor = editor;
+        Context context = container.getContext();
+        mParameter = (ParameterInteger) parameter;
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        LinearLayout lp = (LinearLayout) inflater.inflate(
+                R.layout.filtershow_seekbar, container, true);
+        mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar);
+
+        updateUI();
+        mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (mParameter != null) {
+                    mParameter.setValue(progress + mParameter.getMinimum());
+                    mEditor.commitLocalRepresentation();
+
+                }
+            }
+        });
+    }
+
+    @Override
+    public View getTopView() {
+        return mSeekBar;
+    }
+
+    @Override
+    public void setPrameter(Parameter parameter) {
+        mParameter = (ParameterInteger) parameter;
+        if (mSeekBar != null) {
+            updateUI();
+        }
+    }
+
+    @Override
+    public void updateUI() {
+        mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
+        mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/Control.java b/src/com/android/gallery3d/filtershow/controller/Control.java
new file mode 100644
index 0000000..4342290
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/Control.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public interface Control {
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor);
+
+    public View getTopView();
+
+    public void setPrameter(Parameter parameter);
+
+    public void updateUI();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/FilterView.java b/src/com/android/gallery3d/filtershow/controller/FilterView.java
new file mode 100644
index 0000000..9ca81dc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/FilterView.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public interface FilterView {
+    public void computeIcon(int index, RenderingRequestCaller caller);
+
+    public void commitLocalRepresentation();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/Parameter.java b/src/com/android/gallery3d/filtershow/controller/Parameter.java
new file mode 100644
index 0000000..8f4d5c0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/Parameter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public interface Parameter {
+    String getParameterName();
+
+    String getParameterType();
+
+    String getValueString();
+
+    public void setController(Control c);
+
+    public void setFilterView(FilterView editor);
+
+    public void copyFrom(Parameter src);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java
new file mode 100644
index 0000000..8a05c3a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+public interface ParameterActionAndInt extends ParameterInteger {
+    static String sParameterType = "ParameterActionAndInt";
+
+    public void fireLeftAction();
+
+    public int getLeftIcon();
+
+    public void fireRightAction();
+
+    public int getRightIcon();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java
new file mode 100644
index 0000000..0bfd201
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+public interface ParameterInteger extends Parameter {
+    static String sParameterType = "ParameterInteger";
+
+    int getMaximum();
+
+    int getMinimum();
+
+    int getDefaultValue();
+
+    int getValue();
+
+    void setValue(int value);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterSet.java b/src/com/android/gallery3d/filtershow/controller/ParameterSet.java
new file mode 100644
index 0000000..6b50a4d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterSet.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+public interface ParameterSet {
+    int getNumberOfParameters();
+
+    Parameter getFilterParameter(int index);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java b/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java
new file mode 100644
index 0000000..7d250a0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import android.content.Context;
+
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public interface ParameterStyles extends Parameter {
+    public static String sParameterType = "ParameterStyles";
+
+    int getNumberOfStyles();
+
+    int getDefaultSelected();
+
+    int getSelected();
+
+    void setSelected(int value);
+
+    void getIcon(int index, RenderingRequestCaller caller);
+
+    String getStyleTitle(int index, Context context);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/StyleChooser.java b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java
new file mode 100644
index 0000000..fb613ab
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java
@@ -0,0 +1,88 @@
+package com.android.gallery3d.filtershow.controller;
+
+import android.app.ActionBar.LayoutParams;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+import java.util.Vector;
+
+public class StyleChooser implements Control {
+    private final String LOGTAG = "StyleChooser";
+    protected ParameterStyles mParameter;
+    protected LinearLayout mLinearLayout;
+    protected Editor mEditor;
+    private View mTopView;
+    private Vector<ImageButton> mIconButton = new Vector<ImageButton>();
+    protected int mLayoutID = R.layout.filtershow_control_style_chooser;
+
+    @Override
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+        container.removeAllViews();
+        mEditor = editor;
+        Context context = container.getContext();
+        mParameter = (ParameterStyles) parameter;
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mTopView = inflater.inflate(mLayoutID, container, true);
+        mLinearLayout = (LinearLayout) mTopView.findViewById(R.id.listStyles);
+        mTopView.setVisibility(View.VISIBLE);
+        int n = mParameter.getNumberOfStyles();
+        mIconButton.clear();
+        LayoutParams lp = new LayoutParams(120, 120);
+        for (int i = 0; i < n; i++) {
+            final ImageButton button = new ImageButton(context);
+            button.setScaleType(ScaleType.CENTER_CROP);
+            button.setLayoutParams(lp);
+            button.setBackgroundResource(android.R.color.transparent);
+            mIconButton.add(button);
+            final int buttonNo = i;
+            button.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View arg0) {
+                    mParameter.setSelected(buttonNo);
+                }
+            });
+            mLinearLayout.addView(button);
+            mParameter.getIcon(i, new RenderingRequestCaller() {
+                @Override
+                public void available(RenderingRequest request) {
+                    Bitmap bmap = request.getBitmap();
+                    if (bmap == null) {
+                        return;
+                    }
+                    button.setImageBitmap(bmap);
+                }
+            });
+        }
+    }
+
+    @Override
+    public View getTopView() {
+        return mTopView;
+    }
+
+    @Override
+    public void setPrameter(Parameter parameter) {
+        mParameter = (ParameterStyles) parameter;
+        updateUI();
+    }
+
+    @Override
+    public void updateUI() {
+        if (mParameter == null) {
+            return;
+        }
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/TitledSlider.java b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
new file mode 100644
index 0000000..f29442b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.controller;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class TitledSlider implements Control {
+    private final String LOGTAG = "ParametricEditor";
+    private SeekBar mSeekBar;
+    private TextView mControlName;
+    private TextView mControlValue;
+    protected ParameterInteger mParameter;
+    Editor mEditor;
+    View mTopView;
+    protected int mLayoutID = R.layout.filtershow_control_title_slider;
+
+    @Override
+    public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+        container.removeAllViews();
+        mEditor = editor;
+        Context context = container.getContext();
+        mParameter = (ParameterInteger) parameter;
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mTopView = inflater.inflate(mLayoutID, container, true);
+        mTopView.setVisibility(View.VISIBLE);
+        mSeekBar = (SeekBar) mTopView.findViewById(R.id.controlValueSeekBar);
+        mControlName = (TextView) mTopView.findViewById(R.id.controlName);
+        mControlValue = (TextView) mTopView.findViewById(R.id.controlValue);
+        updateUI();
+        mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (mParameter != null) {
+                    mParameter.setValue(progress + mParameter.getMinimum());
+                    if (mControlName != null) {
+                        mControlName.setText(mParameter.getParameterName());
+                    }
+                    if (mControlValue != null) {
+                        mControlValue.setText(Integer.toString(mParameter.getValue()));
+                    }
+                    mEditor.commitLocalRepresentation();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setPrameter(Parameter parameter) {
+        mParameter = (ParameterInteger) parameter;
+        if (mSeekBar != null)
+            updateUI();
+    }
+
+    @Override
+    public void updateUI() {
+        if (mControlName != null && mParameter.getParameterName() != null) {
+            mControlName.setText(mParameter.getParameterName().toUpperCase());
+        }
+        if (mControlValue != null) {
+            mControlValue.setText(
+                    Integer.toString(mParameter.getValue()));
+        }
+        mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
+        mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
+        mEditor.commitLocalRepresentation();
+    }
+
+    @Override
+    public View getTopView() {
+        return mTopView;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/BoundedRect.java b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
new file mode 100644
index 0000000..13b8d6d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.crop;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+import java.util.Arrays;
+
+/**
+ * Maintains invariant that inner rectangle is constrained to be within the
+ * outer, rotated rectangle.
+ */
+public class BoundedRect {
+    private float rot;
+    private RectF outer;
+    private RectF inner;
+    private float[] innerRotated;
+
+    public BoundedRect(float rotation, Rect outerRect, Rect innerRect) {
+        rot = rotation;
+        outer = new RectF(outerRect);
+        inner = new RectF(innerRect);
+        innerRotated = CropMath.getCornersFromRect(inner);
+        rotateInner();
+        if (!isConstrained())
+            reconstrain();
+    }
+
+    public BoundedRect(float rotation, RectF outerRect, RectF innerRect) {
+        rot = rotation;
+        outer = new RectF(outerRect);
+        inner = new RectF(innerRect);
+        innerRotated = CropMath.getCornersFromRect(inner);
+        rotateInner();
+        if (!isConstrained())
+            reconstrain();
+    }
+
+    public void resetTo(float rotation, RectF outerRect, RectF innerRect) {
+        rot = rotation;
+        outer.set(outerRect);
+        inner.set(innerRect);
+        innerRotated = CropMath.getCornersFromRect(inner);
+        rotateInner();
+        if (!isConstrained())
+            reconstrain();
+    }
+
+    /**
+     * Sets inner, and re-constrains it to fit within the rotated bounding rect.
+     */
+    public void setInner(RectF newInner) {
+        if (inner.equals(newInner))
+            return;
+        inner = newInner;
+        innerRotated = CropMath.getCornersFromRect(inner);
+        rotateInner();
+        if (!isConstrained())
+            reconstrain();
+    }
+
+    /**
+     * Sets rotation, and re-constrains inner to fit within the rotated bounding rect.
+     */
+    public void setRotation(float rotation) {
+        if (rotation == rot)
+            return;
+        rot = rotation;
+        innerRotated = CropMath.getCornersFromRect(inner);
+        rotateInner();
+        if (!isConstrained())
+            reconstrain();
+    }
+
+    public void setToInner(RectF r) {
+        r.set(inner);
+    }
+
+    public void setToOuter(RectF r) {
+        r.set(outer);
+    }
+
+    public RectF getInner() {
+        return new RectF(inner);
+    }
+
+    public RectF getOuter() {
+        return new RectF(outer);
+    }
+
+    /**
+     * Tries to move the inner rectangle by (dx, dy).  If this would cause it to leave
+     * the bounding rectangle, snaps the inner rectangle to the edge of the bounding
+     * rectangle.
+     */
+    public void moveInner(float dx, float dy) {
+        Matrix m0 = getInverseRotMatrix();
+
+        RectF translatedInner = new RectF(inner);
+        translatedInner.offset(dx, dy);
+
+        float[] translatedInnerCorners = CropMath.getCornersFromRect(translatedInner);
+        float[] outerCorners = CropMath.getCornersFromRect(outer);
+
+        m0.mapPoints(translatedInnerCorners);
+        float[] correction = {
+                0, 0
+        };
+
+        // find correction vectors for corners that have moved out of bounds
+        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+            float correctedInnerX = translatedInnerCorners[i] + correction[0];
+            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+            if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
+                float[] badCorner = {
+                        correctedInnerX, correctedInnerY
+                };
+                float[] nearestSide = CropMath.closestSide(badCorner, outerCorners);
+                float[] correctionVec =
+                        GeometryMathUtils.shortestVectorFromPointToLine(badCorner, nearestSide);
+                correction[0] += correctionVec[0];
+                correction[1] += correctionVec[1];
+            }
+        }
+
+        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+            float correctedInnerX = translatedInnerCorners[i] + correction[0];
+            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+            if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
+                float[] correctionVec = {
+                        correctedInnerX, correctedInnerY
+                };
+                CropMath.getEdgePoints(outer, correctionVec);
+                correctionVec[0] -= correctedInnerX;
+                correctionVec[1] -= correctedInnerY;
+                correction[0] += correctionVec[0];
+                correction[1] += correctionVec[1];
+            }
+        }
+
+        // Set correction
+        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+            float correctedInnerX = translatedInnerCorners[i] + correction[0];
+            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+            // update translated corners with correction vectors
+            translatedInnerCorners[i] = correctedInnerX;
+            translatedInnerCorners[i + 1] = correctedInnerY;
+        }
+
+        innerRotated = translatedInnerCorners;
+        // reconstrain to update inner
+        reconstrain();
+    }
+
+    /**
+     * Attempts to resize the inner rectangle.  If this would cause it to leave
+     * the bounding rect, clips the inner rectangle to fit.
+     */
+    public void resizeInner(RectF newInner) {
+        Matrix m = getRotMatrix();
+        Matrix m0 = getInverseRotMatrix();
+
+        float[] outerCorners = CropMath.getCornersFromRect(outer);
+        m.mapPoints(outerCorners);
+        float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
+        float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
+        RectF ret = new RectF(newInner);
+
+        for (int i = 0; i < newInnerCorners.length; i += 2) {
+            float[] c = {
+                    newInnerCorners[i], newInnerCorners[i + 1]
+            };
+            float[] c0 = Arrays.copyOf(c, 2);
+            m0.mapPoints(c0);
+            if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
+                float[] outerSide = CropMath.closestSide(c, outerCorners);
+                float[] pathOfCorner = {
+                        newInnerCorners[i], newInnerCorners[i + 1],
+                        oldInnerCorners[i], oldInnerCorners[i + 1]
+                };
+                float[] p = GeometryMathUtils.lineIntersect(pathOfCorner, outerSide);
+                if (p == null) {
+                    // lines are parallel or not well defined, so don't resize
+                    p = new float[2];
+                    p[0] = oldInnerCorners[i];
+                    p[1] = oldInnerCorners[i + 1];
+                }
+                // relies on corners being in same order as method
+                // getCornersFromRect
+                switch (i) {
+                    case 0:
+                    case 1:
+                        ret.left = (p[0] > ret.left) ? p[0] : ret.left;
+                        ret.top = (p[1] > ret.top) ? p[1] : ret.top;
+                        break;
+                    case 2:
+                    case 3:
+                        ret.right = (p[0] < ret.right) ? p[0] : ret.right;
+                        ret.top = (p[1] > ret.top) ? p[1] : ret.top;
+                        break;
+                    case 4:
+                    case 5:
+                        ret.right = (p[0] < ret.right) ? p[0] : ret.right;
+                        ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
+                        break;
+                    case 6:
+                    case 7:
+                        ret.left = (p[0] > ret.left) ? p[0] : ret.left;
+                        ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+        float[] retCorners = CropMath.getCornersFromRect(ret);
+        m0.mapPoints(retCorners);
+        innerRotated = retCorners;
+        // reconstrain to update inner
+        reconstrain();
+    }
+
+    /**
+     * Attempts to resize the inner rectangle.  If this would cause it to leave
+     * the bounding rect, clips the inner rectangle to fit while maintaining
+     * aspect ratio.
+     */
+    public void fixedAspectResizeInner(RectF newInner) {
+        Matrix m = getRotMatrix();
+        Matrix m0 = getInverseRotMatrix();
+
+        float aspectW = inner.width();
+        float aspectH = inner.height();
+        float aspRatio = aspectW / aspectH;
+        float[] corners = CropMath.getCornersFromRect(outer);
+
+        m.mapPoints(corners);
+        float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
+        float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
+
+        // find fixed corner
+        int fixed = -1;
+        if (inner.top == newInner.top) {
+            if (inner.left == newInner.left)
+                fixed = 0; // top left
+            else if (inner.right == newInner.right)
+                fixed = 2; // top right
+        } else if (inner.bottom == newInner.bottom) {
+            if (inner.right == newInner.right)
+                fixed = 4; // bottom right
+            else if (inner.left == newInner.left)
+                fixed = 6; // bottom left
+        }
+        // no fixed corner, return without update
+        if (fixed == -1)
+            return;
+        float widthSoFar = newInner.width();
+        int moved = -1;
+        for (int i = 0; i < newInnerCorners.length; i += 2) {
+            float[] c = {
+                    newInnerCorners[i], newInnerCorners[i + 1]
+            };
+            float[] c0 = Arrays.copyOf(c, 2);
+            m0.mapPoints(c0);
+            if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
+                moved = i;
+                if (moved == fixed)
+                    continue;
+                float[] l2 = CropMath.closestSide(c, corners);
+                float[] l1 = {
+                        newInnerCorners[i], newInnerCorners[i + 1],
+                        oldInnerCorners[i], oldInnerCorners[i + 1]
+                };
+                float[] p = GeometryMathUtils.lineIntersect(l1, l2);
+                if (p == null) {
+                    // lines are parallel or not well defined, so set to old
+                    // corner
+                    p = new float[2];
+                    p[0] = oldInnerCorners[i];
+                    p[1] = oldInnerCorners[i + 1];
+                }
+                // relies on corners being in same order as method
+                // getCornersFromRect
+                float fixed_x = oldInnerCorners[fixed];
+                float fixed_y = oldInnerCorners[fixed + 1];
+                float newWidth = Math.abs(fixed_x - p[0]);
+                float newHeight = Math.abs(fixed_y - p[1]);
+                newWidth = Math.max(newWidth, aspRatio * newHeight);
+                if (newWidth < widthSoFar)
+                    widthSoFar = newWidth;
+            }
+        }
+
+        float heightSoFar = widthSoFar / aspRatio;
+        RectF ret = new RectF(inner);
+        if (fixed == 0) {
+            ret.right = ret.left + widthSoFar;
+            ret.bottom = ret.top + heightSoFar;
+        } else if (fixed == 2) {
+            ret.left = ret.right - widthSoFar;
+            ret.bottom = ret.top + heightSoFar;
+        } else if (fixed == 4) {
+            ret.left = ret.right - widthSoFar;
+            ret.top = ret.bottom - heightSoFar;
+        } else if (fixed == 6) {
+            ret.right = ret.left + widthSoFar;
+            ret.top = ret.bottom - heightSoFar;
+        }
+        float[] retCorners = CropMath.getCornersFromRect(ret);
+        m0.mapPoints(retCorners);
+        innerRotated = retCorners;
+        // reconstrain to update inner
+        reconstrain();
+    }
+
+    // internal methods
+
+    private boolean isConstrained() {
+        for (int i = 0; i < 8; i += 2) {
+            if (!CropMath.inclusiveContains(outer, innerRotated[i], innerRotated[i + 1]))
+                return false;
+        }
+        return true;
+    }
+
+    private void reconstrain() {
+        // innerRotated has been changed to have incorrect values
+        CropMath.getEdgePoints(outer, innerRotated);
+        Matrix m = getRotMatrix();
+        float[] unrotated = Arrays.copyOf(innerRotated, 8);
+        m.mapPoints(unrotated);
+        inner = CropMath.trapToRect(unrotated);
+    }
+
+    private void rotateInner() {
+        Matrix m = getInverseRotMatrix();
+        m.mapPoints(innerRotated);
+    }
+
+    private Matrix getRotMatrix() {
+        Matrix m = new Matrix();
+        m.setRotate(rot, outer.centerX(), outer.centerY());
+        return m;
+    }
+
+    private Matrix getInverseRotMatrix() {
+        Matrix m = new Matrix();
+        m.setRotate(-rot, outer.centerX(), outer.centerY());
+        return m;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
new file mode 100644
index 0000000..0a0c367
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.crop;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Activity for cropping an image.
+ */
+public class CropActivity extends Activity {
+    private static final String LOGTAG = "CropActivity";
+    public static final String CROP_ACTION = "com.android.camera.action.CROP";
+    private CropExtras mCropExtras = null;
+    private LoadBitmapTask mLoadBitmapTask = null;
+
+    private int mOutputX = 0;
+    private int mOutputY = 0;
+    private Bitmap mOriginalBitmap = null;
+    private RectF mOriginalBounds = null;
+    private int mOriginalRotation = 0;
+    private Uri mSourceUri = null;
+    private CropView mCropView = null;
+    private View mSaveButton = null;
+    private boolean finalIOGuard = false;
+
+    private static final int SELECT_PICTURE = 1; // request code for picker
+
+    private static final int DEFAULT_COMPRESS_QUALITY = 90;
+    /**
+     * The maximum bitmap size we allow to be returned through the intent.
+     * Intents have a maximum of 1MB in total size. However, the Bitmap seems to
+     * have some overhead to hit so that we go way below the limit here to make
+     * sure the intent stays below 1MB.We should consider just returning a byte
+     * array instead of a Bitmap instance to avoid overhead.
+     */
+    public static final int MAX_BMAP_IN_INTENT = 750000;
+
+    // Flags
+    private static final int DO_SET_WALLPAPER = 1;
+    private static final int DO_RETURN_DATA = 1 << 1;
+    private static final int DO_EXTRA_OUTPUT = 1 << 2;
+
+    private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        setResult(RESULT_CANCELED, new Intent());
+        mCropExtras = getExtrasFromIntent(intent);
+        if (mCropExtras != null && mCropExtras.getShowWhenLocked()) {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        }
+
+        setContentView(R.layout.crop_activity);
+        mCropView = (CropView) findViewById(R.id.cropView);
+
+        ActionBar actionBar = getActionBar();
+        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+        actionBar.setCustomView(R.layout.filtershow_actionbar);
+
+        View mSaveButton = actionBar.getCustomView();
+        mSaveButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                startFinishOutput();
+            }
+        });
+
+        if (intent.getData() != null) {
+            mSourceUri = intent.getData();
+            startLoadBitmap(mSourceUri);
+        } else {
+            pickImage();
+        }
+    }
+
+    private void enableSave(boolean enable) {
+        if (mSaveButton != null) {
+            mSaveButton.setEnabled(enable);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mLoadBitmapTask != null) {
+            mLoadBitmapTask.cancel(false);
+        }
+        super.onDestroy();
+    }
+
+    @Override
+    public void onConfigurationChanged (Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mCropView.configChanged();
+    }
+
+    /**
+     * Opens a selector in Gallery to chose an image for use when none was given
+     * in the CROP intent.
+     */
+    private void pickImage() {
+        Intent intent = new Intent();
+        intent.setType("image/*");
+        intent.setAction(Intent.ACTION_GET_CONTENT);
+        startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
+                SELECT_PICTURE);
+    }
+
+    /**
+     * Callback for pickImage().
+     */
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) {
+            mSourceUri = data.getData();
+            startLoadBitmap(mSourceUri);
+        }
+    }
+
+    /**
+     * Gets screen size metric.
+     */
+    private int getScreenImageSize() {
+        DisplayMetrics outMetrics = new DisplayMetrics();
+        getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
+        return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
+    }
+
+    /**
+     * Method that loads a bitmap in an async task.
+     */
+    private void startLoadBitmap(Uri uri) {
+        if (uri != null) {
+            enableSave(false);
+            final View loading = findViewById(R.id.loading);
+            loading.setVisibility(View.VISIBLE);
+            mLoadBitmapTask = new LoadBitmapTask();
+            mLoadBitmapTask.execute(uri);
+        } else {
+            cannotLoadImage();
+            done();
+        }
+    }
+
+    /**
+     * Method called on UI thread with loaded bitmap.
+     */
+    private void doneLoadBitmap(Bitmap bitmap, RectF bounds, int orientation) {
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.GONE);
+        mOriginalBitmap = bitmap;
+        mOriginalBounds = bounds;
+        mOriginalRotation = orientation;
+        if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) {
+            RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
+            mCropView.initialize(bitmap, imgBounds, imgBounds, orientation);
+            if (mCropExtras != null) {
+                int aspectX = mCropExtras.getAspectX();
+                int aspectY = mCropExtras.getAspectY();
+                mOutputX = mCropExtras.getOutputX();
+                mOutputY = mCropExtras.getOutputY();
+                if (mOutputX > 0 && mOutputY > 0) {
+                    mCropView.applyAspect(mOutputX, mOutputY);
+
+                }
+                float spotX = mCropExtras.getSpotlightX();
+                float spotY = mCropExtras.getSpotlightY();
+                if (spotX > 0 && spotY > 0) {
+                    mCropView.setWallpaperSpotlight(spotX, spotY);
+                }
+                if (aspectX > 0 && aspectY > 0) {
+                    mCropView.applyAspect(aspectX, aspectY);
+                }
+            }
+            enableSave(true);
+        } else {
+            Log.w(LOGTAG, "could not load image for cropping");
+            cannotLoadImage();
+            setResult(RESULT_CANCELED, new Intent());
+            done();
+        }
+    }
+
+    /**
+     * Display toast for image loading failure.
+     */
+    private void cannotLoadImage() {
+        CharSequence text = getString(R.string.cannot_load_image);
+        Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
+        toast.show();
+    }
+
+    /**
+     * AsyncTask for loading a bitmap into memory.
+     *
+     * @see #startLoadBitmap(Uri)
+     * @see #doneLoadBitmap(Bitmap)
+     */
+    private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> {
+        int mBitmapSize;
+        Context mContext;
+        Rect mOriginalBounds;
+        int mOrientation;
+
+        public LoadBitmapTask() {
+            mBitmapSize = getScreenImageSize();
+            mContext = getApplicationContext();
+            mOriginalBounds = new Rect();
+            mOrientation = 0;
+        }
+
+        @Override
+        protected Bitmap doInBackground(Uri... params) {
+            Uri uri = params[0];
+            Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize,
+                    mOriginalBounds, false);
+            mOrientation = ImageLoader.getMetadataRotation(mContext, uri);
+            return bmap;
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap result) {
+            doneLoadBitmap(result, new RectF(mOriginalBounds), mOrientation);
+        }
+    }
+
+    private void startFinishOutput() {
+        if (finalIOGuard) {
+            return;
+        } else {
+            finalIOGuard = true;
+        }
+        enableSave(false);
+        Uri destinationUri = null;
+        int flags = 0;
+        if (mOriginalBitmap != null && mCropExtras != null) {
+            if (mCropExtras.getExtraOutput() != null) {
+                destinationUri = mCropExtras.getExtraOutput();
+                if (destinationUri != null) {
+                    flags |= DO_EXTRA_OUTPUT;
+                }
+            }
+            if (mCropExtras.getSetAsWallpaper()) {
+                flags |= DO_SET_WALLPAPER;
+            }
+            if (mCropExtras.getReturnData()) {
+                flags |= DO_RETURN_DATA;
+            }
+        }
+        if (flags == 0) {
+            destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri);
+            if (destinationUri != null) {
+                flags |= DO_EXTRA_OUTPUT;
+            }
+        }
+        if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) {
+            RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
+            RectF crop = getBitmapCrop(photo);
+            startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop,
+                    photo, mOriginalBounds,
+                    (mCropExtras == null) ? null : mCropExtras.getOutputFormat(), mOriginalRotation);
+            return;
+        }
+        setResult(RESULT_CANCELED, new Intent());
+        done();
+        return;
+    }
+
+    private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri,
+            RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format,
+            int rotation) {
+        if (cropBounds == null || photoBounds == null || currentBitmap == null
+                || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0
+                || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0
+                || photoBounds.height() == 0) {
+            return; // fail fast
+        }
+        if ((flags & FLAG_CHECK) == 0) {
+            return; // no output options
+        }
+        if ((flags & DO_SET_WALLPAPER) != 0) {
+            Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
+        }
+
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.VISIBLE);
+        BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds,
+                photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY);
+        ioTask.execute(currentBitmap);
+    }
+
+    private void doneBitmapIO(boolean success, Intent intent) {
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.GONE);
+        if (success) {
+            setResult(RESULT_OK, intent);
+        } else {
+            setResult(RESULT_CANCELED, intent);
+        }
+        done();
+    }
+
+    private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> {
+
+        private final WallpaperManager mWPManager;
+        InputStream mInStream = null;
+        OutputStream mOutStream = null;
+        String mOutputFormat = null;
+        Uri mOutUri = null;
+        Uri mInUri = null;
+        int mFlags = 0;
+        RectF mCrop = null;
+        RectF mPhoto = null;
+        RectF mOrig = null;
+        Intent mResultIntent = null;
+        int mRotation = 0;
+
+        // Helper to setup input stream
+        private void regenerateInputStream() {
+            if (mInUri == null) {
+                Log.w(LOGTAG, "cannot read original file, no input URI given");
+            } else {
+                Utils.closeSilently(mInStream);
+                try {
+                    mInStream = getContentResolver().openInputStream(mInUri);
+                } catch (FileNotFoundException e) {
+                    Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+                }
+            }
+        }
+
+        public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
+                RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation,
+                int outputX, int outputY) {
+            mOutputFormat = outputFormat;
+            mOutStream = null;
+            mOutUri = destUri;
+            mInUri = sourceUri;
+            mFlags = flags;
+            mCrop = cropBounds;
+            mPhoto = photoBounds;
+            mOrig = originalBitmapBounds;
+            mWPManager = WallpaperManager.getInstance(getApplicationContext());
+            mResultIntent = new Intent();
+            mRotation = (rotation < 0) ? -rotation : rotation;
+            mRotation %= 360;
+            mRotation = 90 * (int) (mRotation / 90);  // now mRotation is a multiple of 90
+            mOutputX = outputX;
+            mOutputY = outputY;
+
+            if ((flags & DO_EXTRA_OUTPUT) != 0) {
+                if (mOutUri == null) {
+                    Log.w(LOGTAG, "cannot write file, no output URI given");
+                } else {
+                    try {
+                        mOutStream = getContentResolver().openOutputStream(mOutUri);
+                    } catch (FileNotFoundException e) {
+                        Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e);
+                    }
+                }
+            }
+
+            if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
+                regenerateInputStream();
+            }
+        }
+
+        @Override
+        protected Boolean doInBackground(Bitmap... params) {
+            boolean failure = false;
+            Bitmap img = params[0];
+
+            // Set extra for crop bounds
+            if (mCrop != null && mPhoto != null && mOrig != null) {
+                RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
+                Matrix m = new Matrix();
+                m.setRotate(mRotation);
+                m.mapRect(trueCrop);
+                if (trueCrop != null) {
+                    Rect rounded = new Rect();
+                    trueCrop.roundOut(rounded);
+                    mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded);
+                }
+            }
+
+            // Find the small cropped bitmap that is returned in the intent
+            if ((mFlags & DO_RETURN_DATA) != 0) {
+                assert (img != null);
+                Bitmap ret = getCroppedImage(img, mCrop, mPhoto);
+                if (ret != null) {
+                    ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT);
+                }
+                if (ret == null) {
+                    Log.w(LOGTAG, "could not downsample bitmap to return in data");
+                    failure = true;
+                } else {
+                    if (mRotation > 0) {
+                        Matrix m = new Matrix();
+                        m.setRotate(mRotation);
+                        Bitmap tmp = Bitmap.createBitmap(ret, 0, 0, ret.getWidth(),
+                                ret.getHeight(), m, true);
+                        if (tmp != null) {
+                            ret = tmp;
+                        }
+                    }
+                    mResultIntent.putExtra(CropExtras.KEY_DATA, ret);
+                }
+            }
+
+            // Do the large cropped bitmap and/or set the wallpaper
+            if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
+                // Find crop bounds (scaled to original image size)
+                RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
+                if (trueCrop == null) {
+                    Log.w(LOGTAG, "cannot find crop for full size image");
+                    failure = true;
+                    return false;
+                }
+                Rect roundedTrueCrop = new Rect();
+                trueCrop.roundOut(roundedTrueCrop);
+
+                if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
+                    Log.w(LOGTAG, "crop has bad values for full size image");
+                    failure = true;
+                    return false;
+                }
+
+                // Attempt to open a region decoder
+                BitmapRegionDecoder decoder = null;
+                try {
+                    decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+                } catch (IOException e) {
+                    Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+                }
+
+                Bitmap crop = null;
+                if (decoder != null) {
+                    // Do region decoding to get crop bitmap
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    options.inMutable = true;
+                    crop = decoder.decodeRegion(roundedTrueCrop, options);
+                    decoder.recycle();
+                }
+
+                if (crop == null) {
+                    // BitmapRegionDecoder has failed, try to crop in-memory
+                    regenerateInputStream();
+                    Bitmap fullSize = null;
+                    if (mInStream != null) {
+                        fullSize = BitmapFactory.decodeStream(mInStream);
+                    }
+                    if (fullSize != null) {
+                        crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+                                roundedTrueCrop.top, roundedTrueCrop.width(),
+                                roundedTrueCrop.height());
+                    }
+                }
+
+                if (crop == null) {
+                    Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
+                    failure = true;
+                    return false;
+                }
+                if (mOutputX > 0 && mOutputY > 0) {
+                    Matrix m = new Matrix();
+                    RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
+                    if (mRotation > 0) {
+                        m.setRotate(mRotation);
+                        m.mapRect(cropRect);
+                    }
+                    RectF returnRect = new RectF(0, 0, mOutputX, mOutputY);
+                    m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+                    m.preRotate(mRotation);
+                    Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+                            (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+                    if (tmp != null) {
+                        Canvas c = new Canvas(tmp);
+                        c.drawBitmap(crop, m, new Paint());
+                        crop = tmp;
+                    }
+                } else if (mRotation > 0) {
+                    Matrix m = new Matrix();
+                    m.setRotate(mRotation);
+                    Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
+                            crop.getHeight(), m, true);
+                    if (tmp != null) {
+                        crop = tmp;
+                    }
+                }
+                // Get output compression format
+                CompressFormat cf =
+                        convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
+
+                // If we only need to output to a URI, compress straight to file
+                if (mFlags == DO_EXTRA_OUTPUT) {
+                    if (mOutStream == null
+                            || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) {
+                        Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString());
+                        failure = true;
+                    } else {
+                        mResultIntent.setData(mOutUri);
+                    }
+                } else {
+                    // Compress to byte array
+                    ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
+                    if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
+
+                        // If we need to output to a Uri, write compressed
+                        // bitmap out
+                        if ((mFlags & DO_EXTRA_OUTPUT) != 0) {
+                            if (mOutStream == null) {
+                                Log.w(LOGTAG,
+                                        "failed to compress bitmap to file: " + mOutUri.toString());
+                                failure = true;
+                            } else {
+                                try {
+                                    mOutStream.write(tmpOut.toByteArray());
+                                    mResultIntent.setData(mOutUri);
+                                } catch (IOException e) {
+                                    Log.w(LOGTAG,
+                                            "failed to compress bitmap to file: "
+                                                    + mOutUri.toString(), e);
+                                    failure = true;
+                                }
+                            }
+                        }
+
+                        // If we need to set to the wallpaper, set it
+                        if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) {
+                            if (mWPManager == null) {
+                                Log.w(LOGTAG, "no wallpaper manager");
+                                failure = true;
+                            } else {
+                                try {
+                                    mWPManager.setStream(new ByteArrayInputStream(tmpOut
+                                            .toByteArray()));
+                                } catch (IOException e) {
+                                    Log.w(LOGTAG, "cannot write stream to wallpaper", e);
+                                    failure = true;
+                                }
+                            }
+                        }
+                    } else {
+                        Log.w(LOGTAG, "cannot compress bitmap");
+                        failure = true;
+                    }
+                }
+            }
+            return !failure; // True if any of the operations failed
+        }
+
+        @Override
+        protected void onPostExecute(Boolean result) {
+            Utils.closeSilently(mOutStream);
+            Utils.closeSilently(mInStream);
+            doneBitmapIO(result.booleanValue(), mResultIntent);
+        }
+
+    }
+
+    private void done() {
+        finish();
+    }
+
+    protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) {
+        RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight());
+        RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds);
+        if (crop == null) {
+            return null;
+        }
+        Rect intCrop = new Rect();
+        crop.roundOut(intCrop);
+        return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(),
+                intCrop.height());
+    }
+
+    protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) {
+        if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) {
+            throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()");
+        }
+        int shifts = 0;
+        int size = CropMath.getBitmapSize(image);
+        while (size > max_size) {
+            shifts++;
+            size /= 4;
+        }
+        Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts,
+                image.getHeight() >> shifts, true);
+        if (ret == null) {
+            return null;
+        }
+        // Handle edge case for rounding.
+        if (CropMath.getBitmapSize(ret) > max_size) {
+            return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true);
+        }
+        return ret;
+    }
+
+    /**
+     * Gets the crop extras from the intent, or null if none exist.
+     */
+    protected static CropExtras getExtrasFromIntent(Intent intent) {
+        Bundle extras = intent.getExtras();
+        if (extras != null) {
+            return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0),
+                    extras.getInt(CropExtras.KEY_OUTPUT_Y, 0),
+                    extras.getBoolean(CropExtras.KEY_SCALE, true) &&
+                            extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false),
+                    extras.getInt(CropExtras.KEY_ASPECT_X, 0),
+                    extras.getInt(CropExtras.KEY_ASPECT_Y, 0),
+                    extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false),
+                    extras.getBoolean(CropExtras.KEY_RETURN_DATA, false),
+                    (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT),
+                    extras.getString(CropExtras.KEY_OUTPUT_FORMAT),
+                    extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false),
+                    extras.getFloat(CropExtras.KEY_SPOTLIGHT_X),
+                    extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y));
+        }
+        return null;
+    }
+
+    protected static CompressFormat convertExtensionToCompressFormat(String extension) {
+        return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
+    }
+
+    protected static String getFileExtension(String requestFormat) {
+        String outputFormat = (requestFormat == null)
+                ? "jpg"
+                : requestFormat;
+        outputFormat = outputFormat.toLowerCase();
+        return (outputFormat.equals("png") || outputFormat.equals("gif"))
+                ? "png" // We don't support gif compression.
+                : "jpg";
+    }
+
+    private RectF getBitmapCrop(RectF imageBounds) {
+        RectF crop = mCropView.getCrop();
+        RectF photo = mCropView.getPhoto();
+        if (crop == null || photo == null) {
+            Log.w(LOGTAG, "could not get crop");
+            return null;
+        }
+        RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds);
+        return scaledCrop;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java
new file mode 100644
index 0000000..b0d324c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.crop;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+
+public abstract class CropDrawingUtils {
+
+    public static void drawRuleOfThird(Canvas canvas, RectF bounds) {
+        Paint p = new Paint();
+        p.setStyle(Paint.Style.STROKE);
+        p.setColor(Color.argb(128, 255, 255, 255));
+        p.setStrokeWidth(2);
+        float stepX = bounds.width() / 3.0f;
+        float stepY = bounds.height() / 3.0f;
+        float x = bounds.left + stepX;
+        float y = bounds.top + stepY;
+        for (int i = 0; i < 2; i++) {
+            canvas.drawLine(x, bounds.top, x, bounds.bottom, p);
+            x += stepX;
+        }
+        for (int j = 0; j < 2; j++) {
+            canvas.drawLine(bounds.left, y, bounds.right, y, p);
+            y += stepY;
+        }
+    }
+
+    public static void drawCropRect(Canvas canvas, RectF bounds) {
+        Paint p = new Paint();
+        p.setStyle(Paint.Style.STROKE);
+        p.setColor(Color.WHITE);
+        p.setStrokeWidth(3);
+        canvas.drawRect(bounds, p);
+    }
+
+    public static void drawIndicator(Canvas canvas, Drawable indicator, int indicatorSize,
+            float centerX, float centerY) {
+        int left = (int) centerX - indicatorSize / 2;
+        int top = (int) centerY - indicatorSize / 2;
+        indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize);
+        indicator.draw(canvas);
+    }
+
+    public static void drawIndicators(Canvas canvas, Drawable cropIndicator, int indicatorSize,
+            RectF bounds, boolean fixedAspect, int selection) {
+        boolean notMoving = (selection == CropObject.MOVE_NONE);
+        if (fixedAspect) {
+            if ((selection == CropObject.TOP_LEFT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.top);
+            }
+            if ((selection == CropObject.TOP_RIGHT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.top);
+            }
+            if ((selection == CropObject.BOTTOM_LEFT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.bottom);
+            }
+            if ((selection == CropObject.BOTTOM_RIGHT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.bottom);
+            }
+        } else {
+            if (((selection & CropObject.MOVE_TOP) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.top);
+            }
+            if (((selection & CropObject.MOVE_BOTTOM) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.bottom);
+            }
+            if (((selection & CropObject.MOVE_LEFT) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.centerY());
+            }
+            if (((selection & CropObject.MOVE_RIGHT) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.centerY());
+            }
+        }
+    }
+
+    public static void drawWallpaperSelectionFrame(Canvas canvas, RectF cropBounds, float spotX,
+            float spotY, Paint p, Paint shadowPaint) {
+        float sx = cropBounds.width() * spotX;
+        float sy = cropBounds.height() * spotY;
+        float cx = cropBounds.centerX();
+        float cy = cropBounds.centerY();
+        RectF r1 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2);
+        float temp = sx;
+        sx = sy;
+        sy = temp;
+        RectF r2 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2);
+        canvas.save();
+        canvas.clipRect(cropBounds);
+        canvas.clipRect(r1, Region.Op.DIFFERENCE);
+        canvas.clipRect(r2, Region.Op.DIFFERENCE);
+        canvas.drawPaint(shadowPaint);
+        canvas.restore();
+        Path path = new Path();
+        path.moveTo(r1.left, r1.top);
+        path.lineTo(r1.right, r1.top);
+        path.moveTo(r1.left, r1.top);
+        path.lineTo(r1.left, r1.bottom);
+        path.moveTo(r1.left, r1.bottom);
+        path.lineTo(r1.right, r1.bottom);
+        path.moveTo(r1.right, r1.top);
+        path.lineTo(r1.right, r1.bottom);
+        path.moveTo(r2.left, r2.top);
+        path.lineTo(r2.right, r2.top);
+        path.moveTo(r2.right, r2.top);
+        path.lineTo(r2.right, r2.bottom);
+        path.moveTo(r2.left, r2.bottom);
+        path.lineTo(r2.right, r2.bottom);
+        path.moveTo(r2.left, r2.top);
+        path.lineTo(r2.left, r2.bottom);
+        canvas.drawPath(path, p);
+    }
+
+    public static void drawShadows(Canvas canvas, Paint p, RectF innerBounds, RectF outerBounds) {
+        canvas.drawRect(outerBounds.left, outerBounds.top, innerBounds.right, innerBounds.top, p);
+        canvas.drawRect(innerBounds.right, outerBounds.top, outerBounds.right, innerBounds.bottom,
+                p);
+        canvas.drawRect(innerBounds.left, innerBounds.bottom, outerBounds.right,
+                outerBounds.bottom, p);
+        canvas.drawRect(outerBounds.left, innerBounds.top, innerBounds.left, outerBounds.bottom, p);
+    }
+
+    public static Matrix getBitmapToDisplayMatrix(RectF imageBounds, RectF displayBounds) {
+        Matrix m = new Matrix();
+        CropDrawingUtils.setBitmapToDisplayMatrix(m, imageBounds, displayBounds);
+        return m;
+    }
+
+    public static boolean setBitmapToDisplayMatrix(Matrix m, RectF imageBounds,
+            RectF displayBounds) {
+        m.reset();
+        return m.setRectToRect(imageBounds, displayBounds, Matrix.ScaleToFit.CENTER);
+    }
+
+    public static boolean setImageToScreenMatrix(Matrix dst, RectF image,
+            RectF screen, int rotation) {
+        RectF rotatedImage = new RectF();
+        dst.setRotate(rotation, image.centerX(), image.centerY());
+        if (!dst.mapRect(rotatedImage, image)) {
+            return false; // fails for rotations that are not multiples of 90
+                          // degrees
+        }
+        boolean rToR = dst.setRectToRect(rotatedImage, screen, Matrix.ScaleToFit.CENTER);
+        boolean rot = dst.preRotate(rotation, image.centerX(), image.centerY());
+        return rToR && rot;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropExtras.java b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
new file mode 100644
index 0000000..60fe9af
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.crop;
+
+import android.net.Uri;
+
+public class CropExtras {
+
+    public static final String KEY_CROPPED_RECT = "cropped-rect";
+    public static final String KEY_OUTPUT_X = "outputX";
+    public static final String KEY_OUTPUT_Y = "outputY";
+    public static final String KEY_SCALE = "scale";
+    public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
+    public static final String KEY_ASPECT_X = "aspectX";
+    public static final String KEY_ASPECT_Y = "aspectY";
+    public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
+    public static final String KEY_RETURN_DATA = "return-data";
+    public static final String KEY_DATA = "data";
+    public static final String KEY_SPOTLIGHT_X = "spotlightX";
+    public static final String KEY_SPOTLIGHT_Y = "spotlightY";
+    public static final String KEY_SHOW_WHEN_LOCKED = "showWhenLocked";
+    public static final String KEY_OUTPUT_FORMAT = "outputFormat";
+
+    private int mOutputX = 0;
+    private int mOutputY = 0;
+    private boolean mScaleUp = true;
+    private int mAspectX = 0;
+    private int mAspectY = 0;
+    private boolean mSetAsWallpaper = false;
+    private boolean mReturnData = false;
+    private Uri mExtraOutput = null;
+    private String mOutputFormat = null;
+    private boolean mShowWhenLocked = false;
+    private float mSpotlightX = 0;
+    private float mSpotlightY = 0;
+
+    public CropExtras(int outputX, int outputY, boolean scaleUp, int aspectX, int aspectY,
+            boolean setAsWallpaper, boolean returnData, Uri extraOutput, String outputFormat,
+            boolean showWhenLocked, float spotlightX, float spotlightY) {
+        mOutputX = outputX;
+        mOutputY = outputY;
+        mScaleUp = scaleUp;
+        mAspectX = aspectX;
+        mAspectY = aspectY;
+        mSetAsWallpaper = setAsWallpaper;
+        mReturnData = returnData;
+        mExtraOutput = extraOutput;
+        mOutputFormat = outputFormat;
+        mShowWhenLocked = showWhenLocked;
+        mSpotlightX = spotlightX;
+        mSpotlightY = spotlightY;
+    }
+
+    public CropExtras(CropExtras c) {
+        this(c.mOutputX, c.mOutputY, c.mScaleUp, c.mAspectX, c.mAspectY, c.mSetAsWallpaper,
+                c.mReturnData, c.mExtraOutput, c.mOutputFormat, c.mShowWhenLocked,
+                c.mSpotlightX, c.mSpotlightY);
+    }
+
+    public int getOutputX() {
+        return mOutputX;
+    }
+
+    public int getOutputY() {
+        return mOutputY;
+    }
+
+    public boolean getScaleUp() {
+        return mScaleUp;
+    }
+
+    public int getAspectX() {
+        return mAspectX;
+    }
+
+    public int getAspectY() {
+        return mAspectY;
+    }
+
+    public boolean getSetAsWallpaper() {
+        return mSetAsWallpaper;
+    }
+
+    public boolean getReturnData() {
+        return mReturnData;
+    }
+
+    public Uri getExtraOutput() {
+        return mExtraOutput;
+    }
+
+    public String getOutputFormat() {
+        return mOutputFormat;
+    }
+
+    public boolean getShowWhenLocked() {
+        return mShowWhenLocked;
+    }
+
+    public float getSpotlightX() {
+        return mSpotlightX;
+    }
+
+    public float getSpotlightY() {
+        return mSpotlightY;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java
new file mode 100644
index 0000000..02c6531
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.crop;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+import java.util.Arrays;
+
+public class CropMath {
+
+    /**
+     * Gets a float array of the 2D coordinates representing a rectangles
+     * corners.
+     * The order of the corners in the float array is:
+     * 0------->1
+     * ^        |
+     * |        v
+     * 3<-------2
+     *
+     * @param r  the rectangle to get the corners of
+     * @return  the float array of corners (8 floats)
+     */
+
+    public static float[] getCornersFromRect(RectF r) {
+        float[] corners = {
+                r.left, r.top,
+                r.right, r.top,
+                r.right, r.bottom,
+                r.left, r.bottom
+        };
+        return corners;
+    }
+
+    /**
+     * Returns true iff point (x, y) is within or on the rectangle's bounds.
+     * RectF's "contains" function treats points on the bottom and right bound
+     * as not being contained.
+     *
+     * @param r the rectangle
+     * @param x the x value of the point
+     * @param y the y value of the point
+     * @return
+     */
+    public static boolean inclusiveContains(RectF r, float x, float y) {
+        return !(x > r.right || x < r.left || y > r.bottom || y < r.top);
+    }
+
+    /**
+     * Takes an array of 2D coordinates representing corners and returns the
+     * smallest rectangle containing those coordinates.
+     *
+     * @param array array of 2D coordinates
+     * @return smallest rectangle containing coordinates
+     */
+    public static RectF trapToRect(float[] array) {
+        RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+                Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+        for (int i = 1; i < array.length; i += 2) {
+            float x = array[i - 1];
+            float y = array[i];
+            r.left = (x < r.left) ? x : r.left;
+            r.top = (y < r.top) ? y : r.top;
+            r.right = (x > r.right) ? x : r.right;
+            r.bottom = (y > r.bottom) ? y : r.bottom;
+        }
+        r.sort();
+        return r;
+    }
+
+    /**
+     * If edge point [x, y] in array [x0, y0, x1, y1, ...] is outside of the
+     * image bound rectangle, clamps it to the edge of the rectangle.
+     *
+     * @param imageBound the rectangle to clamp edge points to.
+     * @param array an array of points to clamp to the rectangle, gets set to
+     *            the clamped values.
+     */
+    public static void getEdgePoints(RectF imageBound, float[] array) {
+        if (array.length < 2)
+            return;
+        for (int x = 0; x < array.length; x += 2) {
+            array[x] = GeometryMathUtils.clamp(array[x], imageBound.left, imageBound.right);
+            array[x + 1] = GeometryMathUtils.clamp(array[x + 1], imageBound.top, imageBound.bottom);
+        }
+    }
+
+    /**
+     * Takes a point and the corners of a rectangle and returns the two corners
+     * representing the side of the rectangle closest to the point.
+     *
+     * @param point the point which is being checked
+     * @param corners the corners of the rectangle
+     * @return two corners representing the side of the rectangle
+     */
+    public static float[] closestSide(float[] point, float[] corners) {
+        int len = corners.length;
+        float oldMag = Float.POSITIVE_INFINITY;
+        float[] bestLine = null;
+        for (int i = 0; i < len; i += 2) {
+            float[] line = {
+                    corners[i], corners[(i + 1) % len],
+                    corners[(i + 2) % len], corners[(i + 3) % len]
+            };
+            float mag = GeometryMathUtils.vectorLength(
+                    GeometryMathUtils.shortestVectorFromPointToLine(point, line));
+            if (mag < oldMag) {
+                oldMag = mag;
+                bestLine = line;
+            }
+        }
+        return bestLine;
+    }
+
+    /**
+     * Checks if a given point is within a rotated rectangle.
+     *
+     * @param point 2D point to check
+     * @param bound rectangle to rotate
+     * @param rot angle of rotation about rectangle center
+     * @return true if point is within rotated rectangle
+     */
+    public static boolean pointInRotatedRect(float[] point, RectF bound, float rot) {
+        Matrix m = new Matrix();
+        float[] p = Arrays.copyOf(point, 2);
+        m.setRotate(rot, bound.centerX(), bound.centerY());
+        Matrix m0 = new Matrix();
+        if (!m.invert(m0))
+            return false;
+        m0.mapPoints(p);
+        return inclusiveContains(bound, p[0], p[1]);
+    }
+
+    /**
+     * Checks if a given point is within a rotated rectangle.
+     *
+     * @param point 2D point to check
+     * @param rotatedRect corners of a rotated rectangle
+     * @param center center of the rotated rectangle
+     * @return true if point is within rotated rectangle
+     */
+    public static boolean pointInRotatedRect(float[] point, float[] rotatedRect, float[] center) {
+        RectF unrotated = new RectF();
+        float angle = getUnrotated(rotatedRect, center, unrotated);
+        return pointInRotatedRect(point, unrotated, angle);
+    }
+
+    /**
+     * Resizes rectangle to have a certain aspect ratio (center remains
+     * stationary).
+     *
+     * @param r rectangle to resize
+     * @param w new width aspect
+     * @param h new height aspect
+     */
+    public static void fixAspectRatio(RectF r, float w, float h) {
+        float scale = Math.min(r.width() / w, r.height() / h);
+        float centX = r.centerX();
+        float centY = r.centerY();
+        float hw = scale * w / 2;
+        float hh = scale * h / 2;
+        r.set(centX - hw, centY - hh, centX + hw, centY + hh);
+    }
+
+    /**
+     * Resizes rectangle to have a certain aspect ratio (center remains
+     * stationary) while constraining it to remain within the original rect.
+     *
+     * @param r rectangle to resize
+     * @param w new width aspect
+     * @param h new height aspect
+     */
+    public static void fixAspectRatioContained(RectF r, float w, float h) {
+        float origW = r.width();
+        float origH = r.height();
+        float origA = origW / origH;
+        float a = w / h;
+        float finalW = origW;
+        float finalH = origH;
+        if (origA < a) {
+            finalH = origW / a;
+            r.top = r.centerY() - finalH / 2;
+            r.bottom = r.top + finalH;
+        } else {
+            finalW = origH * a;
+            r.left = r.centerX() - finalW / 2;
+            r.right = r.left + finalW;
+        }
+    }
+
+    /**
+     * Stretches/Scales/Translates photoBounds to match displayBounds, and
+     * and returns an equivalent stretched/scaled/translated cropBounds or null
+     * if the mapping is invalid.
+     * @param cropBounds  cropBounds to transform
+     * @param photoBounds  original bounds containing crop bounds
+     * @param displayBounds  final bounds for crop
+     * @return  the stretched/scaled/translated crop bounds that fit within displayBounds
+     */
+    public static RectF getScaledCropBounds(RectF cropBounds, RectF photoBounds,
+            RectF displayBounds) {
+        Matrix m = new Matrix();
+        m.setRectToRect(photoBounds, displayBounds, Matrix.ScaleToFit.FILL);
+        RectF trueCrop = new RectF(cropBounds);
+        if (!m.mapRect(trueCrop)) {
+            return null;
+        }
+        return trueCrop;
+    }
+
+    /**
+     * Returns the size of a bitmap in bytes.
+     * @param bmap  bitmap whose size to check
+     * @return  bitmap size in bytes
+     */
+    public static int getBitmapSize(Bitmap bmap) {
+        return bmap.getRowBytes() * bmap.getHeight();
+    }
+
+    /**
+     * Constrains rotation to be in [0, 90, 180, 270] rounding down.
+     * @param rotation  any rotation value, in degrees
+     * @return  integer rotation in [0, 90, 180, 270]
+     */
+    public static int constrainedRotation(float rotation) {
+        int r = (int) ((rotation % 360) / 90);
+        r = (r < 0) ? (r + 4) : r;
+        return r * 90;
+    }
+
+    private static float getUnrotated(float[] rotatedRect, float[] center, RectF unrotated) {
+        float dy = rotatedRect[1] - rotatedRect[3];
+        float dx = rotatedRect[0] - rotatedRect[2];
+        float angle = (float) (Math.atan(dy / dx) * 180 / Math.PI);
+        Matrix m = new Matrix();
+        m.setRotate(-angle, center[0], center[1]);
+        float[] unrotatedRect = new float[rotatedRect.length];
+        m.mapPoints(unrotatedRect, rotatedRect);
+        unrotated.set(trapToRect(unrotatedRect));
+        return angle;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropObject.java b/src/com/android/gallery3d/filtershow/crop/CropObject.java
new file mode 100644
index 0000000..b98ed1b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropObject.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.crop;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+public class CropObject {
+    private BoundedRect mBoundedRect;
+    private float mAspectWidth = 1;
+    private float mAspectHeight = 1;
+    private boolean mFixAspectRatio = false;
+    private float mRotation = 0;
+    private float mTouchTolerance = 45;
+    private float mMinSideSize = 20;
+
+    public static final int MOVE_NONE = 0;
+    // Sides
+    public static final int MOVE_LEFT = 1;
+    public static final int MOVE_TOP = 2;
+    public static final int MOVE_RIGHT = 4;
+    public static final int MOVE_BOTTOM = 8;
+    public static final int MOVE_BLOCK = 16;
+
+    // Corners
+    public static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT;
+    public static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT;
+    public static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT;
+    public static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT;
+
+    private int mMovingEdges = MOVE_NONE;
+
+    public CropObject(Rect outerBound, Rect innerBound, int outerAngle) {
+        mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound);
+    }
+
+    public CropObject(RectF outerBound, RectF innerBound, int outerAngle) {
+        mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound);
+    }
+
+    public void resetBoundsTo(RectF inner, RectF outer) {
+        mBoundedRect.resetTo(0, outer, inner);
+    }
+
+    public void getInnerBounds(RectF r) {
+        mBoundedRect.setToInner(r);
+    }
+
+    public void getOuterBounds(RectF r) {
+        mBoundedRect.setToOuter(r);
+    }
+
+    public RectF getInnerBounds() {
+        return mBoundedRect.getInner();
+    }
+
+    public RectF getOuterBounds() {
+        return mBoundedRect.getOuter();
+    }
+
+    public int getSelectState() {
+        return mMovingEdges;
+    }
+
+    public boolean isFixedAspect() {
+        return mFixAspectRatio;
+    }
+
+    public void rotateOuter(int angle) {
+        mRotation = angle % 360;
+        mBoundedRect.setRotation(mRotation);
+        clearSelectState();
+    }
+
+    public boolean setInnerAspectRatio(float width, float height) {
+        if (width <= 0 || height <= 0) {
+            throw new IllegalArgumentException("Width and Height must be greater than zero");
+        }
+        RectF inner = mBoundedRect.getInner();
+        CropMath.fixAspectRatioContained(inner, width, height);
+        if (inner.width() < mMinSideSize || inner.height() < mMinSideSize) {
+            return false;
+        }
+        mAspectWidth = width;
+        mAspectHeight = height;
+        mFixAspectRatio = true;
+        mBoundedRect.setInner(inner);
+        clearSelectState();
+        return true;
+    }
+
+    public void setTouchTolerance(float tolerance) {
+        if (tolerance <= 0) {
+            throw new IllegalArgumentException("Tolerance must be greater than zero");
+        }
+        mTouchTolerance = tolerance;
+    }
+
+    public void setMinInnerSideSize(float minSide) {
+        if (minSide <= 0) {
+            throw new IllegalArgumentException("Min dide must be greater than zero");
+        }
+        mMinSideSize = minSide;
+    }
+
+    public void unsetAspectRatio() {
+        mFixAspectRatio = false;
+        clearSelectState();
+    }
+
+    public boolean hasSelectedEdge() {
+        return mMovingEdges != MOVE_NONE;
+    }
+
+    public static boolean checkCorner(int selected) {
+        return selected == TOP_LEFT || selected == TOP_RIGHT || selected == BOTTOM_RIGHT
+                || selected == BOTTOM_LEFT;
+    }
+
+    public static boolean checkEdge(int selected) {
+        return selected == MOVE_LEFT || selected == MOVE_TOP || selected == MOVE_RIGHT
+                || selected == MOVE_BOTTOM;
+    }
+
+    public static boolean checkBlock(int selected) {
+        return selected == MOVE_BLOCK;
+    }
+
+    public static boolean checkValid(int selected) {
+        return selected == MOVE_NONE || checkBlock(selected) || checkEdge(selected)
+                || checkCorner(selected);
+    }
+
+    public void clearSelectState() {
+        mMovingEdges = MOVE_NONE;
+    }
+
+    public int wouldSelectEdge(float x, float y) {
+        int edgeSelected = calculateSelectedEdge(x, y);
+        if (edgeSelected != MOVE_NONE && edgeSelected != MOVE_BLOCK) {
+            return edgeSelected;
+        }
+        return MOVE_NONE;
+    }
+
+    public boolean selectEdge(int edge) {
+        if (!checkValid(edge)) {
+            // temporary
+            throw new IllegalArgumentException("bad edge selected");
+            // return false;
+        }
+        if ((mFixAspectRatio && !checkCorner(edge)) && !checkBlock(edge) && edge != MOVE_NONE) {
+            // temporary
+            throw new IllegalArgumentException("bad corner selected");
+            // return false;
+        }
+        mMovingEdges = edge;
+        return true;
+    }
+
+    public boolean selectEdge(float x, float y) {
+        int edgeSelected = calculateSelectedEdge(x, y);
+        if (mFixAspectRatio) {
+            edgeSelected = fixEdgeToCorner(edgeSelected);
+        }
+        if (edgeSelected == MOVE_NONE) {
+            return false;
+        }
+        return selectEdge(edgeSelected);
+    }
+
+    public boolean moveCurrentSelection(float dX, float dY) {
+        if (mMovingEdges == MOVE_NONE) {
+            return false;
+        }
+        RectF crop = mBoundedRect.getInner();
+
+        float minWidthHeight = mMinSideSize;
+
+        int movingEdges = mMovingEdges;
+        if (movingEdges == MOVE_BLOCK) {
+            mBoundedRect.moveInner(dX, dY);
+            return true;
+        } else {
+            float dx = 0;
+            float dy = 0;
+
+            if ((movingEdges & MOVE_LEFT) != 0) {
+                dx = Math.min(crop.left + dX, crop.right - minWidthHeight) - crop.left;
+            }
+            if ((movingEdges & MOVE_TOP) != 0) {
+                dy = Math.min(crop.top + dY, crop.bottom - minWidthHeight) - crop.top;
+            }
+            if ((movingEdges & MOVE_RIGHT) != 0) {
+                dx = Math.max(crop.right + dX, crop.left + minWidthHeight)
+                        - crop.right;
+            }
+            if ((movingEdges & MOVE_BOTTOM) != 0) {
+                dy = Math.max(crop.bottom + dY, crop.top + minWidthHeight)
+                        - crop.bottom;
+            }
+
+            if (mFixAspectRatio) {
+                float[] l1 = {
+                        crop.left, crop.bottom
+                };
+                float[] l2 = {
+                        crop.right, crop.top
+                };
+                if (movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT) {
+                    l1[1] = crop.top;
+                    l2[1] = crop.bottom;
+                }
+                float[] b = {
+                        l1[0] - l2[0], l1[1] - l2[1]
+                };
+                float[] disp = {
+                        dx, dy
+                };
+                float[] bUnit = GeometryMathUtils.normalize(b);
+                float sp = GeometryMathUtils.scalarProjection(disp, bUnit);
+                dx = sp * bUnit[0];
+                dy = sp * bUnit[1];
+                RectF newCrop = fixedCornerResize(crop, movingEdges, dx, dy);
+
+                mBoundedRect.fixedAspectResizeInner(newCrop);
+            } else {
+                if ((movingEdges & MOVE_LEFT) != 0) {
+                    crop.left += dx;
+                }
+                if ((movingEdges & MOVE_TOP) != 0) {
+                    crop.top += dy;
+                }
+                if ((movingEdges & MOVE_RIGHT) != 0) {
+                    crop.right += dx;
+                }
+                if ((movingEdges & MOVE_BOTTOM) != 0) {
+                    crop.bottom += dy;
+                }
+                mBoundedRect.resizeInner(crop);
+            }
+        }
+        return true;
+    }
+
+    // Helper methods
+
+    private int calculateSelectedEdge(float x, float y) {
+        RectF cropped = mBoundedRect.getInner();
+
+        float left = Math.abs(x - cropped.left);
+        float right = Math.abs(x - cropped.right);
+        float top = Math.abs(y - cropped.top);
+        float bottom = Math.abs(y - cropped.bottom);
+
+        int edgeSelected = MOVE_NONE;
+        // Check left or right.
+        if ((left <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top)
+                && ((y - mTouchTolerance) <= cropped.bottom) && (left < right)) {
+            edgeSelected |= MOVE_LEFT;
+        }
+        else if ((right <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top)
+                && ((y - mTouchTolerance) <= cropped.bottom)) {
+            edgeSelected |= MOVE_RIGHT;
+        }
+
+        // Check top or bottom.
+        if ((top <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left)
+                && ((x - mTouchTolerance) <= cropped.right) && (top < bottom)) {
+            edgeSelected |= MOVE_TOP;
+        }
+        else if ((bottom <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left)
+                && ((x - mTouchTolerance) <= cropped.right)) {
+            edgeSelected |= MOVE_BOTTOM;
+        }
+        return edgeSelected;
+    }
+
+    private static RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy) {
+        RectF newCrop = null;
+        // Fix opposite corner in place and move sides
+        if (moving_corner == BOTTOM_RIGHT) {
+            newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height()
+                    + dy);
+        } else if (moving_corner == BOTTOM_LEFT) {
+            newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height()
+                    + dy);
+        } else if (moving_corner == TOP_LEFT) {
+            newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy,
+                    r.right, r.bottom);
+        } else if (moving_corner == TOP_RIGHT) {
+            newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left
+                    + r.width() + dx, r.bottom);
+        }
+        return newCrop;
+    }
+
+    private static int fixEdgeToCorner(int moving_edges) {
+        if (moving_edges == MOVE_LEFT) {
+            moving_edges |= MOVE_TOP;
+        }
+        if (moving_edges == MOVE_TOP) {
+            moving_edges |= MOVE_LEFT;
+        }
+        if (moving_edges == MOVE_RIGHT) {
+            moving_edges |= MOVE_BOTTOM;
+        }
+        if (moving_edges == MOVE_BOTTOM) {
+            moving_edges |= MOVE_RIGHT;
+        }
+        return moving_edges;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropView.java b/src/com/android/gallery3d/filtershow/crop/CropView.java
new file mode 100644
index 0000000..bbb7cfd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropView.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.crop;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+
+public class CropView extends View {
+    private static final String LOGTAG = "CropView";
+
+    private RectF mImageBounds = new RectF();
+    private RectF mScreenBounds = new RectF();
+    private RectF mScreenImageBounds = new RectF();
+    private RectF mScreenCropBounds = new RectF();
+    private Rect mShadowBounds = new Rect();
+
+    private Bitmap mBitmap;
+    private Paint mPaint = new Paint();
+
+    private NinePatchDrawable mShadow;
+    private CropObject mCropObj = null;
+    private Drawable mCropIndicator;
+    private int mIndicatorSize;
+    private int mRotation = 0;
+    private boolean mMovingBlock = false;
+    private Matrix mDisplayMatrix = null;
+    private Matrix mDisplayMatrixInverse = null;
+    private boolean mDirty = false;
+
+    private float mPrevX = 0;
+    private float mPrevY = 0;
+    private float mSpotX = 0;
+    private float mSpotY = 0;
+    private boolean mDoSpot = false;
+
+    private int mShadowMargin = 15;
+    private int mMargin = 32;
+    private int mOverlayShadowColor = 0xCF000000;
+    private int mOverlayWPShadowColor = 0x5F000000;
+    private int mWPMarkerColor = 0x7FFFFFFF;
+    private int mMinSideSize = 90;
+    private int mTouchTolerance = 40;
+    private float mDashOnLength = 20;
+    private float mDashOffLength = 10;
+
+    private enum Mode {
+        NONE, MOVE
+    }
+
+    private Mode mState = Mode.NONE;
+
+    public CropView(Context context) {
+        super(context);
+        setup(context);
+    }
+
+    public CropView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setup(context);
+    }
+
+    public CropView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        setup(context);
+    }
+
+    private void setup(Context context) {
+        Resources rsc = context.getResources();
+        mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow);
+        mCropIndicator = rsc.getDrawable(R.drawable.camera_crop);
+        mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size);
+        mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin);
+        mMargin = (int) rsc.getDimension(R.dimen.preview_margin);
+        mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side);
+        mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance);
+        mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color);
+        mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color);
+        mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers);
+        mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length);
+        mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length);
+    }
+
+    public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) {
+        mBitmap = image;
+        if (mCropObj != null) {
+            RectF crop = mCropObj.getInnerBounds();
+            RectF containing = mCropObj.getOuterBounds();
+            if (crop != newCropBounds || containing != newPhotoBounds
+                    || mRotation != rotation) {
+                mRotation = rotation;
+                mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds);
+                clearDisplay();
+            }
+        } else {
+            mRotation = rotation;
+            mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0);
+            clearDisplay();
+        }
+    }
+
+    public RectF getCrop() {
+        return mCropObj.getInnerBounds();
+    }
+
+    public RectF getPhoto() {
+        return mCropObj.getOuterBounds();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+            return true;
+        }
+        float[] touchPoint = {
+                x, y
+        };
+        mDisplayMatrixInverse.mapPoints(touchPoint);
+        x = touchPoint[0];
+        y = touchPoint[1];
+        switch (event.getActionMasked()) {
+            case (MotionEvent.ACTION_DOWN):
+                if (mState == Mode.NONE) {
+                    if (!mCropObj.selectEdge(x, y)) {
+                        mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK);
+                    }
+                    mPrevX = x;
+                    mPrevY = y;
+                    mState = Mode.MOVE;
+                }
+                break;
+            case (MotionEvent.ACTION_UP):
+                if (mState == Mode.MOVE) {
+                    mCropObj.selectEdge(CropObject.MOVE_NONE);
+                    mMovingBlock = false;
+                    mPrevX = x;
+                    mPrevY = y;
+                    mState = Mode.NONE;
+                }
+                break;
+            case (MotionEvent.ACTION_MOVE):
+                if (mState == Mode.MOVE) {
+                    float dx = x - mPrevX;
+                    float dy = y - mPrevY;
+                    mCropObj.moveCurrentSelection(dx, dy);
+                    mPrevX = x;
+                    mPrevY = y;
+                }
+                break;
+            default:
+                break;
+        }
+        invalidate();
+        return true;
+    }
+
+    private void reset() {
+        Log.w(LOGTAG, "crop reset called");
+        mState = Mode.NONE;
+        mCropObj = null;
+        mRotation = 0;
+        mMovingBlock = false;
+        clearDisplay();
+    }
+
+    private void clearDisplay() {
+        mDisplayMatrix = null;
+        mDisplayMatrixInverse = null;
+        invalidate();
+    }
+
+    protected void configChanged() {
+        mDirty = true;
+    }
+
+    public void applyFreeAspect() {
+        mCropObj.unsetAspectRatio();
+        invalidate();
+    }
+
+    public void applyOriginalAspect() {
+        RectF outer = mCropObj.getOuterBounds();
+        float w = outer.width();
+        float h = outer.height();
+        if (w > 0 && h > 0) {
+            applyAspect(w, h);
+            mCropObj.resetBoundsTo(outer, outer);
+        } else {
+            Log.w(LOGTAG, "failed to set aspect ratio original");
+        }
+    }
+
+    public void applySquareAspect() {
+        applyAspect(1, 1);
+    }
+
+    public void applyAspect(float x, float y) {
+        if (x <= 0 || y <= 0) {
+            throw new IllegalArgumentException("Bad arguments to applyAspect");
+        }
+        // If we are rotated by 90 degrees from horizontal, swap x and y
+        if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) {
+            float tmp = x;
+            x = y;
+            y = tmp;
+        }
+        if (!mCropObj.setInnerAspectRatio(x, y)) {
+            Log.w(LOGTAG, "failed to set aspect ratio");
+        }
+        invalidate();
+    }
+
+    public void setWallpaperSpotlight(float spotlightX, float spotlightY) {
+        mSpotX = spotlightX;
+        mSpotY = spotlightY;
+        if (mSpotX > 0 && mSpotY > 0) {
+            mDoSpot = true;
+        }
+    }
+
+    public void unsetWallpaperSpotlight() {
+        mDoSpot = false;
+    }
+
+    /**
+     * Rotates first d bits in integer x to the left some number of times.
+     */
+    private int bitCycleLeft(int x, int times, int d) {
+        int mask = (1 << d) - 1;
+        int mout = x & mask;
+        times %= d;
+        int hi = mout >> (d - times);
+        int low = (mout << times) & mask;
+        int ret = x & ~mask;
+        ret |= low;
+        ret |= hi;
+        return ret;
+    }
+
+    /**
+     * Find the selected edge or corner in screen coordinates.
+     */
+    private int decode(int movingEdges, float rotation) {
+        int rot = CropMath.constrainedRotation(rotation);
+        switch (rot) {
+            case 90:
+                return bitCycleLeft(movingEdges, 1, 4);
+            case 180:
+                return bitCycleLeft(movingEdges, 2, 4);
+            case 270:
+                return bitCycleLeft(movingEdges, 3, 4);
+            default:
+                return movingEdges;
+        }
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        if (mBitmap == null) {
+            return;
+        }
+        if (mDirty) {
+            mDirty = false;
+            clearDisplay();
+        }
+
+        mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+        mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
+        mScreenBounds.inset(mMargin, mMargin);
+
+        // If crop object doesn't exist, create it and update it from master
+        // state
+        if (mCropObj == null) {
+            reset();
+            mCropObj = new CropObject(mImageBounds, mImageBounds, 0);
+        }
+
+        // If display matrix doesn't exist, create it and its dependencies
+        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+            mDisplayMatrix = new Matrix();
+            mDisplayMatrix.reset();
+            if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds,
+                    mRotation)) {
+                Log.w(LOGTAG, "failed to get screen matrix");
+                mDisplayMatrix = null;
+                return;
+            }
+            mDisplayMatrixInverse = new Matrix();
+            mDisplayMatrixInverse.reset();
+            if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) {
+                Log.w(LOGTAG, "could not invert display matrix");
+                mDisplayMatrixInverse = null;
+                return;
+            }
+            // Scale min side and tolerance by display matrix scale factor
+            mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
+            mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
+        }
+
+        mScreenImageBounds.set(mImageBounds);
+
+        // Draw background shadow
+        if (mDisplayMatrix.mapRect(mScreenImageBounds)) {
+            int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin);
+            mScreenImageBounds.roundOut(mShadowBounds);
+            mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top -
+                    margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin);
+            mShadow.setBounds(mShadowBounds);
+            mShadow.draw(canvas);
+        }
+
+        mPaint.setAntiAlias(true);
+        mPaint.setFilterBitmap(true);
+        // Draw actual bitmap
+        canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint);
+
+        mCropObj.getInnerBounds(mScreenCropBounds);
+
+        if (mDisplayMatrix.mapRect(mScreenCropBounds)) {
+
+            // Draw overlay shadows
+            Paint p = new Paint();
+            p.setColor(mOverlayShadowColor);
+            p.setStyle(Paint.Style.FILL);
+            CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds);
+
+            // Draw crop rect and markers
+            CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds);
+            if (!mDoSpot) {
+                CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds);
+            } else {
+                Paint wpPaint = new Paint();
+                wpPaint.setColor(mWPMarkerColor);
+                wpPaint.setStrokeWidth(3);
+                wpPaint.setStyle(Paint.Style.STROKE);
+                wpPaint.setPathEffect(new DashPathEffect(new float[]
+                        {mDashOnLength, mDashOnLength + mDashOffLength}, 0));
+                p.setColor(mOverlayWPShadowColor);
+                CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds,
+                        mSpotX, mSpotY, wpPaint, p);
+            }
+            CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize,
+                    mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation));
+        }
+
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java
new file mode 100644
index 0000000..e18d310
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class FilterStackDBHelper extends SQLiteOpenHelper {
+
+    public static final int DATABASE_VERSION = 1;
+    public static final String DATABASE_NAME = "filterstacks.db";
+    private static final String SQL_CREATE_TABLE = "CREATE TABLE ";
+
+    public static interface FilterStack {
+        /** The row uid */
+        public static final String _ID = "_id";
+        /** The table name */
+        public static final String TABLE = "filterstack";
+        /** The stack name */
+        public static final String STACK_ID = "stack_id";
+        /** A serialized stack of filters. */
+        public static final String FILTER_STACK= "stack";
+    }
+
+    private static final String[][] CREATE_FILTER_STACK = {
+            { FilterStack._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+            { FilterStack.STACK_ID, "TEXT" },
+            { FilterStack.FILTER_STACK, "BLOB" },
+    };
+
+    public FilterStackDBHelper(Context context, String name, int version) {
+        super(context, name, null, version);
+    }
+
+    public FilterStackDBHelper(Context context, String name) {
+        this(context, name, DATABASE_VERSION);
+    }
+
+    public FilterStackDBHelper(Context context) {
+        this(context, DATABASE_NAME);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        createTable(db, FilterStack.TABLE, CREATE_FILTER_STACK);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        dropTable(db, FilterStack.TABLE);
+        onCreate(db);
+    }
+
+    protected static void createTable(SQLiteDatabase db, String table, String[][] columns) {
+        StringBuilder create = new StringBuilder(SQL_CREATE_TABLE);
+        create.append(table).append('(');
+        boolean first = true;
+        for (String[] column : columns) {
+            if (!first) {
+                create.append(',');
+            }
+            first = false;
+            for (String val : column) {
+                create.append(val).append(' ');
+            }
+        }
+        create.append(')');
+        db.beginTransaction();
+        try {
+            db.execSQL(create.toString());
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    protected static void dropTable(SQLiteDatabase db, String table) {
+        db.beginTransaction();
+        try {
+            db.execSQL("drop table if exists " + table);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackSource.java b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java
new file mode 100644
index 0000000..d283771
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.gallery3d.filtershow.data.FilterStackDBHelper.FilterStack;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FilterStackSource {
+    private static final String LOGTAG = "FilterStackSource";
+
+    private SQLiteDatabase database = null;
+    private final FilterStackDBHelper dbHelper;
+
+    public FilterStackSource(Context context) {
+        dbHelper = new FilterStackDBHelper(context);
+    }
+
+    public void open() {
+        try {
+            database = dbHelper.getWritableDatabase();
+        } catch (SQLiteException e) {
+            Log.w(LOGTAG, "could not open database", e);
+        }
+    }
+
+    public void close() {
+        database = null;
+        dbHelper.close();
+    }
+
+    public boolean insertStack(String stackName, byte[] stackBlob) {
+        boolean ret = true;
+        ContentValues val = new ContentValues();
+        val.put(FilterStack.STACK_ID, stackName);
+        val.put(FilterStack.FILTER_STACK, stackBlob);
+        database.beginTransaction();
+        try {
+            ret = (-1 != database.insert(FilterStack.TABLE, null, val));
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+        return ret;
+    }
+
+    public void updateStackName(int id, String stackName) {
+        ContentValues val = new ContentValues();
+        val.put(FilterStack.STACK_ID, stackName);
+        database.beginTransaction();
+        try {
+            database.update(FilterStack.TABLE, val, FilterStack._ID + " = ?",
+                    new String[] { "" + id});
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+    }
+
+    public boolean removeStack(int id) {
+        boolean ret = true;
+        database.beginTransaction();
+        try {
+            ret = (0 != database.delete(FilterStack.TABLE, FilterStack._ID + " = ?",
+                    new String[] { "" + id }));
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+        return ret;
+    }
+
+    public void removeAllStacks() {
+        database.beginTransaction();
+        try {
+            database.delete(FilterStack.TABLE, null, null);
+            database.setTransactionSuccessful();
+        } finally {
+            database.endTransaction();
+        }
+    }
+
+    public byte[] getStack(String stackName) {
+        byte[] ret = null;
+        Cursor c = null;
+        database.beginTransaction();
+        try {
+            c = database.query(FilterStack.TABLE,
+                    new String[] { FilterStack.FILTER_STACK },
+                    FilterStack.STACK_ID + " = ?",
+                    new String[] { stackName }, null, null, null, null);
+            if (c != null && c.moveToFirst() && !c.isNull(0)) {
+                ret = c.getBlob(0);
+            }
+            database.setTransactionSuccessful();
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+            database.endTransaction();
+        }
+        return ret;
+    }
+
+    public ArrayList<FilterUserPresetRepresentation> getAllUserPresets() {
+        ArrayList<FilterUserPresetRepresentation> ret =
+                new ArrayList<FilterUserPresetRepresentation>();
+
+        Cursor c = null;
+        database.beginTransaction();
+        try {
+            c = database.query(FilterStack.TABLE,
+                    new String[] { FilterStack._ID,
+                            FilterStack.STACK_ID,
+                            FilterStack.FILTER_STACK },
+                    null, null, null, null, null, null);
+            if (c != null) {
+                boolean loopCheck = c.moveToFirst();
+                while (loopCheck) {
+                    int id = c.getInt(0);
+                    String name = (c.isNull(1)) ?  null : c.getString(1);
+                    byte[] b = (c.isNull(2)) ? null : c.getBlob(2);
+                    String json = new String(b);
+
+                    ImagePreset preset = new ImagePreset();
+                    preset.readJsonFromString(json);
+                    FilterUserPresetRepresentation representation =
+                            new FilterUserPresetRepresentation(name, preset, id);
+                    ret.add(representation);
+                    loopCheck = c.moveToNext();
+                }
+            }
+            database.setTransactionSuccessful();
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+            database.endTransaction();
+        }
+
+        return ret;
+    }
+
+    public List<Pair<String, byte[]>> getAllStacks() {
+        List<Pair<String, byte[]>> ret = new ArrayList<Pair<String, byte[]>>();
+        Cursor c = null;
+        database.beginTransaction();
+        try {
+            c = database.query(FilterStack.TABLE,
+                    new String[] { FilterStack.STACK_ID, FilterStack.FILTER_STACK },
+                    null, null, null, null, null, null);
+            if (c != null) {
+                boolean loopCheck = c.moveToFirst();
+                while (loopCheck) {
+                    String name = (c.isNull(0)) ?  null : c.getString(0);
+                    byte[] b = (c.isNull(1)) ? null : c.getBlob(1);
+                    ret.add(new Pair<String, byte[]>(name, b));
+                    loopCheck = c.moveToNext();
+                }
+            }
+            database.setTransactionSuccessful();
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+            database.endTransaction();
+        }
+        if (ret.size() <= 0) {
+            return null;
+        }
+        return ret;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java b/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java
new file mode 100644
index 0000000..114cd3e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java
@@ -0,0 +1,149 @@
+package com.android.gallery3d.filtershow.data;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+
+public class UserPresetsManager implements Handler.Callback {
+
+    private static final String LOGTAG = "UserPresetsManager";
+
+    private FilterShowActivity mActivity;
+    private HandlerThread mHandlerThread = null;
+    private Handler mProcessingHandler = null;
+    private FilterStackSource mUserPresets;
+
+    private static final int LOAD = 1;
+    private static final int LOAD_RESULT = 2;
+    private static final int SAVE = 3;
+    private static final int DELETE = 4;
+    private static final int UPDATE = 5;
+
+    private ArrayList<FilterUserPresetRepresentation> mRepresentations;
+
+    private final Handler mResultHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case LOAD_RESULT:
+                    resultLoad(msg);
+                    break;
+            }
+        }
+    };
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case LOAD:
+                processLoad();
+                return true;
+            case SAVE:
+                processSave(msg);
+                return true;
+            case DELETE:
+                processDelete(msg);
+                return true;
+            case UPDATE:
+                processUpdate(msg);
+                return true;
+        }
+        return false;
+    }
+
+    public UserPresetsManager(FilterShowActivity context) {
+        mActivity = context;
+        mHandlerThread = new HandlerThread(LOGTAG,
+                android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        mHandlerThread.start();
+        mProcessingHandler = new Handler(mHandlerThread.getLooper(), this);
+        mUserPresets = new FilterStackSource(mActivity);
+        mUserPresets.open();
+    }
+
+    public ArrayList<FilterUserPresetRepresentation> getRepresentations() {
+        return mRepresentations;
+    }
+
+    public void load() {
+        Message msg = mProcessingHandler.obtainMessage(LOAD);
+        mProcessingHandler.sendMessage(msg);
+    }
+
+    public void close() {
+        mUserPresets.close();
+        mHandlerThread.quit();
+    }
+
+    static class SaveOperation {
+        String json;
+        String name;
+    }
+
+    public void save(ImagePreset preset) {
+        Message msg = mProcessingHandler.obtainMessage(SAVE);
+        SaveOperation op = new SaveOperation();
+        op.json = preset.getJsonString(mActivity.getString(R.string.saved));
+        op.name= mActivity.getString(R.string.filtershow_new_preset);
+        msg.obj = op;
+        mProcessingHandler.sendMessage(msg);
+    }
+
+    public void delete(int id) {
+        Message msg = mProcessingHandler.obtainMessage(DELETE);
+        msg.arg1 = id;
+        mProcessingHandler.sendMessage(msg);
+    }
+
+    static class UpdateOperation {
+        int id;
+        String name;
+    }
+
+    public void update(FilterUserPresetRepresentation representation) {
+        Message msg = mProcessingHandler.obtainMessage(UPDATE);
+        UpdateOperation op = new UpdateOperation();
+        op.id = representation.getId();
+        op.name = representation.getName();
+        msg.obj = op;
+        mProcessingHandler.sendMessage(msg);
+    }
+
+    private void processLoad() {
+        ArrayList<FilterUserPresetRepresentation> list = mUserPresets.getAllUserPresets();
+        Message msg = mResultHandler.obtainMessage(LOAD_RESULT);
+        msg.obj = list;
+        mResultHandler.sendMessage(msg);
+    }
+
+    private void resultLoad(Message msg) {
+        mRepresentations =
+                (ArrayList<FilterUserPresetRepresentation>) msg.obj;
+        mActivity.updateUserPresetsFromManager();
+    }
+
+    private void processSave(Message msg) {
+        SaveOperation op = (SaveOperation) msg.obj;
+        mUserPresets.insertStack(op.name, op.json.getBytes());
+        processLoad();
+    }
+
+    private void processDelete(Message msg) {
+        int id = msg.arg1;
+        mUserPresets.removeStack(id);
+        processLoad();
+    }
+
+    private void processUpdate(Message msg) {
+        UpdateOperation op = (UpdateOperation) msg.obj;
+        mUserPresets.updateStackName(op.id, op.name);
+        processLoad();
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/BasicEditor.java b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java
new file mode 100644
index 0000000..af694d8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
+import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+
+/**
+ * The basic editor that all the one parameter filters
+ */
+public class BasicEditor extends ParametricEditor implements ParameterInteger {
+    public static int ID = R.id.basicEditor;
+    private final String LOGTAG = "BasicEditor";
+
+    public BasicEditor() {
+        super(ID, R.layout.filtershow_default_editor, R.id.basicEditor);
+    }
+
+    protected BasicEditor(int id) {
+        super(id, R.layout.filtershow_default_editor, R.id.basicEditor);
+    }
+
+    protected BasicEditor(int id, int layoutID, int viewID) {
+        super(id, layoutID, viewID);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        if (getLocalRepresentation() != null && getLocalRepresentation() instanceof FilterBasicRepresentation) {
+            FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+            updateText();
+        }
+    }
+
+    private FilterBasicRepresentation getBasicRepresentation() {
+        FilterRepresentation tmpRep = getLocalRepresentation();
+        if (tmpRep != null && tmpRep instanceof FilterBasicRepresentation) {
+            return (FilterBasicRepresentation) tmpRep;
+
+        }
+        return null;
+    }
+
+    @Override
+    public int getMaximum() {
+        FilterBasicRepresentation rep = getBasicRepresentation();
+        if (rep == null) {
+            return 0;
+        }
+        return rep.getMaximum();
+    }
+
+    @Override
+    public int getMinimum() {
+        FilterBasicRepresentation rep = getBasicRepresentation();
+        if (rep == null) {
+            return 0;
+        }
+        return rep.getMinimum();
+    }
+
+    @Override
+    public int getDefaultValue() {
+        return 0;
+    }
+
+    @Override
+    public int getValue() {
+        FilterBasicRepresentation rep = getBasicRepresentation();
+        if (rep == null) {
+            return 0;
+        }
+        return rep.getValue();
+    }
+
+    @Override
+    public String getValueString() {
+        return null;
+    }
+
+    @Override
+    public void setValue(int value) {
+        FilterBasicRepresentation rep = getBasicRepresentation();
+        if (rep == null) {
+            return;
+        }
+        rep.setValue(value);
+        commitLocalRepresentation();
+    }
+
+    @Override
+    public String getParameterName() {
+        FilterBasicRepresentation rep = getBasicRepresentation();
+        return mContext.getString(rep.getTextId());
+    }
+
+    @Override
+    public String getParameterType() {
+        return sParameterType;
+    }
+
+    @Override
+    public void setController(Control c) {
+    }
+
+    @Override
+    public void setFilterView(FilterView editor) {
+
+    }
+
+    @Override
+    public void copyFrom(Parameter src) {
+
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/Editor.java b/src/com/android/gallery3d/filtershow/editors/Editor.java
new file mode 100644
index 0000000..a9e56e0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/Editor.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Base class for Editors Must contain a mImageShow and a top level view
+ */
+public class Editor implements OnSeekBarChangeListener, SwapButton.SwapButtonListener {
+    protected Context mContext;
+    protected View mView;
+    protected ImageShow mImageShow;
+    protected FrameLayout mFrameLayout;
+    protected SeekBar mSeekBar;
+    Button mEditTitle;
+    protected Button mFilterTitle;
+    protected int mID;
+    private final String LOGTAG = "Editor";
+    protected boolean mChangesGeometry = false;
+    protected FilterRepresentation mLocalRepresentation = null;
+    protected byte mShowParameter = SHOW_VALUE_UNDEFINED;
+    private Button mButton;
+    public static byte SHOW_VALUE_UNDEFINED = -1;
+    public static byte SHOW_VALUE_OFF = 0;
+    public static byte SHOW_VALUE_INT = 1;
+
+    public static void hackFixStrings(Menu menu) {
+        int count = menu.size();
+        for (int i = 0; i < count; i++) {
+            MenuItem item = menu.getItem(i);
+            item.setTitle(item.getTitle().toString().toUpperCase());
+        }
+    }
+
+    public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+        return effectName.toUpperCase() + " " + parameterValue;
+    }
+
+    protected Editor(int id) {
+        mID = id;
+    }
+
+    public int getID() {
+        return mID;
+    }
+
+    public byte showParameterValue() {
+        return mShowParameter;
+    }
+
+    public boolean showsSeekBar() {
+        return true;
+    }
+
+    public void setUpEditorUI(View actionButton, View editControl,
+                              Button editTitle, Button stateButton) {
+        mEditTitle = editTitle;
+        mFilterTitle = stateButton;
+        mButton = editTitle;
+        setMenuIcon(true);
+        setUtilityPanelUI(actionButton, editControl);
+    }
+
+    public boolean showsPopupIndicator() {
+        return true;
+    }
+
+    /**
+     * @param actionButton the would be the area for menu etc
+     * @param editControl this is the black area for sliders etc
+     */
+    public void setUtilityPanelUI(View actionButton, View editControl) {
+
+        AttributeSet aset;
+        Context context = editControl.getContext();
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        LinearLayout lp = (LinearLayout) inflater.inflate(
+                R.layout.filtershow_seekbar, (ViewGroup) editControl, true);
+        mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar);
+        mSeekBar.setOnSeekBarChangeListener(this);
+
+        if (showsSeekBar()) {
+            mSeekBar.setOnSeekBarChangeListener(this);
+            mSeekBar.setVisibility(View.VISIBLE);
+        } else {
+            mSeekBar.setVisibility(View.INVISIBLE);
+        }
+
+        if (mButton != null) {
+            if (showsPopupIndicator()) {
+                mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0,
+                        R.drawable.filtershow_menu_marker, 0);
+            } else {
+                mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
+            }
+        }
+    }
+
+    @Override
+    public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) {
+
+    }
+
+    public void setPanel() {
+
+    }
+
+    public void createEditor(Context context,FrameLayout frameLayout) {
+        mContext = context;
+        mFrameLayout = frameLayout;
+        mLocalRepresentation = null;
+    }
+
+    protected void unpack(int viewid, int layoutid) {
+
+        if (mView == null) {
+            mView = mFrameLayout.findViewById(viewid);
+            if (mView == null) {
+                LayoutInflater inflater = (LayoutInflater) mContext.getSystemService
+                        (Context.LAYOUT_INFLATER_SERVICE);
+                mView = inflater.inflate(layoutid, mFrameLayout, false);
+                mFrameLayout.addView(mView, mView.getLayoutParams());
+            }
+        }
+        mImageShow = findImageShow(mView);
+    }
+
+    private ImageShow findImageShow(View view) {
+        if (view instanceof ImageShow) {
+            return (ImageShow) view;
+        }
+        if (!(view instanceof ViewGroup)) {
+            return null;
+        }
+        ViewGroup vg = (ViewGroup) view;
+        int n = vg.getChildCount();
+        for (int i = 0; i < n; i++) {
+            View v = vg.getChildAt(i);
+            if (v instanceof ImageShow) {
+                return (ImageShow) v;
+            } else if (v instanceof ViewGroup) {
+                return findImageShow(v);
+            }
+        }
+        return null;
+    }
+
+    public View getTopLevelView() {
+        return mView;
+    }
+
+    public ImageShow getImageShow() {
+        return mImageShow;
+    }
+
+    public void setVisibility(int visible) {
+        mView.setVisibility(visible);
+    }
+
+    public FilterRepresentation getLocalRepresentation() {
+        if (mLocalRepresentation == null) {
+            ImagePreset preset = MasterImage.getImage().getPreset();
+            FilterRepresentation filterRepresentation = MasterImage.getImage().getCurrentFilterRepresentation();
+            mLocalRepresentation = preset.getFilterRepresentationCopyFrom(filterRepresentation);
+            if (mShowParameter == SHOW_VALUE_UNDEFINED && filterRepresentation != null) {
+                boolean show = filterRepresentation.showParameterValue();
+                mShowParameter = show ? SHOW_VALUE_INT : SHOW_VALUE_OFF;
+            }
+
+        }
+        return mLocalRepresentation;
+    }
+
+    /**
+     *  Call this to update the preset in MasterImage with the current representation
+     *  returned by getLocalRepresentation.  This causes the preview bitmap to be
+     *  regenerated.
+     */
+    public void commitLocalRepresentation() {
+        commitLocalRepresentation(getLocalRepresentation());
+    }
+
+    /**
+     *  Call this to update the preset in MasterImage with a given representation.
+     *  This causes the preview bitmap to be regenerated.
+     */
+    public void commitLocalRepresentation(FilterRepresentation rep) {
+        ArrayList<FilterRepresentation> filter = new ArrayList<FilterRepresentation>(1);
+        filter.add(rep);
+        commitLocalRepresentation(filter);
+    }
+
+    /**
+     *  Call this to update the preset in MasterImage with a collection of FilterRepresnations.
+     *  This causes the preview bitmap to be regenerated.
+     */
+    public void commitLocalRepresentation(Collection<FilterRepresentation> reps) {
+        ImagePreset preset = MasterImage.getImage().getPreset();
+        preset.updateFilterRepresentations(reps);
+        if (mButton != null) {
+            updateText();
+        }
+        if (mChangesGeometry) {
+            // Regenerate both the filtered and the geometry-only bitmaps
+            MasterImage.getImage().updatePresets(true);
+        } else {
+            // Regenerate only the filtered bitmap.
+            MasterImage.getImage().invalidateFiltersOnly();
+        }
+        preset.fillImageStateAdapter(MasterImage.getImage().getState());
+    }
+
+    /**
+     * This is called in response to a click to apply and leave the editor.
+     */
+    public void finalApplyCalled() {
+        commitLocalRepresentation();
+    }
+
+    protected void updateText() {
+        String s = "";
+        if (mLocalRepresentation != null) {
+            s = mContext.getString(mLocalRepresentation.getTextId());
+        }
+        mButton.setText(calculateUserMessage(mContext, s, ""));
+    }
+
+    /**
+     * called after the filter is set and the select is called
+     */
+    public void reflectCurrentFilter() {
+        mLocalRepresentation = null;
+        FilterRepresentation representation = getLocalRepresentation();
+        if (representation != null && mFilterTitle != null && representation.getTextId() != 0) {
+            String text = mContext.getString(representation.getTextId()).toUpperCase();
+            mFilterTitle.setText(text);
+            updateText();
+        }
+    }
+
+    public boolean useUtilityPanel() {
+        return true;
+    }
+
+    public void openUtilityPanel(LinearLayout mAccessoryViewList) {
+        setMenuIcon(false);
+        if (mImageShow != null) {
+            mImageShow.openUtilityPanel(mAccessoryViewList);
+        }
+    }
+
+    protected void setMenuIcon(boolean on) {
+        mEditTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                0, 0, on ? R.drawable.filtershow_menu_marker : 0, 0);
+    }
+
+    protected void createMenu(int[] strId, View button) {
+        PopupMenu pmenu = new PopupMenu(mContext, button);
+        Menu menu = pmenu.getMenu();
+        for (int i = 0; i < strId.length; i++) {
+            menu.add(Menu.NONE, Menu.FIRST + i, 0, mContext.getString(strId[i]));
+        }
+        setMenuIcon(true);
+
+    }
+
+    public Control[] getControls() {
+        return null;
+    }
+    @Override
+    public void onStartTrackingTouch(SeekBar arg0) {
+
+    }
+
+    @Override
+    public void onStopTrackingTouch(SeekBar arg0) {
+
+    }
+
+    @Override
+    public void swapLeft(MenuItem item) {
+
+    }
+
+    @Override
+    public void swapRight(MenuItem item) {
+
+    }
+
+    public void detach() {
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java b/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java
new file mode 100644
index 0000000..7e31f09
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.BasicParameterStyle;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.filters.FilterChanSatRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public class EditorChanSat extends ParametricEditor implements OnSeekBarChangeListener, FilterView {
+    public static final int ID = R.id.editorChanSat;
+    private final String LOGTAG = "EditorGrunge";
+    private SwapButton mButton;
+    private final Handler mHandler = new Handler();
+
+    int[] mMenuStrings = {
+            R.string.editor_chan_sat_main,
+            R.string.editor_chan_sat_red,
+            R.string.editor_chan_sat_yellow,
+            R.string.editor_chan_sat_green,
+            R.string.editor_chan_sat_cyan,
+            R.string.editor_chan_sat_blue,
+            R.string.editor_chan_sat_magenta
+    };
+
+    String mCurrentlyEditing = null;
+
+    public EditorChanSat() {
+        super(ID, R.layout.filtershow_default_editor, R.id.basicEditor);
+    }
+
+    @Override
+    public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep == null || !(rep instanceof FilterChanSatRepresentation)) {
+            return "";
+        }
+        FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep;
+        int mode = csrep.getParameterMode();
+        String paramString;
+
+        paramString = mContext.getString(mMenuStrings[mode]);
+
+        int val = csrep.getCurrentParameter();
+        return paramString + ((val > 0) ? " +" : " ") + val;
+    }
+
+    @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        mButton = (SwapButton) accessoryViewList.findViewById(R.id.applyEffect);
+        mButton.setText(mContext.getString(R.string.editor_chan_sat_main));
+
+        final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), mButton);
+
+        popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_chan_sat, popupMenu.getMenu());
+
+        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                selectMenuItem(item);
+                return true;
+            }
+        });
+        mButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                popupMenu.show();
+            }
+        });
+        mButton.setListener(this);
+
+        FilterChanSatRepresentation csrep = getChanSatRep();
+        String menuString = mContext.getString(mMenuStrings[0]);
+        switchToMode(csrep, FilterChanSatRepresentation.MODE_MASTER, menuString);
+
+    }
+
+    public int getParameterIndex(int id) {
+        switch (id) {
+            case R.id.editor_chan_sat_main:
+                return FilterChanSatRepresentation.MODE_MASTER;
+            case R.id.editor_chan_sat_red:
+                return FilterChanSatRepresentation.MODE_RED;
+            case R.id.editor_chan_sat_yellow:
+                return FilterChanSatRepresentation.MODE_YELLOW;
+            case R.id.editor_chan_sat_green:
+                return FilterChanSatRepresentation.MODE_GREEN;
+            case R.id.editor_chan_sat_cyan:
+                return FilterChanSatRepresentation.MODE_CYAN;
+            case R.id.editor_chan_sat_blue:
+                return FilterChanSatRepresentation.MODE_BLUE;
+            case R.id.editor_chan_sat_magenta:
+                return FilterChanSatRepresentation.MODE_MAGENTA;
+        }
+        return -1;
+    }
+
+    @Override
+    public void detach() {
+        mButton.setListener(null);
+        mButton.setOnClickListener(null);
+    }
+
+    private void updateSeekBar(FilterChanSatRepresentation rep) {
+        mControl.updateUI();
+    }
+
+    @Override
+    protected Parameter getParameterToEdit(FilterRepresentation rep) {
+        if (rep instanceof FilterChanSatRepresentation) {
+            FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep;
+            Parameter param = csrep.getFilterParameter(csrep.getParameterMode());
+            if (param instanceof BasicParameterStyle) {
+                param.setFilterView(EditorChanSat.this);
+            }
+            return param;
+        }
+        return null;
+    }
+
+    private FilterChanSatRepresentation getChanSatRep() {
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep != null
+                && rep instanceof FilterChanSatRepresentation) {
+            FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep;
+            return csrep;
+        }
+        return null;
+    }
+
+    @Override
+    public void computeIcon(int n, RenderingRequestCaller caller) {
+        FilterChanSatRepresentation rep = getChanSatRep();
+        if (rep == null) return;
+        rep = (FilterChanSatRepresentation) rep.copy();
+        ImagePreset preset = new ImagePreset();
+        preset.addFilter(rep);
+        Bitmap src = MasterImage.getImage().getThumbnailBitmap();
+        RenderingRequest.post(null, src, preset, RenderingRequest.STYLE_ICON_RENDERING,
+                caller);
+    }
+
+    protected void selectMenuItem(MenuItem item) {
+        if (getLocalRepresentation() != null
+                && getLocalRepresentation() instanceof FilterChanSatRepresentation) {
+            FilterChanSatRepresentation csrep =
+                    (FilterChanSatRepresentation) getLocalRepresentation();
+
+            switchToMode(csrep, getParameterIndex(item.getItemId()), item.getTitle().toString());
+
+        }
+    }
+
+    protected void switchToMode(FilterChanSatRepresentation csrep, int mode, String title) {
+        csrep.setParameterMode(mode);
+        mCurrentlyEditing = title;
+        mButton.setText(mCurrentlyEditing);
+        {
+            Parameter param = getParameterToEdit(csrep);
+
+            control(param, mEditControl);
+        }
+        updateSeekBar(csrep);
+        mView.invalidate();
+    }
+
+    @Override
+    public void swapLeft(MenuItem item) {
+        super.swapLeft(item);
+        mButton.setTranslationX(0);
+        mButton.animate().translationX(mButton.getWidth()).setDuration(SwapButton.ANIM_DURATION);
+        Runnable updateButton = new Runnable() {
+            @Override
+            public void run() {
+                mButton.animate().cancel();
+                mButton.setTranslationX(0);
+            }
+        };
+        mHandler.postDelayed(updateButton, SwapButton.ANIM_DURATION);
+        selectMenuItem(item);
+    }
+
+    @Override
+    public void swapRight(MenuItem item) {
+        super.swapRight(item);
+        mButton.setTranslationX(0);
+        mButton.animate().translationX(-mButton.getWidth()).setDuration(SwapButton.ANIM_DURATION);
+        Runnable updateButton = new Runnable() {
+            @Override
+            public void run() {
+                mButton.animate().cancel();
+                mButton.setTranslationX(0);
+            }
+        };
+        mHandler.postDelayed(updateButton, SwapButton.ANIM_DURATION);
+        selectMenuItem(item);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCrop.java b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java
new file mode 100644
index 0000000..511d4ff
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageCrop;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorCrop extends Editor implements EditorInfo {
+    public static final String TAG = EditorCrop.class.getSimpleName();
+    public static final int ID = R.id.editorCrop;
+
+    // Holder for an aspect ratio it's string id
+    protected static final class AspectInfo {
+        int mAspectX;
+        int mAspectY;
+        int mStringId;
+        AspectInfo(int stringID, int x, int y) {
+            mStringId = stringID;
+            mAspectX = x;
+            mAspectY = y;
+        }
+    };
+
+    // Mapping from menu id to aspect ratio
+    protected static final SparseArray<AspectInfo> sAspects;
+    static {
+        sAspects = new SparseArray<AspectInfo>();
+        sAspects.put(R.id.crop_menu_1to1, new AspectInfo(R.string.aspect1to1_effect, 1, 1));
+        sAspects.put(R.id.crop_menu_4to3, new AspectInfo(R.string.aspect4to3_effect, 4, 3));
+        sAspects.put(R.id.crop_menu_3to4, new AspectInfo(R.string.aspect3to4_effect, 3, 4));
+        sAspects.put(R.id.crop_menu_5to7, new AspectInfo(R.string.aspect5to7_effect, 5, 7));
+        sAspects.put(R.id.crop_menu_7to5, new AspectInfo(R.string.aspect7to5_effect, 7, 5));
+        sAspects.put(R.id.crop_menu_none, new AspectInfo(R.string.aspectNone_effect, 0, 0));
+        sAspects.put(R.id.crop_menu_original, new AspectInfo(R.string.aspectOriginal_effect, 0, 0));
+    }
+
+    protected ImageCrop mImageCrop;
+    private String mAspectString = "";
+
+    public EditorCrop() {
+        super(ID);
+        mChangesGeometry = true;
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageCrop == null) {
+            mImageCrop = new ImageCrop(context);
+        }
+        mView = mImageShow = mImageCrop;
+        mImageCrop.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        MasterImage master = MasterImage.getImage();
+        master.setCurrentFilterRepresentation(master.getPreset()
+                .getFilterWithSerializationName(FilterCropRepresentation.SERIALIZATION_NAME));
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep == null || rep instanceof FilterCropRepresentation) {
+            mImageCrop.setFilterCropRepresentation((FilterCropRepresentation) rep);
+        } else {
+            Log.w(TAG, "Could not reflect current filter, not of type: "
+                    + FilterCropRepresentation.class.getSimpleName());
+        }
+        mImageCrop.invalidate();
+    }
+
+    @Override
+    public void finalApplyCalled() {
+        commitLocalRepresentation(mImageCrop.getFinalRepresentation());
+    }
+
+    @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        view.setText(mContext.getString(R.string.crop));
+        view.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                showPopupMenu(accessoryViewList);
+            }
+        });
+    }
+
+    private void changeCropAspect(int itemId) {
+        AspectInfo info = sAspects.get(itemId);
+        if (info == null) {
+            throw new IllegalArgumentException("Invalid resource ID: " + itemId);
+        }
+        if (itemId == R.id.crop_menu_original) {
+            mImageCrop.applyOriginalAspect();
+        } else if (itemId == R.id.crop_menu_none) {
+            mImageCrop.applyFreeAspect();
+        } else {
+            mImageCrop.applyAspect(info.mAspectX, info.mAspectY);
+        }
+        setAspectString(mContext.getString(info.mStringId));
+    }
+
+    private void showPopupMenu(LinearLayout accessoryViewList) {
+        final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), button);
+        popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_crop, popupMenu.getMenu());
+        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                changeCropAspect(item.getItemId());
+                return true;
+            }
+        });
+        popupMenu.show();
+    }
+
+    @Override
+    public boolean showsSeekBar() {
+        return false;
+    }
+
+    @Override
+    public int getTextId() {
+        return R.string.crop;
+    }
+
+    @Override
+    public int getOverlayId() {
+        return R.drawable.filtershow_button_geometry_crop;
+    }
+
+    @Override
+    public boolean getOverlayOnly() {
+        return true;
+    }
+
+    private void setAspectString(String s) {
+        mAspectString = s;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCurves.java b/src/com/android/gallery3d/filtershow/editors/EditorCurves.java
new file mode 100644
index 0000000..83fbced
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorCurves.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageCurves;
+
+public class EditorCurves extends Editor {
+    public static final int ID = R.id.imageCurves;
+    ImageCurves mImageCurves;
+
+    public EditorCurves() {
+        super(ID);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        mView = mImageShow = mImageCurves = new ImageCurves(context);
+        mImageCurves.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep != null && getLocalRepresentation() instanceof FilterCurvesRepresentation) {
+            FilterCurvesRepresentation drawRep = (FilterCurvesRepresentation) rep;
+            mImageCurves.setFilterDrawRepresentation(drawRep);
+        }
+    }
+
+    @Override
+    public boolean showsSeekBar() {
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorDraw.java b/src/com/android/gallery3d/filtershow/editors/EditorDraw.java
new file mode 100644
index 0000000..4b09051
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorDraw.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.colorpicker.ColorGridDialog;
+import com.android.gallery3d.filtershow.colorpicker.RGBListener;
+import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilterDraw;
+import com.android.gallery3d.filtershow.imageshow.ImageDraw;
+
+public class EditorDraw extends Editor {
+    private static final String LOGTAG = "EditorDraw";
+    public static final int ID = R.id.editorDraw;
+    public ImageDraw mImageDraw;
+
+    public EditorDraw() {
+        super(ID);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        mView = mImageShow = mImageDraw = new ImageDraw(context);
+        mImageDraw.setEditor(this);
+
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+
+        if (rep != null && getLocalRepresentation() instanceof FilterDrawRepresentation) {
+            FilterDrawRepresentation drawRep = (FilterDrawRepresentation) getLocalRepresentation();
+            mImageDraw.setFilterDrawRepresentation(drawRep);
+        }
+    }
+
+    @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        view.setText(mContext.getString(R.string.draw_style));
+        view.setOnClickListener(new OnClickListener() {
+
+                @Override
+            public void onClick(View arg0) {
+                showPopupMenu(accessoryViewList);
+            }
+        });
+    }
+
+    @Override
+    public boolean showsSeekBar() {
+        return false;
+    }
+
+    private void showPopupMenu(LinearLayout accessoryViewList) {
+        final Button button = (Button) accessoryViewList.findViewById(
+                R.id.applyEffect);
+        if (button == null) {
+            return;
+        }
+        final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), button);
+        popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_draw, popupMenu.getMenu());
+        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                ImageFilterDraw filter = (ImageFilterDraw) mImageShow.getCurrentFilter();
+                if (item.getItemId() == R.id.draw_menu_color) {
+                    showColorGrid(item);
+                } else if (item.getItemId() == R.id.draw_menu_size) {
+                    showSizeDialog(item);
+                } else if (item.getItemId() == R.id.draw_menu_style_brush_marker) {
+                    ImageDraw idraw = (ImageDraw) mImageShow;
+                    idraw.setStyle(ImageFilterDraw.BRUSH_STYLE_MARKER);
+                } else if (item.getItemId() == R.id.draw_menu_style_brush_spatter) {
+                    ImageDraw idraw = (ImageDraw) mImageShow;
+                    idraw.setStyle(ImageFilterDraw.BRUSH_STYLE_SPATTER);
+                } else if (item.getItemId() == R.id.draw_menu_style_line) {
+                    ImageDraw idraw = (ImageDraw) mImageShow;
+                    idraw.setStyle(ImageFilterDraw.SIMPLE_STYLE);
+                } else if (item.getItemId() == R.id.draw_menu_clear) {
+                    ImageDraw idraw = (ImageDraw) mImageShow;
+                    idraw.resetParameter();
+                    commitLocalRepresentation();
+                }
+                mView.invalidate();
+                return true;
+            }
+        });
+        popupMenu.show();
+    }
+
+    public void showSizeDialog(final MenuItem item) {
+        FilterShowActivity ctx = mImageShow.getActivity();
+        final Dialog dialog = new Dialog(ctx);
+        dialog.setTitle(R.string.draw_size_title);
+        dialog.setContentView(R.layout.filtershow_draw_size);
+        final SeekBar bar = (SeekBar) dialog.findViewById(R.id.sizeSeekBar);
+        ImageDraw idraw = (ImageDraw) mImageShow;
+        bar.setProgress(idraw.getSize());
+        Button button = (Button) dialog.findViewById(R.id.sizeAcceptButton);
+        button.setOnClickListener(new OnClickListener() {
+
+            @Override
+            public void onClick(View arg0) {
+                int p = bar.getProgress();
+                ImageDraw idraw = (ImageDraw) mImageShow;
+                idraw.setSize(p + 1);
+                dialog.dismiss();
+            }
+        });
+        dialog.show();
+    }
+
+    public void showColorGrid(final MenuItem item) {
+        RGBListener cl = new RGBListener() {
+            @Override
+            public void setColor(int rgb) {
+                ImageDraw idraw = (ImageDraw) mImageShow;
+                idraw.setColor(rgb);
+            }
+        };
+        ColorGridDialog cpd = new ColorGridDialog(mImageShow.getActivity(), cl);
+        cpd.show();
+        LayoutParams params = cpd.getWindow().getAttributes();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorGrad.java b/src/com/android/gallery3d/filtershow/editors/EditorGrad.java
new file mode 100644
index 0000000..f427ccb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorGrad.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.ToggleButton;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterActionAndInt;
+import com.android.gallery3d.filtershow.filters.FilterGradRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageGrad;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorGrad extends ParametricEditor
+        implements OnSeekBarChangeListener, ParameterActionAndInt {
+    private static final String LOGTAG = "EditorGrad";
+    public static final int ID = R.id.editorGrad;
+    PopupMenu mPopupMenu;
+    ToggleButton mAddModeButton;
+    String mEffectName = "";
+    private static final int MODE_BRIGHTNESS = FilterGradRepresentation.PARAM_BRIGHTNESS;
+    private static final int MODE_SATURATION = FilterGradRepresentation.PARAM_SATURATION;
+    private static final int MODE_CONTRAST = FilterGradRepresentation.PARAM_CONTRAST;
+    private static final int ADD_ICON = R.drawable.ic_grad_add;
+    private static final int DEL_ICON = R.drawable.ic_grad_del;
+    private int mSliderMode = MODE_BRIGHTNESS;
+    ImageGrad mImageGrad;
+
+    public EditorGrad() {
+        super(ID, R.layout.filtershow_grad_editor, R.id.gradEditor);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        mImageGrad = (ImageGrad) mImageShow;
+        mImageGrad.setEditor(this);
+
+    }
+
+    public void clearAddMode() {
+        mAddModeButton.setChecked(false);
+        FilterRepresentation tmpRep = getLocalRepresentation();
+        if (tmpRep instanceof FilterGradRepresentation) {
+            updateMenuItems((FilterGradRepresentation) tmpRep);
+        }
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        FilterRepresentation tmpRep = getLocalRepresentation();
+        if (tmpRep instanceof FilterGradRepresentation) {
+            FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep;
+            boolean f = rep.showParameterValue();
+
+            mImageGrad.setRepresentation(rep);
+        }
+    }
+
+    public void updateSeekBar(FilterGradRepresentation rep) {
+        mControl.updateUI();
+    }
+
+    @Override
+    public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) {
+        FilterRepresentation tmpRep = getLocalRepresentation();
+        if (tmpRep instanceof FilterGradRepresentation) {
+            FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep;
+            int min = rep.getParameterMin(mSliderMode);
+            int value = progress + min;
+            rep.setParameter(mSliderMode, value);
+            mView.invalidate();
+            commitLocalRepresentation();
+        }
+    }
+
+    @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        view.setText(mContext.getString(R.string.editor_grad_brightness));
+        view.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                showPopupMenu(accessoryViewList);
+            }
+        });
+
+        setUpPopupMenu(view);
+        setEffectName();
+    }
+
+    private void updateMenuItems(FilterGradRepresentation rep) {
+        int n = rep.getNumberOfBands();
+    }
+
+    public void setEffectName() {
+        if (mPopupMenu != null) {
+            MenuItem item = mPopupMenu.getMenu().findItem(R.id.editor_grad_brightness);
+            mEffectName = item.getTitle().toString();
+        }
+    }
+
+    private void showPopupMenu(LinearLayout accessoryViewList) {
+        Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        if (button == null) {
+            return;
+        }
+
+        if (mPopupMenu == null) {
+            setUpPopupMenu(button);
+        }
+        mPopupMenu.show();
+    }
+
+    private void setUpPopupMenu(Button button) {
+        mPopupMenu = new PopupMenu(mImageShow.getActivity(), button);
+        mPopupMenu.getMenuInflater()
+                .inflate(R.menu.filtershow_menu_grad, mPopupMenu.getMenu());
+        FilterGradRepresentation rep = (FilterGradRepresentation) getLocalRepresentation();
+        if (rep == null) {
+            return;
+        }
+        updateMenuItems(rep);
+        hackFixStrings(mPopupMenu.getMenu());
+        setEffectName();
+        updateText();
+
+        mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                FilterRepresentation tmpRep = getLocalRepresentation();
+
+                if (tmpRep instanceof FilterGradRepresentation) {
+                    FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep;
+                    int cmdID = item.getItemId();
+                    switch (cmdID) {
+                        case R.id.editor_grad_brightness:
+                            mSliderMode = MODE_BRIGHTNESS;
+                            mEffectName = item.getTitle().toString();
+                            break;
+                        case R.id.editor_grad_contrast:
+                            mSliderMode = MODE_CONTRAST;
+                            mEffectName = item.getTitle().toString();
+                            break;
+                        case R.id.editor_grad_saturation:
+                            mSliderMode = MODE_SATURATION;
+                            mEffectName = item.getTitle().toString();
+                            break;
+                    }
+                    updateMenuItems(rep);
+                    updateSeekBar(rep);
+
+                    commitLocalRepresentation();
+                    mView.invalidate();
+                }
+                return true;
+            }
+        });
+    }
+
+    @Override
+    public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+        FilterGradRepresentation rep = getGradRepresentation();
+        if (rep == null) {
+            return mEffectName;
+        }
+        int val = rep.getParameter(mSliderMode);
+        return mEffectName.toUpperCase() + ((val > 0) ? " +" : " ") + val;
+    }
+
+    private FilterGradRepresentation getGradRepresentation() {
+        FilterRepresentation tmpRep = getLocalRepresentation();
+        if (tmpRep instanceof FilterGradRepresentation) {
+            return (FilterGradRepresentation) tmpRep;
+        }
+        return null;
+    }
+
+    @Override
+    public int getMaximum() {
+        FilterGradRepresentation rep = getGradRepresentation();
+        if (rep == null) {
+            return 0;
+        }
+        return rep.getParameterMax(mSliderMode);
+    }
+
+    @Override
+    public int getMinimum() {
+        FilterGradRepresentation rep = getGradRepresentation();
+        if (rep == null) {
+            return 0;
+        }
+        return rep.getParameterMin(mSliderMode);
+    }
+
+    @Override
+    public int getDefaultValue() {
+        return 0;
+    }
+
+    @Override
+    public int getValue() {
+        FilterGradRepresentation rep = getGradRepresentation();
+        if (rep == null) {
+            return 0;
+        }
+        return rep.getParameter(mSliderMode);
+    }
+
+    @Override
+    public String getValueString() {
+        return null;
+    }
+
+    @Override
+    public void setValue(int value) {
+        FilterGradRepresentation rep = getGradRepresentation();
+        if (rep == null) {
+            return;
+        }
+        rep.setParameter(mSliderMode, value);
+    }
+
+    @Override
+    public String getParameterName() {
+        return mEffectName;
+    }
+
+    @Override
+    public String getParameterType() {
+        return sParameterType;
+    }
+
+    @Override
+    public void setController(Control c) {
+
+    }
+
+    @Override
+    public void fireLeftAction() {
+        FilterGradRepresentation rep = getGradRepresentation();
+        if (rep == null) {
+            return;
+        }
+        rep.addBand(MasterImage.getImage().getOriginalBounds());
+        updateMenuItems(rep);
+        updateSeekBar(rep);
+
+        commitLocalRepresentation();
+        mView.invalidate();
+    }
+
+    @Override
+    public int getLeftIcon() {
+        return ADD_ICON;
+    }
+
+    @Override
+    public void fireRightAction() {
+        FilterGradRepresentation rep = getGradRepresentation();
+        if (rep == null) {
+            return;
+        }
+        rep.deleteCurrentBand();
+
+        updateMenuItems(rep);
+        updateSeekBar(rep);
+        commitLocalRepresentation();
+        mView.invalidate();
+    }
+
+    @Override
+    public int getRightIcon() {
+        return DEL_ICON;
+    }
+
+    @Override
+    public void setFilterView(FilterView editor) {
+
+    }
+
+    @Override
+    public void copyFrom(Parameter src) {
+
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorInfo.java b/src/com/android/gallery3d/filtershow/editors/EditorInfo.java
new file mode 100644
index 0000000..75afe49
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorInfo.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+public interface EditorInfo {
+    public int getTextId();
+    public int getOverlayId();
+    public boolean getOverlayOnly();
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorMirror.java b/src/com/android/gallery3d/filtershow/editors/EditorMirror.java
new file mode 100644
index 0000000..d6d9ee7
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorMirror.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageMirror;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorMirror extends Editor implements EditorInfo {
+    public static final String TAG = EditorMirror.class.getSimpleName();
+    public static final int ID = R.id.editorFlip;
+    ImageMirror mImageMirror;
+
+    public EditorMirror() {
+        super(ID);
+        mChangesGeometry = true;
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageMirror == null) {
+            mImageMirror = new ImageMirror(context);
+        }
+        mView = mImageShow = mImageMirror;
+        mImageMirror.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        MasterImage master = MasterImage.getImage();
+        master.setCurrentFilterRepresentation(master.getPreset()
+                .getFilterWithSerializationName(FilterMirrorRepresentation.SERIALIZATION_NAME));
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep == null || rep instanceof FilterMirrorRepresentation) {
+            mImageMirror.setFilterMirrorRepresentation((FilterMirrorRepresentation) rep);
+        } else {
+            Log.w(TAG, "Could not reflect current filter, not of type: "
+                    + FilterMirrorRepresentation.class.getSimpleName());
+        }
+        mImageMirror.invalidate();
+    }
+
+    @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                mImageMirror.flip();
+            }
+        });
+    }
+
+    @Override
+    public void finalApplyCalled() {
+        commitLocalRepresentation(mImageMirror.getFinalRepresentation());
+    }
+
+    @Override
+    public int getTextId() {
+        return R.string.mirror;
+    }
+
+    @Override
+    public int getOverlayId() {
+        return R.drawable.filtershow_button_geometry_flip;
+    }
+
+    @Override
+    public boolean getOverlayOnly() {
+        return true;
+    }
+
+    @Override
+    public boolean showsSeekBar() {
+        return false;
+    }
+
+    @Override
+    public boolean showsPopupIndicator() {
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorPanel.java b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java
new file mode 100644
index 0000000..bc4ca6a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.history.HistoryManager;
+import com.android.gallery3d.filtershow.category.MainPanel;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.state.StatePanel;
+
+public class EditorPanel extends Fragment {
+
+    private static final String LOGTAG = "EditorPanel";
+
+    private LinearLayout mMainView;
+    private Editor mEditor;
+    private int mEditorID;
+
+    public void setEditor(int editor) {
+        mEditorID = editor;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        FilterShowActivity filterShowActivity = (FilterShowActivity) activity;
+        mEditor = filterShowActivity.getEditor(mEditorID);
+    }
+
+    public void cancelCurrentFilter() {
+        MasterImage masterImage = MasterImage.getImage();
+        HistoryManager adapter = masterImage.getHistory();
+
+        int position = adapter.undo();
+        masterImage.onHistoryItemClick(position);
+        ((FilterShowActivity)getActivity()).invalidateViews();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        FilterShowActivity activity = (FilterShowActivity) getActivity();
+        if (mMainView != null) {
+            if (mMainView.getParent() != null) {
+                ViewGroup parent = (ViewGroup) mMainView.getParent();
+                parent.removeView(mMainView);
+            }
+            showImageStatePanel(activity.isShowingImageStatePanel());
+            return mMainView;
+        }
+        mMainView = (LinearLayout) inflater.inflate(R.layout.filtershow_editor_panel, null);
+
+        View actionControl = mMainView.findViewById(R.id.panelAccessoryViewList);
+        View editControl = mMainView.findViewById(R.id.controlArea);
+        ImageButton cancelButton = (ImageButton) mMainView.findViewById(R.id.cancelFilter);
+        ImageButton applyButton = (ImageButton) mMainView.findViewById(R.id.applyFilter);
+        Button editTitle = (Button) mMainView.findViewById(R.id.applyEffect);
+        cancelButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                cancelCurrentFilter();
+                FilterShowActivity activity = (FilterShowActivity) getActivity();
+                activity.backToMain();
+            }
+        });
+
+        Button toggleState = (Button) mMainView.findViewById(R.id.toggle_state);
+        mEditor = activity.getEditor(mEditorID);
+        if (mEditor != null) {
+            mEditor.setUpEditorUI(actionControl, editControl, editTitle, toggleState);
+            mEditor.reflectCurrentFilter();
+            if (mEditor.useUtilityPanel()) {
+                mEditor.openUtilityPanel((LinearLayout) actionControl);
+            }
+        }
+        applyButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                FilterShowActivity activity = (FilterShowActivity) getActivity();
+                mEditor.finalApplyCalled();
+                activity.backToMain();
+            }
+        });
+
+        showImageStatePanel(activity.isShowingImageStatePanel());
+        return mMainView;
+    }
+
+    @Override
+    public void onDetach() {
+        if (mEditor != null) {
+            mEditor.detach();
+        }
+        super.onDetach();
+    }
+
+    public void showImageStatePanel(boolean show) {
+        if (mMainView.findViewById(R.id.state_panel_container) == null) {
+            return;
+        }
+        FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+        Fragment panel = getActivity().getSupportFragmentManager().findFragmentByTag(
+                MainPanel.FRAGMENT_TAG);
+        if (panel == null || panel instanceof MainPanel) {
+            transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
+        }
+        if (show) {
+            StatePanel statePanel = new StatePanel();
+            transaction.replace(R.id.state_panel_container, statePanel, StatePanel.FRAGMENT_TAG);
+        } else {
+            Fragment statePanel = getChildFragmentManager().findFragmentByTag(StatePanel.FRAGMENT_TAG);
+            if (statePanel != null) {
+                transaction.remove(statePanel);
+            }
+        }
+        transaction.commit();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java
new file mode 100644
index 0000000..b0e88dd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRedEyeRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageRedEye;
+
+/**
+ * The editor with no slider for filters without UI
+ */
+public class EditorRedEye extends Editor {
+    public static int ID = R.id.editorRedEye;
+    private final String LOGTAG = "EditorRedEye";
+    ImageRedEye mImageRedEyes;
+
+    public EditorRedEye() {
+        super(ID);
+    }
+
+    protected EditorRedEye(int id) {
+        super(id);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        mView = mImageShow = mImageRedEyes=  new ImageRedEye(context);
+        mImageRedEyes.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep != null && getLocalRepresentation() instanceof FilterRedEyeRepresentation) {
+            FilterRedEyeRepresentation redEyeRep = (FilterRedEyeRepresentation) rep;
+
+            mImageRedEyes.setRepresentation(redEyeRep);
+        }
+    }
+
+    @Override
+    public boolean showsSeekBar() {
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorRotate.java b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
new file mode 100644
index 0000000..9452bf0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageRotate;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorRotate extends Editor implements EditorInfo {
+    public static final String TAG = EditorRotate.class.getSimpleName();
+    public static final int ID = R.id.editorRotate;
+    ImageRotate mImageRotate;
+
+    public EditorRotate() {
+        super(ID);
+        mChangesGeometry = true;
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageRotate == null) {
+            mImageRotate = new ImageRotate(context);
+        }
+        mView = mImageShow = mImageRotate;
+        mImageRotate.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        MasterImage master = MasterImage.getImage();
+        master.setCurrentFilterRepresentation(master.getPreset()
+                .getFilterWithSerializationName(FilterRotateRepresentation.SERIALIZATION_NAME));
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep == null || rep instanceof FilterRotateRepresentation) {
+            mImageRotate.setFilterRotateRepresentation((FilterRotateRepresentation) rep);
+        } else {
+            Log.w(TAG, "Could not reflect current filter, not of type: "
+                    + FilterRotateRepresentation.class.getSimpleName());
+        }
+        mImageRotate.invalidate();
+    }
+
+    @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                mImageRotate.rotate();
+                String displayVal = mContext.getString(getTextId()) + " "
+                        + mImageRotate.getLocalValue();
+                button.setText(displayVal);
+            }
+        });
+    }
+
+    @Override
+    public void finalApplyCalled() {
+        commitLocalRepresentation(mImageRotate.getFinalRepresentation());
+    }
+
+    @Override
+    public int getTextId() {
+        return R.string.rotate;
+    }
+
+    @Override
+    public int getOverlayId() {
+        return R.drawable.filtershow_button_geometry_rotate;
+    }
+
+    @Override
+    public boolean getOverlayOnly() {
+        return true;
+    }
+
+    @Override
+    public boolean showsSeekBar() {
+        return false;
+    }
+
+    @Override
+    public boolean showsPopupIndicator() {
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java b/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java
new file mode 100644
index 0000000..ff84ba8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageStraighten;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorStraighten extends Editor implements EditorInfo {
+    public static final String TAG = EditorStraighten.class.getSimpleName();
+    public static final int ID = R.id.editorStraighten;
+    ImageStraighten mImageStraighten;
+
+    public EditorStraighten() {
+        super(ID);
+        mShowParameter = SHOW_VALUE_INT;
+        mChangesGeometry = true;
+    }
+
+    @Override
+    public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+        String apply = context.getString(R.string.apply_effect);
+        apply += " " + effectName;
+        return apply.toUpperCase();
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageStraighten == null) {
+            mImageStraighten = new ImageStraighten(context);
+        }
+        mView = mImageShow = mImageStraighten;
+        mImageStraighten.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        MasterImage master = MasterImage.getImage();
+        master.setCurrentFilterRepresentation(master.getPreset().getFilterWithSerializationName(
+                FilterStraightenRepresentation.SERIALIZATION_NAME));
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep == null || rep instanceof FilterStraightenRepresentation) {
+            mImageStraighten
+                    .setFilterStraightenRepresentation((FilterStraightenRepresentation) rep);
+        } else {
+            Log.w(TAG, "Could not reflect current filter, not of type: "
+                    + FilterStraightenRepresentation.class.getSimpleName());
+        }
+        mImageStraighten.invalidate();
+    }
+
+    @Override
+    public void finalApplyCalled() {
+        commitLocalRepresentation(mImageStraighten.getFinalRepresentation());
+    }
+
+    @Override
+    public int getTextId() {
+        return R.string.straighten;
+    }
+
+    @Override
+    public int getOverlayId() {
+        return R.drawable.filtershow_button_geometry_straighten;
+    }
+
+    @Override
+    public boolean getOverlayOnly() {
+        return true;
+    }
+
+    @Override
+    public boolean showsSeekBar() {
+        return false;
+    }
+
+    @Override
+    public boolean showsPopupIndicator() {
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java
new file mode 100644
index 0000000..9376fbe
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet;
+
+public class EditorTinyPlanet extends BasicEditor {
+    public static final int ID = R.id.tinyPlanetEditor;
+    private static final String LOGTAG = "EditorTinyPlanet";
+    ImageTinyPlanet mImageTinyPlanet;
+
+    public EditorTinyPlanet() {
+        super(ID, R.layout.filtershow_tiny_planet_editor, R.id.imageTinyPlanet);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        mImageTinyPlanet = (ImageTinyPlanet) mImageShow;
+        mImageTinyPlanet.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep != null && rep instanceof FilterTinyPlanetRepresentation) {
+            FilterTinyPlanetRepresentation drawRep = (FilterTinyPlanetRepresentation) rep;
+            mImageTinyPlanet.setRepresentation(drawRep);
+        }
+    }
+
+    public void updateUI() {
+        if (mControl != null) {
+            mControl.updateUI();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
new file mode 100644
index 0000000..7127b21
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageVignette;
+
+public class EditorVignette extends ParametricEditor {
+    public static final int ID = R.id.vignetteEditor;
+    private static final String LOGTAG = "EditorVignettePlanet";
+    ImageVignette mImageVignette;
+
+    public EditorVignette() {
+        super(ID, R.layout.filtershow_vignette_editor, R.id.imageVignette);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        mImageVignette = (ImageVignette) mImageShow;
+        mImageVignette.setEditor(this);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+
+        FilterRepresentation rep = getLocalRepresentation();
+        if (rep != null && getLocalRepresentation() instanceof FilterVignetteRepresentation) {
+            FilterVignetteRepresentation drawRep = (FilterVignetteRepresentation) rep;
+            mImageVignette.setRepresentation(drawRep);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorZoom.java b/src/com/android/gallery3d/filtershow/editors/EditorZoom.java
new file mode 100644
index 0000000..ea8e3d1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorZoom.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import com.android.gallery3d.R;
+
+public class EditorZoom extends BasicEditor {
+    public static final int ID = R.id.imageZoom;
+
+    public EditorZoom() {
+        super(ID, R.layout.filtershow_zoom_editor,R.id.imageZoom);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java b/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java
new file mode 100644
index 0000000..d4e66ed
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+
+/**
+ * The editor with no slider for filters without UI
+ */
+public class ImageOnlyEditor extends Editor {
+    public final static int ID = R.id.imageOnlyEditor;
+    private final String LOGTAG = "ImageOnlyEditor";
+
+    public ImageOnlyEditor() {
+        super(ID);
+    }
+
+    protected ImageOnlyEditor(int id) {
+        super(id);
+    }
+
+    public boolean useUtilityPanel() {
+        return false;
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        mView = mImageShow = new ImageShow(context);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java
new file mode 100644
index 0000000..9ec858c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.ActionSlider;
+import com.android.gallery3d.filtershow.controller.BasicSlider;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterActionAndInt;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
+import com.android.gallery3d.filtershow.controller.ParameterStyles;
+import com.android.gallery3d.filtershow.controller.StyleChooser;
+import com.android.gallery3d.filtershow.controller.TitledSlider;
+import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+
+public class ParametricEditor extends Editor {
+    private int mLayoutID;
+    private int mViewID;
+    public static int ID = R.id.editorParametric;
+    private final String LOGTAG = "ParametricEditor";
+    protected Control mControl;
+    public static final int MINIMUM_WIDTH = 600;
+    public static final int MINIMUM_HEIGHT = 800;
+    View mActionButton;
+    View mEditControl;
+    static HashMap<String, Class> portraitMap = new HashMap<String, Class>();
+    static HashMap<String, Class> landscapeMap = new HashMap<String, Class>();
+    static {
+        portraitMap.put(ParameterInteger.sParameterType, BasicSlider.class);
+        landscapeMap.put(ParameterInteger.sParameterType, TitledSlider.class);
+        portraitMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class);
+        landscapeMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class);
+        portraitMap.put(ParameterStyles.sParameterType, StyleChooser.class);
+        landscapeMap.put(ParameterStyles.sParameterType, StyleChooser.class);
+    }
+
+    static Constructor getConstructor(Class cl) {
+        try {
+            return cl.getConstructor(Context.class, ViewGroup.class);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public ParametricEditor() {
+        super(ID);
+    }
+
+    protected ParametricEditor(int id) {
+        super(id);
+    }
+
+    protected ParametricEditor(int id, int layoutID, int viewID) {
+        super(id);
+        mLayoutID = layoutID;
+        mViewID = viewID;
+    }
+
+    @Override
+    public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+        String apply = "";
+
+        if (mShowParameter == SHOW_VALUE_INT & useCompact(context)) {
+           if (getLocalRepresentation() instanceof FilterBasicRepresentation) {
+            FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+                apply += " " + effectName.toUpperCase() + " " + interval.getStateRepresentation();
+           } else {
+                apply += " " + effectName.toUpperCase() + " " + parameterValue;
+           }
+        } else {
+            apply += " " + effectName.toUpperCase();
+        }
+        return apply;
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        unpack(mViewID, mLayoutID);
+    }
+
+    @Override
+    public void reflectCurrentFilter() {
+        super.reflectCurrentFilter();
+        if (getLocalRepresentation() != null
+                && getLocalRepresentation() instanceof FilterBasicRepresentation) {
+            FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+            mControl.setPrameter(interval);
+        }
+    }
+
+    @Override
+    public Control[] getControls() {
+        BasicSlider slider = new BasicSlider();
+        return new Control[] {
+                slider
+        };
+    }
+
+    // TODO: need a better way to decide which representation
+    static boolean useCompact(Context context) {
+        WindowManager w = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE));
+        Point size = new Point();
+        w.getDefaultDisplay().getSize(size);
+        if (size.x < size.y) { // if tall than wider
+            return true;
+        }
+        if (size.x < MINIMUM_WIDTH) {
+            return true;
+        }
+        if (size.y < MINIMUM_HEIGHT) {
+            return true;
+        }
+        return false;
+    }
+
+    protected Parameter getParameterToEdit(FilterRepresentation rep) {
+        if (this instanceof Parameter) {
+            return (Parameter) this;
+        } else if (rep instanceof Parameter) {
+            return ((Parameter) rep);
+        }
+        return null;
+    }
+
+    @Override
+    public void setUtilityPanelUI(View actionButton, View editControl) {
+        mActionButton = actionButton;
+        mEditControl = editControl;
+        FilterRepresentation rep = getLocalRepresentation();
+        Parameter param = getParameterToEdit(rep);
+        if (param != null) {
+            control(param, editControl);
+        } else {
+            mSeekBar = new SeekBar(editControl.getContext());
+            LayoutParams lp = new LinearLayout.LayoutParams(
+                    LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+            mSeekBar.setLayoutParams(lp);
+            ((LinearLayout) editControl).addView(mSeekBar);
+            mSeekBar.setOnSeekBarChangeListener(this);
+        }
+    }
+
+    protected void control(Parameter p, View editControl) {
+        String pType = p.getParameterType();
+        Context context = editControl.getContext();
+        Class c = ((useCompact(context)) ? portraitMap : landscapeMap).get(pType);
+
+        if (c != null) {
+            try {
+                mControl = (Control) c.newInstance();
+                p.setController(mControl);
+                mControl.setUp((ViewGroup) editControl, p, this);
+            } catch (Exception e) {
+                Log.e(LOGTAG, "Error in loading Control ", e);
+            }
+        } else {
+            Log.e(LOGTAG, "Unable to find class for " + pType);
+            for (String string : portraitMap.keySet()) {
+                Log.e(LOGTAG, "for " + string + " use " + portraitMap.get(string));
+            }
+        }
+    }
+
+    @Override
+    public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) {
+    }
+
+    @Override
+    public void onStartTrackingTouch(SeekBar arg0) {
+    }
+
+    @Override
+    public void onStopTrackingTouch(SeekBar arg0) {
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/SwapButton.java b/src/com/android/gallery3d/filtershow/editors/SwapButton.java
new file mode 100644
index 0000000..bb4432e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/SwapButton.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.editors;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.widget.Button;
+
+public class SwapButton extends Button implements GestureDetector.OnGestureListener {
+
+    public static int ANIM_DURATION = 200;
+
+    public interface SwapButtonListener {
+        public void swapLeft(MenuItem item);
+        public void swapRight(MenuItem item);
+    }
+
+    private GestureDetector mDetector;
+    private SwapButtonListener mListener;
+    private Menu mMenu;
+    private int mCurrentMenuIndex;
+
+    public SwapButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mDetector = new GestureDetector(context, this);
+    }
+
+    public SwapButtonListener getListener() {
+        return mListener;
+    }
+
+    public void setListener(SwapButtonListener listener) {
+        mListener = listener;
+    }
+
+    public boolean onTouchEvent(MotionEvent me) {
+        if (!mDetector.onTouchEvent(me)) {
+            return super.onTouchEvent(me);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onDown(MotionEvent e) {
+        return true;
+    }
+
+    @Override
+    public void onShowPress(MotionEvent e) {
+    }
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+        callOnClick();
+        return true;
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        return false;
+    }
+
+    @Override
+    public void onLongPress(MotionEvent e) {
+    }
+
+    @Override
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        if (mMenu == null) {
+            return false;
+        }
+        if (e1.getX() - e2.getX() > 0) {
+            // right to left
+            mCurrentMenuIndex++;
+            if (mCurrentMenuIndex == mMenu.size()) {
+                mCurrentMenuIndex = 0;
+            }
+            if (mListener != null) {
+                mListener.swapRight(mMenu.getItem(mCurrentMenuIndex));
+            }
+        } else {
+            // left to right
+            mCurrentMenuIndex--;
+            if (mCurrentMenuIndex < 0) {
+                mCurrentMenuIndex = mMenu.size() - 1;
+            }
+            if (mListener != null) {
+                mListener.swapLeft(mMenu.getItem(mCurrentMenuIndex));
+            }
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
new file mode 100644
index 0000000..3fa9191
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Vector;
+
+public abstract class BaseFiltersManager implements FiltersManagerInterface {
+    protected HashMap<Class, ImageFilter> mFilters = null;
+    protected HashMap<String, FilterRepresentation> mRepresentationLookup = null;
+    private static final String LOGTAG = "BaseFiltersManager";
+
+    protected ArrayList<FilterRepresentation> mLooks = new ArrayList<FilterRepresentation>();
+    protected ArrayList<FilterRepresentation> mBorders = new ArrayList<FilterRepresentation>();
+    protected ArrayList<FilterRepresentation> mTools = new ArrayList<FilterRepresentation>();
+    protected ArrayList<FilterRepresentation> mEffects = new ArrayList<FilterRepresentation>();
+
+    protected void init() {
+        mFilters = new HashMap<Class, ImageFilter>();
+        mRepresentationLookup = new HashMap<String, FilterRepresentation>();
+        Vector<Class> filters = new Vector<Class>();
+        addFilterClasses(filters);
+        for (Class filterClass : filters) {
+            try {
+                Object filterInstance = filterClass.newInstance();
+                if (filterInstance instanceof ImageFilter) {
+                    mFilters.put(filterClass, (ImageFilter) filterInstance);
+
+                    FilterRepresentation rep =
+                        ((ImageFilter) filterInstance).getDefaultRepresentation();
+                    if (rep != null) {
+                        addRepresentation(rep);
+                    }
+                }
+            } catch (InstantiationException e) {
+                e.printStackTrace();
+            } catch (IllegalAccessException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public void addRepresentation(FilterRepresentation rep) {
+        mRepresentationLookup.put(rep.getSerializationName(), rep);
+    }
+
+    public FilterRepresentation createFilterFromName(String name) {
+        try {
+            return mRepresentationLookup.get(name).copy();
+        } catch (Exception e) {
+            Log.v(LOGTAG, "unable to generate a filter representation for \"" + name + "\"");
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public ImageFilter getFilter(Class c) {
+        return mFilters.get(c);
+    }
+
+    @Override
+    public ImageFilter getFilterForRepresentation(FilterRepresentation representation) {
+        return mFilters.get(representation.getFilterClass());
+    }
+
+    public FilterRepresentation getRepresentation(Class c) {
+        ImageFilter filter = mFilters.get(c);
+        if (filter != null) {
+            return filter.getDefaultRepresentation();
+        }
+        return null;
+    }
+
+    public void freeFilterResources(ImagePreset preset) {
+        if (preset == null) {
+            return;
+        }
+        Vector<ImageFilter> usedFilters = preset.getUsedFilters(this);
+        for (Class c : mFilters.keySet()) {
+            ImageFilter filter = mFilters.get(c);
+            if (!usedFilters.contains(filter)) {
+                filter.freeResources();
+            }
+        }
+    }
+
+    public void freeRSFilterScripts() {
+        for (Class c : mFilters.keySet()) {
+            ImageFilter filter = mFilters.get(c);
+            if (filter != null && filter instanceof ImageFilterRS) {
+                ((ImageFilterRS) filter).resetScripts();
+            }
+        }
+    }
+
+    protected void addFilterClasses(Vector<Class> filters) {
+        filters.add(ImageFilterTinyPlanet.class);
+        filters.add(ImageFilterRedEye.class);
+        filters.add(ImageFilterWBalance.class);
+        filters.add(ImageFilterExposure.class);
+        filters.add(ImageFilterVignette.class);
+        filters.add(ImageFilterGrad.class);
+        filters.add(ImageFilterContrast.class);
+        filters.add(ImageFilterShadows.class);
+        filters.add(ImageFilterHighlights.class);
+        filters.add(ImageFilterVibrance.class);
+        filters.add(ImageFilterSharpen.class);
+        filters.add(ImageFilterCurves.class);
+        filters.add(ImageFilterDraw.class);
+        filters.add(ImageFilterHue.class);
+        filters.add(ImageFilterChanSat.class);
+        filters.add(ImageFilterSaturated.class);
+        filters.add(ImageFilterBwFilter.class);
+        filters.add(ImageFilterNegative.class);
+        filters.add(ImageFilterEdge.class);
+        filters.add(ImageFilterKMeans.class);
+        filters.add(ImageFilterFx.class);
+        filters.add(ImageFilterBorder.class);
+        filters.add(ImageFilterParametricBorder.class);
+    }
+
+    public ArrayList<FilterRepresentation> getLooks() {
+        return mLooks;
+    }
+
+    public ArrayList<FilterRepresentation> getBorders() {
+        return mBorders;
+    }
+
+    public ArrayList<FilterRepresentation> getTools() {
+        return mTools;
+    }
+
+    public ArrayList<FilterRepresentation> getEffects() {
+        return mEffects;
+    }
+
+    public void addBorders(Context context) {
+
+    }
+
+    public void addLooks(Context context) {
+        int[] drawid = {
+                R.drawable.filtershow_fx_0005_punch,
+                R.drawable.filtershow_fx_0000_vintage,
+                R.drawable.filtershow_fx_0004_bw_contrast,
+                R.drawable.filtershow_fx_0002_bleach,
+                R.drawable.filtershow_fx_0001_instant,
+                R.drawable.filtershow_fx_0007_washout,
+                R.drawable.filtershow_fx_0003_blue_crush,
+                R.drawable.filtershow_fx_0008_washout_color,
+                R.drawable.filtershow_fx_0006_x_process
+        };
+
+        int[] fxNameid = {
+                R.string.ffx_punch,
+                R.string.ffx_vintage,
+                R.string.ffx_bw_contrast,
+                R.string.ffx_bleach,
+                R.string.ffx_instant,
+                R.string.ffx_washout,
+                R.string.ffx_blue_crush,
+                R.string.ffx_washout_color,
+                R.string.ffx_x_process
+        };
+
+        // Do not localize.
+        String[] serializationNames = {
+                "LUT3D_PUNCH",
+                "LUT3D_VINTAGE",
+                "LUT3D_BW",
+                "LUT3D_BLEACH",
+                "LUT3D_INSTANT",
+                "LUT3D_WASHOUT",
+                "LUT3D_BLUECRUSH",
+                "LUT3D_WASHOUT",
+                "LUT3D_XPROCESS"
+        };
+
+        FilterFxRepresentation nullFx =
+                new FilterFxRepresentation(context.getString(R.string.none),
+                        0, R.string.none);
+        mLooks.add(nullFx);
+
+        for (int i = 0; i < drawid.length; i++) {
+            FilterFxRepresentation fx = new FilterFxRepresentation(
+                    context.getString(fxNameid[i]), drawid[i], fxNameid[i]);
+            fx.setSerializationName(serializationNames[i]);
+            ImagePreset preset = new ImagePreset();
+            preset.addFilter(fx);
+            FilterUserPresetRepresentation rep = new FilterUserPresetRepresentation(
+                    context.getString(fxNameid[i]), preset, -1);
+            mLooks.add(rep);
+            addRepresentation(fx);
+        }
+    }
+
+    public void addEffects() {
+        mEffects.add(getRepresentation(ImageFilterTinyPlanet.class));
+        mEffects.add(getRepresentation(ImageFilterWBalance.class));
+        mEffects.add(getRepresentation(ImageFilterExposure.class));
+        mEffects.add(getRepresentation(ImageFilterVignette.class));
+        mEffects.add(getRepresentation(ImageFilterGrad.class));
+        mEffects.add(getRepresentation(ImageFilterContrast.class));
+        mEffects.add(getRepresentation(ImageFilterShadows.class));
+        mEffects.add(getRepresentation(ImageFilterHighlights.class));
+        mEffects.add(getRepresentation(ImageFilterVibrance.class));
+        mEffects.add(getRepresentation(ImageFilterSharpen.class));
+        mEffects.add(getRepresentation(ImageFilterCurves.class));
+        mEffects.add(getRepresentation(ImageFilterHue.class));
+        mEffects.add(getRepresentation(ImageFilterChanSat.class));
+        mEffects.add(getRepresentation(ImageFilterBwFilter.class));
+        mEffects.add(getRepresentation(ImageFilterNegative.class));
+        mEffects.add(getRepresentation(ImageFilterEdge.class));
+        mEffects.add(getRepresentation(ImageFilterKMeans.class));
+    }
+
+    public void addTools(Context context) {
+
+        int[] editorsId = {
+                EditorCrop.ID,
+                EditorStraighten.ID,
+                EditorRotate.ID,
+                EditorMirror.ID
+        };
+
+        int[] textId = {
+                R.string.crop,
+                R.string.straighten,
+                R.string.rotate,
+                R.string.mirror
+        };
+
+        int[] overlayId = {
+                R.drawable.filtershow_button_geometry_crop,
+                R.drawable.filtershow_button_geometry_straighten,
+                R.drawable.filtershow_button_geometry_rotate,
+                R.drawable.filtershow_button_geometry_flip
+        };
+
+        FilterRepresentation[] geometryFilters = {
+                new FilterCropRepresentation(),
+                new FilterStraightenRepresentation(),
+                new FilterRotateRepresentation(),
+                new FilterMirrorRepresentation()
+        };
+
+        for (int i = 0; i < editorsId.length; i++) {
+            int editorId = editorsId[i];
+            FilterRepresentation geometry = geometryFilters[i];
+            geometry.setEditorId(editorId);
+            geometry.setTextId(textId[i]);
+            geometry.setOverlayId(overlayId[i]);
+            geometry.setOverlayOnly(true);
+            if (geometry.getTextId() != 0) {
+                geometry.setName(context.getString(geometry.getTextId()));
+            }
+            mTools.add(geometry);
+        }
+
+        mTools.add(getRepresentation(ImageFilterRedEye.class));
+        mTools.add(getRepresentation(ImageFilterDraw.class));
+    }
+
+    public void setFilterResources(Resources resources) {
+        ImageFilterBorder filterBorder = (ImageFilterBorder) getFilter(ImageFilterBorder.class);
+        filterBorder.setResources(resources);
+        ImageFilterFx filterFx = (ImageFilterFx) getFilter(ImageFilterFx.class);
+        filterFx.setResources(resources);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java b/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java
new file mode 100644
index 0000000..7c307a9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import java.util.Arrays;
+
+public class ColorSpaceMatrix {
+    private final float[] mMatrix = new float[16];
+    private static final float RLUM = 0.3086f;
+    private static final float GLUM = 0.6094f;
+    private static final float BLUM = 0.0820f;
+
+    public ColorSpaceMatrix() {
+        identity();
+    }
+
+    /**
+     * Copy constructor
+     *
+     * @param matrix
+     */
+    public ColorSpaceMatrix(ColorSpaceMatrix matrix) {
+        System.arraycopy(matrix.mMatrix, 0, mMatrix, 0, matrix.mMatrix.length);
+    }
+
+    /**
+     * get the matrix
+     *
+     * @return the internal matrix
+     */
+    public float[] getMatrix() {
+        return mMatrix;
+    }
+
+    /**
+     * set matrix to identity
+     */
+    public void identity() {
+        Arrays.fill(mMatrix, 0);
+        mMatrix[0] = mMatrix[5] = mMatrix[10] = mMatrix[15] = 1;
+    }
+
+    public void convertToLuminance() {
+        mMatrix[0] = mMatrix[1] = mMatrix[2] = 0.3086f;
+        mMatrix[4] = mMatrix[5] = mMatrix[6] = 0.6094f;
+        mMatrix[8] = mMatrix[9] = mMatrix[10] = 0.0820f;
+    }
+
+    private void multiply(float[] a)
+    {
+        int x, y;
+        float[] temp = new float[16];
+
+        for (y = 0; y < 4; y++) {
+            int y4 = y * 4;
+            for (x = 0; x < 4; x++) {
+                temp[y4 + x] = mMatrix[y4 + 0] * a[x]
+                        + mMatrix[y4 + 1] * a[4 + x]
+                        + mMatrix[y4 + 2] * a[8 + x]
+                        + mMatrix[y4 + 3] * a[12 + x];
+            }
+        }
+        for (int i = 0; i < 16; i++)
+            mMatrix[i] = temp[i];
+    }
+
+    private void xRotateMatrix(float rs, float rc)
+    {
+        ColorSpaceMatrix c = new ColorSpaceMatrix();
+        float[] tmp = c.mMatrix;
+
+        tmp[5] = rc;
+        tmp[6] = rs;
+        tmp[9] = -rs;
+        tmp[10] = rc;
+
+        multiply(tmp);
+    }
+
+    private void yRotateMatrix(float rs, float rc)
+    {
+        ColorSpaceMatrix c = new ColorSpaceMatrix();
+        float[] tmp = c.mMatrix;
+
+        tmp[0] = rc;
+        tmp[2] = -rs;
+        tmp[8] = rs;
+        tmp[10] = rc;
+
+        multiply(tmp);
+    }
+
+    private void zRotateMatrix(float rs, float rc)
+    {
+        ColorSpaceMatrix c = new ColorSpaceMatrix();
+        float[] tmp = c.mMatrix;
+
+        tmp[0] = rc;
+        tmp[1] = rs;
+        tmp[4] = -rs;
+        tmp[5] = rc;
+        multiply(tmp);
+    }
+
+    private void zShearMatrix(float dx, float dy)
+    {
+        ColorSpaceMatrix c = new ColorSpaceMatrix();
+        float[] tmp = c.mMatrix;
+
+        tmp[2] = dx;
+        tmp[6] = dy;
+        multiply(tmp);
+    }
+
+    /**
+     * sets the transform to a shift in Hue
+     *
+     * @param rot rotation in degrees
+     */
+    public void setHue(float rot)
+    {
+        float mag = (float) Math.sqrt(2.0);
+        float xrs = 1 / mag;
+        float xrc = 1 / mag;
+        xRotateMatrix(xrs, xrc);
+        mag = (float) Math.sqrt(3.0);
+        float yrs = -1 / mag;
+        float yrc = (float) Math.sqrt(2.0) / mag;
+        yRotateMatrix(yrs, yrc);
+
+        float lx = getRedf(RLUM, GLUM, BLUM);
+        float ly = getGreenf(RLUM, GLUM, BLUM);
+        float lz = getBluef(RLUM, GLUM, BLUM);
+        float zsx = lx / lz;
+        float zsy = ly / lz;
+        zShearMatrix(zsx, zsy);
+
+        float zrs = (float) Math.sin(rot * Math.PI / 180.0);
+        float zrc = (float) Math.cos(rot * Math.PI / 180.0);
+        zRotateMatrix(zrs, zrc);
+        zShearMatrix(-zsx, -zsy);
+        yRotateMatrix(-yrs, yrc);
+        xRotateMatrix(-xrs, xrc);
+    }
+
+    /**
+     * set it to a saturation matrix
+     *
+     * @param s
+     */
+    public void changeSaturation(float s) {
+        mMatrix[0] = (1 - s) * RLUM + s;
+        mMatrix[1] = (1 - s) * RLUM;
+        mMatrix[2] = (1 - s) * RLUM;
+        mMatrix[4] = (1 - s) * GLUM;
+        mMatrix[5] = (1 - s) * GLUM + s;
+        mMatrix[6] = (1 - s) * GLUM;
+        mMatrix[8] = (1 - s) * BLUM;
+        mMatrix[9] = (1 - s) * BLUM;
+        mMatrix[10] = (1 - s) * BLUM + s;
+    }
+
+    /**
+     * Transform RGB value
+     *
+     * @param r red pixel value
+     * @param g green pixel value
+     * @param b blue pixel value
+     * @return computed red pixel value
+     */
+    public float getRed(int r, int g, int b) {
+        return r * mMatrix[0] + g * mMatrix[4] + b * mMatrix[8] + mMatrix[12];
+    }
+
+    /**
+     * Transform RGB value
+     *
+     * @param r red pixel value
+     * @param g green pixel value
+     * @param b blue pixel value
+     * @return computed green pixel value
+     */
+    public float getGreen(int r, int g, int b) {
+        return r * mMatrix[1] + g * mMatrix[5] + b * mMatrix[9] + mMatrix[13];
+    }
+
+    /**
+     * Transform RGB value
+     *
+     * @param r red pixel value
+     * @param g green pixel value
+     * @param b blue pixel value
+     * @return computed blue pixel value
+     */
+    public float getBlue(int r, int g, int b) {
+        return r * mMatrix[2] + g * mMatrix[6] + b * mMatrix[10] + mMatrix[14];
+    }
+
+    private float getRedf(float r, float g, float b) {
+        return r * mMatrix[0] + g * mMatrix[4] + b * mMatrix[8] + mMatrix[12];
+    }
+
+    private float getGreenf(float r, float g, float b) {
+        return r * mMatrix[1] + g * mMatrix[5] + b * mMatrix[9] + mMatrix[13];
+    }
+
+    private float getBluef(float r, float g, float b) {
+        return r * mMatrix[2] + g * mMatrix[6] + b * mMatrix[10] + mMatrix[14];
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
new file mode 100644
index 0000000..1eebdb5
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+
+import android.util.Log;
+
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
+
+public class FilterBasicRepresentation extends FilterRepresentation implements ParameterInteger {
+    private static final String LOGTAG = "FilterBasicRep";
+    private int mMinimum;
+    private int mValue;
+    private int mMaximum;
+    private int mDefaultValue;
+    private int mPreviewValue;
+    public static final String SERIAL_NAME = "Name";
+    public static final String SERIAL_VALUE = "Value";
+    private boolean mLogVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
+
+    public FilterBasicRepresentation(String name, int minimum, int value, int maximum) {
+        super(name);
+        mMinimum = minimum;
+        mMaximum = maximum;
+        setValue(value);
+    }
+
+    @Override
+    public String toString() {
+        return getName() + " : " + mMinimum + " < " + mValue + " < " + mMaximum;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterBasicRepresentation representation = new FilterBasicRepresentation(getName(),0,0,0);
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (a instanceof FilterBasicRepresentation) {
+            FilterBasicRepresentation representation = (FilterBasicRepresentation) a;
+            setMinimum(representation.getMinimum());
+            setMaximum(representation.getMaximum());
+            setValue(representation.getValue());
+            setDefaultValue(representation.getDefaultValue());
+            setPreviewValue(representation.getPreviewValue());
+        }
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+        if (representation instanceof FilterBasicRepresentation) {
+            FilterBasicRepresentation basic = (FilterBasicRepresentation) representation;
+            if (basic.mMinimum == mMinimum
+                    && basic.mMaximum == mMaximum
+                    && basic.mValue == mValue
+                    && basic.mDefaultValue == mDefaultValue
+                    && basic.mPreviewValue == mPreviewValue) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int getMinimum() {
+        return mMinimum;
+    }
+
+    public void setMinimum(int minimum) {
+        mMinimum = minimum;
+    }
+
+    @Override
+    public int getValue() {
+        return mValue;
+    }
+
+    @Override
+    public void setValue(int value) {
+        mValue = value;
+        if (mValue < mMinimum) {
+            mValue = mMinimum;
+        }
+        if (mValue > mMaximum) {
+            mValue = mMaximum;
+        }
+    }
+
+    @Override
+    public int getMaximum() {
+        return mMaximum;
+    }
+
+    public void setMaximum(int maximum) {
+        mMaximum = maximum;
+    }
+
+    public void setDefaultValue(int defaultValue) {
+        mDefaultValue = defaultValue;
+    }
+
+    @Override
+    public int getDefaultValue() {
+        return mDefaultValue;
+    }
+
+    public int getPreviewValue() {
+        return mPreviewValue;
+    }
+
+    public void setPreviewValue(int previewValue) {
+        mPreviewValue = previewValue;
+    }
+
+    @Override
+    public String getStateRepresentation() {
+        int val = getValue();
+        return ((val > 0) ? "+" : "") + val;
+    }
+
+    @Override
+    public String getParameterType(){
+        return sParameterType;
+    }
+
+    @Override
+    public void setController(Control control) {
+    }
+
+    @Override
+    public String getValueString() {
+        return getStateRepresentation();
+    }
+
+    @Override
+    public String getParameterName() {
+        return getName();
+    }
+
+    @Override
+    public void setFilterView(FilterView editor) {
+    }
+
+    @Override
+    public void copyFrom(Parameter src) {
+        useParametersFrom((FilterBasicRepresentation) src);
+    }
+
+    @Override
+    public String[][] serializeRepresentation() {
+        String[][] ret = {
+                {SERIAL_NAME  , getName() },
+                {SERIAL_VALUE , Integer.toString(mValue)}};
+        return ret;
+    }
+
+    @Override
+    public void deSerializeRepresentation(String[][] rep) {
+        super.deSerializeRepresentation(rep);
+        for (int i = 0; i < rep.length; i++) {
+            if (SERIAL_VALUE.equals(rep[i][0])) {
+                mValue = Integer.parseInt(rep[i][1]);
+                break;
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java
new file mode 100644
index 0000000..7ce67dd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.BasicParameterInt;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterSet;
+import com.android.gallery3d.filtershow.editors.EditorChanSat;
+import com.android.gallery3d.filtershow.imageshow.ControlPoint;
+import com.android.gallery3d.filtershow.imageshow.Spline;
+
+import java.io.IOException;
+import java.util.Vector;
+
+/**
+ * Representation for a filter that has per channel & Master saturation
+ */
+public class FilterChanSatRepresentation extends FilterRepresentation implements ParameterSet {
+    private static final String LOGTAG = "FilterChanSatRepresentation";
+    private static final String ARGS = "ARGS";
+    private static final String SERIALIZATION_NAME = "channelsaturation";
+
+    public static final int MODE_MASTER = 0;
+    public static final int MODE_RED = 1;
+    public static final int MODE_YELLOW = 2;
+    public static final int MODE_GREEN = 3;
+    public static final int MODE_CYAN = 4;
+    public static final int MODE_BLUE = 5;
+    public static final int MODE_MAGENTA = 6;
+    private int mParameterMode = MODE_MASTER;
+
+    private static int MINSAT = -100;
+    private static int MAXSAT = 100;
+    private BasicParameterInt mParamMaster = new BasicParameterInt(MODE_MASTER, 0, MINSAT, MAXSAT);
+    private BasicParameterInt mParamRed = new BasicParameterInt(MODE_RED, 0, MINSAT, MAXSAT);
+    private BasicParameterInt mParamYellow = new BasicParameterInt(MODE_YELLOW, 0, MINSAT, MAXSAT);
+    private BasicParameterInt mParamGreen = new BasicParameterInt(MODE_GREEN, 0, MINSAT, MAXSAT);
+    private BasicParameterInt mParamCyan = new BasicParameterInt(MODE_CYAN, 0, MINSAT, MAXSAT);
+    private BasicParameterInt mParamBlue = new BasicParameterInt(MODE_BLUE, 0, MINSAT, MAXSAT);
+    private BasicParameterInt mParamMagenta = new BasicParameterInt(MODE_MAGENTA, 0, MINSAT, MAXSAT);
+
+    private BasicParameterInt[] mAllParam = {
+            mParamMaster,
+            mParamRed,
+            mParamYellow,
+            mParamGreen,
+            mParamCyan,
+            mParamBlue,
+            mParamMagenta};
+
+    public FilterChanSatRepresentation() {
+        super("ChannelSaturation");
+        setTextId(R.string.saturation);
+        setFilterType(FilterRepresentation.TYPE_NORMAL);
+        setSerializationName(SERIALIZATION_NAME);
+        setFilterClass(ImageFilterChanSat.class);
+        setEditorId(EditorChanSat.ID);
+    }
+
+    public String toString() {
+        return getName() + " : " + mParamRed + ", " + mParamCyan + ", " + mParamRed
+                + ", " + mParamGreen + ", " + mParamMaster + ", " + mParamYellow;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterChanSatRepresentation representation = new FilterChanSatRepresentation();
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    public void useParametersFrom(FilterRepresentation a) {
+        if (a instanceof FilterChanSatRepresentation) {
+            FilterChanSatRepresentation representation = (FilterChanSatRepresentation) a;
+
+            for (int i = 0; i < mAllParam.length; i++) {
+                mAllParam[i].copyFrom(representation.mAllParam[i]);
+            }
+        }
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+        if (representation instanceof FilterChanSatRepresentation) {
+            FilterChanSatRepresentation rep = (FilterChanSatRepresentation) representation;
+            for (int i = 0; i < mAllParam.length; i++) {
+                if (rep.getValue(i) != getValue(i))
+                    return false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public int getValue(int mode) {
+        return mAllParam[mode].getValue();
+    }
+
+    public void setValue(int mode, int value) {
+        mAllParam[mode].setValue(value);
+    }
+
+    public int getMinimum() {
+        return mParamMaster.getMinimum();
+    }
+
+    public int getMaximum() {
+        return mParamMaster.getMaximum();
+    }
+
+    public int getParameterMode() {
+        return mParameterMode;
+    }
+
+    public void setParameterMode(int parameterMode) {
+        mParameterMode = parameterMode;
+    }
+
+    public int getCurrentParameter() {
+        return getValue(mParameterMode);
+    }
+
+    public void setCurrentParameter(int value) {
+        setValue(mParameterMode, value);
+    }
+
+    @Override
+    public int getNumberOfParameters() {
+        return 6;
+    }
+
+    @Override
+    public Parameter getFilterParameter(int index) {
+        return mAllParam[index];
+    }
+
+    @Override
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+
+        writer.name(ARGS);
+        writer.beginArray();
+        writer.value(getValue(MODE_MASTER));
+        writer.value(getValue(MODE_RED));
+        writer.value(getValue(MODE_YELLOW));
+        writer.value(getValue(MODE_GREEN));
+        writer.value(getValue(MODE_CYAN));
+        writer.value(getValue(MODE_BLUE));
+        writer.value(getValue(MODE_MAGENTA));
+        writer.endArray();
+        writer.endObject();
+    }
+
+    @Override
+    public void deSerializeRepresentation(JsonReader sreader) throws IOException {
+        sreader.beginObject();
+
+        while (sreader.hasNext()) {
+            String name = sreader.nextName();
+            if (name.startsWith(ARGS)) {
+                sreader.beginArray();
+                sreader.hasNext();
+                setValue(MODE_MASTER, sreader.nextInt());
+                sreader.hasNext();
+                setValue(MODE_RED, sreader.nextInt());
+                sreader.hasNext();
+                setValue(MODE_YELLOW, sreader.nextInt());
+                sreader.hasNext();
+                setValue(MODE_GREEN, sreader.nextInt());
+                sreader.hasNext();
+                setValue(MODE_CYAN, sreader.nextInt());
+                sreader.hasNext();
+                setValue(MODE_BLUE, sreader.nextInt());
+                sreader.hasNext();
+                setValue(MODE_MAGENTA, sreader.nextInt());
+                sreader.hasNext();
+                sreader.endArray();
+            } else {
+                sreader.skipValue();
+            }
+        }
+        sreader.endObject();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java
new file mode 100644
index 0000000..94eb206
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class FilterColorBorderRepresentation extends FilterRepresentation {
+    private int mColor;
+    private int mBorderSize;
+    private int mBorderRadius;
+
+    public FilterColorBorderRepresentation(int color, int size, int radius) {
+        super("ColorBorder");
+        mColor = color;
+        mBorderSize = size;
+        mBorderRadius = radius;
+        setFilterType(FilterRepresentation.TYPE_BORDER);
+        setTextId(R.string.borders);
+        setEditorId(ImageOnlyEditor.ID);
+        setShowParameterValue(false);
+    }
+
+    public String toString() {
+        return "FilterBorder: " + getName();
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterColorBorderRepresentation representation = new FilterColorBorderRepresentation(0,0,0);
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    public void useParametersFrom(FilterRepresentation a) {
+        if (a instanceof FilterColorBorderRepresentation) {
+            FilterColorBorderRepresentation representation = (FilterColorBorderRepresentation) a;
+            setName(representation.getName());
+            setColor(representation.getColor());
+            setBorderSize(representation.getBorderSize());
+            setBorderRadius(representation.getBorderRadius());
+        }
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+        if (representation instanceof FilterColorBorderRepresentation) {
+            FilterColorBorderRepresentation border = (FilterColorBorderRepresentation) representation;
+            if (border.mColor == mColor
+                    && border.mBorderSize == mBorderSize
+                    && border.mBorderRadius == mBorderRadius) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+
+    @Override
+    public int getTextId() {
+        return R.string.borders;
+    }
+
+    public int getColor() {
+        return mColor;
+    }
+
+    public void setColor(int color) {
+        mColor = color;
+    }
+
+    public int getBorderSize() {
+        return mBorderSize;
+    }
+
+    public void setBorderSize(int borderSize) {
+        mBorderSize = borderSize;
+    }
+
+    public int getBorderRadius() {
+        return mBorderRadius;
+    }
+
+    public void setBorderRadius(int borderRadius) {
+        mBorderRadius = borderRadius;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java
new file mode 100644
index 0000000..c1bd7b3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.RectF;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+
+import java.io.IOException;
+
+public class FilterCropRepresentation extends FilterRepresentation {
+    public static final String SERIALIZATION_NAME = "CROP";
+    public static final String[] BOUNDS = {
+            "C0", "C1", "C2", "C3"
+    };
+    private static final String TAG = FilterCropRepresentation.class.getSimpleName();
+
+    RectF mCrop = getNil();
+
+    public FilterCropRepresentation(RectF crop) {
+        super(FilterCropRepresentation.class.getSimpleName());
+        setSerializationName(SERIALIZATION_NAME);
+        setShowParameterValue(true);
+        setFilterClass(FilterCropRepresentation.class);
+        setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+        setTextId(R.string.crop);
+        setEditorId(EditorCrop.ID);
+        setCrop(crop);
+    }
+
+    public FilterCropRepresentation(FilterCropRepresentation m) {
+        this(m.mCrop);
+    }
+
+    public FilterCropRepresentation() {
+        this(sNilRect);
+    }
+
+    public void set(FilterCropRepresentation r) {
+        mCrop.set(r.mCrop);
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation rep) {
+        if (!(rep instanceof FilterCropRepresentation)) {
+            return false;
+        }
+        FilterCropRepresentation crop = (FilterCropRepresentation) rep;
+        if (mCrop.bottom != crop.mCrop.bottom
+            || mCrop.left != crop.mCrop.left
+            || mCrop.right != crop.mCrop.right
+            || mCrop.top != crop.mCrop.top) {
+            return false;
+        }
+        return true;
+    }
+
+    public RectF getCrop() {
+        return new RectF(mCrop);
+    }
+
+    public void getCrop(RectF r) {
+        r.set(mCrop);
+    }
+
+    public void setCrop(RectF crop) {
+        if (crop == null) {
+            throw new IllegalArgumentException("Argument to setCrop is null");
+        }
+        mCrop.set(crop);
+    }
+
+    /**
+     * Takes a crop rect contained by [0, 0, 1, 1] and scales it by the height
+     * and width of the image rect.
+     */
+    public static void findScaledCrop(RectF crop, int bitmapWidth, int bitmapHeight) {
+        crop.left *= bitmapWidth;
+        crop.top *= bitmapHeight;
+        crop.right *= bitmapWidth;
+        crop.bottom *= bitmapHeight;
+    }
+
+    /**
+     * Takes crop rect and normalizes it by scaling down by the height and width
+     * of the image rect.
+     */
+    public static void findNormalizedCrop(RectF crop, int bitmapWidth, int bitmapHeight) {
+        crop.left /= bitmapWidth;
+        crop.top /= bitmapHeight;
+        crop.right /= bitmapWidth;
+        crop.bottom /= bitmapHeight;
+    }
+
+    @Override
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        return new FilterCropRepresentation(this);
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        if (!(representation instanceof FilterCropRepresentation)) {
+            throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+        }
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (!(a instanceof FilterCropRepresentation)) {
+            throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+        }
+        setCrop(((FilterCropRepresentation) a).mCrop);
+    }
+
+    private static final RectF sNilRect = new RectF(0, 0, 1, 1);
+
+    @Override
+    public boolean isNil() {
+        return mCrop.equals(sNilRect);
+    }
+
+    public static RectF getNil() {
+        return new RectF(sNilRect);
+    }
+
+    @Override
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+        writer.name(BOUNDS[0]).value(mCrop.left);
+        writer.name(BOUNDS[1]).value(mCrop.top);
+        writer.name(BOUNDS[2]).value(mCrop.right);
+        writer.name(BOUNDS[3]).value(mCrop.bottom);
+        writer.endObject();
+    }
+
+    @Override
+    public void deSerializeRepresentation(JsonReader reader) throws IOException {
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (BOUNDS[0].equals(name)) {
+                mCrop.left = (float) reader.nextDouble();
+            } else if (BOUNDS[1].equals(name)) {
+                mCrop.top = (float) reader.nextDouble();
+            } else if (BOUNDS[2].equals(name)) {
+                mCrop.right = (float) reader.nextDouble();
+            } else if (BOUNDS[3].equals(name)) {
+                mCrop.bottom = (float) reader.nextDouble();
+            } else {
+                reader.skipValue();
+            }
+        }
+        reader.endObject();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
new file mode 100644
index 0000000..edab2a0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
@@ -0,0 +1,170 @@
+package com.android.gallery3d.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.imageshow.ControlPoint;
+import com.android.gallery3d.filtershow.imageshow.Spline;
+
+import java.io.IOException;
+
+/**
+ * TODO: Insert description here. (generated by hoford)
+ */
+public class FilterCurvesRepresentation extends FilterRepresentation {
+    private static final String LOGTAG = "FilterCurvesRepresentation";
+    public static final String SERIALIZATION_NAME = "Curve";
+    private static final int MAX_SPLINE_NUMBER = 4;
+
+    private Spline[] mSplines = new Spline[MAX_SPLINE_NUMBER];
+
+    public FilterCurvesRepresentation() {
+        super("Curves");
+        setSerializationName("CURVES");
+        setFilterClass(ImageFilterCurves.class);
+        setTextId(R.string.curvesRGB);
+        setOverlayId(R.drawable.filtershow_button_colors_curve);
+        setEditorId(R.id.imageCurves);
+        setShowParameterValue(false);
+        setSupportsPartialRendering(true);
+        reset();
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterCurvesRepresentation representation = new FilterCurvesRepresentation();
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (!(a instanceof FilterCurvesRepresentation)) {
+            Log.v(LOGTAG, "cannot use parameters from " + a);
+            return;
+        }
+        FilterCurvesRepresentation representation = (FilterCurvesRepresentation) a;
+        Spline[] spline = new Spline[MAX_SPLINE_NUMBER];
+        for (int i = 0; i < spline.length; i++) {
+            Spline sp = representation.mSplines[i];
+            if (sp != null) {
+                spline[i] = new Spline(sp);
+            } else {
+                spline[i] = new Spline();
+            }
+        }
+        mSplines = spline;
+    }
+
+    @Override
+    public boolean isNil() {
+        for (int i = 0; i < MAX_SPLINE_NUMBER; i++) {
+            if (getSpline(i) != null && !getSpline(i).isOriginal()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+
+        if (!(representation instanceof FilterCurvesRepresentation)) {
+            return false;
+        } else {
+            FilterCurvesRepresentation curve =
+                    (FilterCurvesRepresentation) representation;
+            for (int i = 0; i < MAX_SPLINE_NUMBER; i++) {
+                if (!getSpline(i).sameValues(curve.getSpline(i))) {
+                    return false;
+                }
+            }
+        }
+        // Every spline matches, therefore they are the same.
+        return true;
+    }
+
+    public void reset() {
+        Spline spline = new Spline();
+
+        spline.addPoint(0.0f, 1.0f);
+        spline.addPoint(1.0f, 0.0f);
+
+        for (int i = 0; i < MAX_SPLINE_NUMBER; i++) {
+            mSplines[i] = new Spline(spline);
+        }
+    }
+
+    public void setSpline(int splineIndex, Spline s) {
+        mSplines[splineIndex] = s;
+    }
+
+    public Spline getSpline(int splineIndex) {
+        return mSplines[splineIndex];
+    }
+
+    @Override
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+        {
+            writer.name(NAME_TAG);
+            writer.value(getName());
+            for (int i = 0; i < mSplines.length; i++) {
+                writer.name(SERIALIZATION_NAME + i);
+                writer.beginArray();
+                int nop = mSplines[i].getNbPoints();
+                for (int j = 0; j < nop; j++) {
+                    ControlPoint p = mSplines[i].getPoint(j);
+                    writer.beginArray();
+                    writer.value(p.x);
+                    writer.value(p.y);
+                    writer.endArray();
+                }
+                writer.endArray();
+            }
+
+        }
+        writer.endObject();
+    }
+
+    @Override
+    public void deSerializeRepresentation(JsonReader sreader) throws IOException {
+        sreader.beginObject();
+        Spline[] spline = new Spline[MAX_SPLINE_NUMBER];
+        while (sreader.hasNext()) {
+            String name = sreader.nextName();
+            if (NAME_TAG.equals(name)) {
+                setName(sreader.nextString());
+            } else if (name.startsWith(SERIALIZATION_NAME)) {
+                int curveNo = Integer.parseInt(name.substring(SERIALIZATION_NAME.length()));
+                spline[curveNo] = new Spline();
+                sreader.beginArray();
+                while (sreader.hasNext()) {
+                    sreader.beginArray();
+                    sreader.hasNext();
+                    float x = (float) sreader.nextDouble();
+                    sreader.hasNext();
+                    float y = (float) sreader.nextDouble();
+                    sreader.endArray();
+                    spline[curveNo].addPoint(x, y);
+                }
+                sreader.endArray();
+
+            }
+        }
+        mSplines = spline;
+        sreader.endObject();
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java
new file mode 100644
index 0000000..ac0cb74
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+public class FilterDirectRepresentation extends FilterRepresentation {
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterDirectRepresentation representation = new FilterDirectRepresentation(getName());
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    public FilterDirectRepresentation(String name) {
+        super(name);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
new file mode 100644
index 0000000..977dbea
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.Path;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorDraw;
+
+import java.util.Vector;
+
+public class FilterDrawRepresentation extends FilterRepresentation {
+    private static final String LOGTAG = "FilterDrawRepresentation";
+
+    public static class StrokeData implements Cloneable {
+        public byte mType;
+        public Path mPath;
+        public float mRadius;
+        public int mColor;
+        public int noPoints = 0;
+        @Override
+        public String toString() {
+            return "stroke(" + mType + ", path(" + (mPath) + "), " + mRadius + " , "
+                    + Integer.toHexString(mColor) + ")";
+        }
+        @Override
+        public StrokeData clone() throws CloneNotSupportedException {
+            return (StrokeData) super.clone();
+        }
+    }
+
+    private Vector<StrokeData> mDrawing = new Vector<StrokeData>();
+    private StrokeData mCurrent; // used in the currently drawing style
+
+    public FilterDrawRepresentation() {
+        super("Draw");
+        setFilterClass(ImageFilterDraw.class);
+        setSerializationName("DRAW");
+        setFilterType(FilterRepresentation.TYPE_VIGNETTE);
+        setTextId(R.string.imageDraw);
+        setEditorId(EditorDraw.ID);
+        setOverlayId(R.drawable.filtershow_drawing);
+        setOverlayOnly(true);
+    }
+
+    @Override
+    public String toString() {
+        return getName() + " : strokes=" + mDrawing.size()
+                + ((mCurrent == null) ? " no current "
+                        : ("draw=" + mCurrent.mType + " " + mCurrent.noPoints));
+    }
+
+    public Vector<StrokeData> getDrawing() {
+        return mDrawing;
+    }
+
+    public StrokeData getCurrentDrawing() {
+        return mCurrent;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterDrawRepresentation representation = new FilterDrawRepresentation();
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public boolean isNil() {
+        return getDrawing().isEmpty();
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (a instanceof FilterDrawRepresentation) {
+            FilterDrawRepresentation representation = (FilterDrawRepresentation) a;
+            try {
+                if (representation.mCurrent != null) {
+                    mCurrent = (StrokeData) representation.mCurrent.clone();
+                } else {
+                    mCurrent = null;
+                }
+                if (representation.mDrawing != null) {
+                    mDrawing = (Vector<StrokeData>) representation.mDrawing.clone();
+                } else {
+                    mDrawing = null;
+                }
+
+            } catch (CloneNotSupportedException e) {
+                e.printStackTrace();
+            }
+        } else {
+            Log.v(LOGTAG, "cannot use parameters from " + a);
+        }
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+        if (representation instanceof FilterDrawRepresentation) {
+            FilterDrawRepresentation fdRep = (FilterDrawRepresentation) representation;
+            if (fdRep.mDrawing.size() != mDrawing.size())
+                return false;
+            if (fdRep.mCurrent == null && mCurrent.mPath == null) {
+                return true;
+            }
+            if (fdRep.mCurrent != null && mCurrent.mPath != null) {
+                if (fdRep.mCurrent.noPoints == mCurrent.noPoints) {
+                    return true;
+                }
+                return false;
+            }
+        }
+        return false;
+    }
+
+    public void startNewSection(byte type, int color, float size, float x, float y) {
+        mCurrent = new StrokeData();
+        mCurrent.mColor = color;
+        mCurrent.mRadius = size;
+        mCurrent.mType = type;
+        mCurrent.mPath = new Path();
+        mCurrent.mPath.moveTo(x, y);
+        mCurrent.noPoints = 0;
+    }
+
+    public void addPoint(float x, float y) {
+        mCurrent.noPoints++;
+        mCurrent.mPath.lineTo(x, y);
+    }
+
+    public void endSection(float x, float y) {
+        mCurrent.mPath.lineTo(x, y);
+        mCurrent.noPoints++;
+        mDrawing.add(mCurrent);
+        mCurrent = null;
+    }
+
+    public void clearCurrentSection() {
+        mCurrent = null;
+    }
+
+    public void clear() {
+        mCurrent = null;
+        mDrawing.clear();
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
new file mode 100644
index 0000000..e5a6fdd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class FilterFxRepresentation extends FilterRepresentation {
+   private static final String LOGTAG = "FilterFxRepresentation";
+    // TODO: When implementing serialization, we should find a unique way of
+    // specifying bitmaps / names (the resource IDs being random)
+    private int mBitmapResource = 0;
+    private int mNameResource = 0;
+
+    public FilterFxRepresentation(String name, int bitmapResource, int nameResource) {
+        super(name);
+        setFilterClass(ImageFilterFx.class);
+        mBitmapResource = bitmapResource;
+        mNameResource = nameResource;
+        setFilterType(FilterRepresentation.TYPE_FX);
+        setTextId(nameResource);
+        setEditorId(ImageOnlyEditor.ID);
+        setShowParameterValue(false);
+        setSupportsPartialRendering(true);
+    }
+
+    @Override
+    public String toString() {
+        return "FilterFx: " + hashCode() + " : " + getName() + " bitmap rsc: " + mBitmapResource;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterFxRepresentation representation = new FilterFxRepresentation(getName(),0,0);
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public synchronized void useParametersFrom(FilterRepresentation a) {
+        if (a instanceof FilterFxRepresentation) {
+            FilterFxRepresentation representation = (FilterFxRepresentation) a;
+            setName(representation.getName());
+            setSerializationName(representation.getSerializationName());
+            setBitmapResource(representation.getBitmapResource());
+            setNameResource(representation.getNameResource());
+        }
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+        if (representation instanceof FilterFxRepresentation) {
+            FilterFxRepresentation fx = (FilterFxRepresentation) representation;
+            if (fx.mNameResource == mNameResource
+                    && fx.mBitmapResource == mBitmapResource) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean same(FilterRepresentation representation) {
+        if (!super.same(representation)) {
+            return false;
+        }
+        return equals(representation);
+    }
+
+    @Override
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+
+    public int getNameResource() {
+        return mNameResource;
+    }
+
+    public void setNameResource(int nameResource) {
+        mNameResource = nameResource;
+    }
+
+    public int getBitmapResource() {
+        return mBitmapResource;
+    }
+
+    public void setBitmapResource(int bitmapResource) {
+        mBitmapResource = bitmapResource;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java
new file mode 100644
index 0000000..0c272d4
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.Rect;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorGrad;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.imageshow.Line;
+
+import java.io.IOException;
+import java.util.Vector;
+
+public class FilterGradRepresentation extends FilterRepresentation
+        implements Line {
+    private static final String LOGTAG = "FilterGradRepresentation";
+    public static final int MAX_POINTS = 16;
+    public static final int PARAM_BRIGHTNESS = 0;
+    public static final int PARAM_SATURATION = 1;
+    public static final int PARAM_CONTRAST = 2;
+    private static final double ADD_MIN_DIST = .05;
+    private static String LINE_NAME = "Point";
+    private static final  String SERIALIZATION_NAME = "grad";
+
+    public FilterGradRepresentation() {
+        super("Grad");
+        setSerializationName(SERIALIZATION_NAME);
+        creatExample();
+        setOverlayId(R.drawable.filtershow_button_grad);
+        setFilterClass(ImageFilterGrad.class);
+        setTextId(R.string.grad);
+        setEditorId(EditorGrad.ID);
+    }
+
+    public void trimVector(){
+        int n = mBands.size();
+        for (int i = n; i < MAX_POINTS; i++) {
+            mBands.add(new Band());
+        }
+        for (int i = MAX_POINTS; i <  n; i++) {
+            mBands.remove(i);
+        }
+    }
+
+    Vector<Band> mBands = new Vector<Band>();
+    Band mCurrentBand;
+
+    static class Band {
+        private boolean mask = true;
+
+        private int xPos1 = -1;
+        private int yPos1 = 100;
+        private int xPos2 = -1;
+        private int yPos2 = 100;
+        private int brightness = 40;
+        private int contrast = 0;
+        private int saturation = 0;
+
+
+        public Band() {
+        }
+
+        public Band(int x, int y) {
+            xPos1 = x;
+            yPos1 = y+30;
+            xPos2 = x;
+            yPos2 = y-30;
+        }
+
+        public Band(Band copy) {
+            mask = copy.mask;
+            xPos1 = copy.xPos1;
+            yPos1 = copy.yPos1;
+            xPos2 = copy.xPos2;
+            yPos2 = copy.yPos2;
+            brightness = copy.brightness;
+            contrast = copy.contrast;
+            saturation = copy.saturation;
+        }
+
+    }
+
+    @Override
+    public String toString() {
+        int count = 0;
+        for (Band point : mBands) {
+            if (!point.mask) {
+                count++;
+            }
+        }
+        return "c=" + mBands.indexOf(mBands) + "[" + mBands.size() + "]" + count;
+    }
+
+    private void creatExample() {
+        Band p = new Band();
+        p.mask = false;
+        p.xPos1 = -1;
+        p.yPos1 = 100;
+        p.xPos2 = -1;
+        p.yPos2 = 100;
+        p.brightness = 40;
+        p.contrast = 0;
+        p.saturation = 0;
+        mBands.add(0, p);
+        mCurrentBand = p;
+        trimVector();
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+            FilterGradRepresentation rep = (FilterGradRepresentation) a;
+            Vector<Band> tmpBands = new Vector<Band>();
+            int n = (rep.mCurrentBand == null) ? 0 : rep.mBands.indexOf(rep.mCurrentBand);
+            for (Band band : rep.mBands) {
+                tmpBands.add(new Band(band));
+            }
+            mCurrentBand = null;
+            mBands = tmpBands;
+            mCurrentBand = mBands.elementAt(n);
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterGradRepresentation representation = new FilterGradRepresentation();
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (representation instanceof FilterGradRepresentation) {
+            FilterGradRepresentation rep = (FilterGradRepresentation) representation;
+            int n = getNumberOfBands();
+            if (rep.getNumberOfBands() != n) {
+                return false;
+            }
+            for (int i = 0; i < mBands.size(); i++) {
+                Band b1 = mBands.get(i);
+                Band b2 = rep.mBands.get(i);
+                if (b1.mask != b2.mask
+                        || b1.brightness != b2.brightness
+                        || b1.contrast != b2.contrast
+                        || b1.saturation != b2.saturation
+                        || b1.xPos1 != b2.xPos1
+                        || b1.xPos2 != b2.xPos2
+                        || b1.yPos1 != b2.yPos1
+                        || b1.yPos2 != b2.yPos2) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public int getNumberOfBands() {
+        int count = 0;
+        for (Band point : mBands) {
+            if (!point.mask) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    public int addBand(Rect rect) {
+        mBands.add(0, mCurrentBand = new Band(rect.centerX(), rect.centerY()));
+        mCurrentBand.mask = false;
+        int x = (mCurrentBand.xPos1 + mCurrentBand.xPos2)/2;
+        int y = (mCurrentBand.yPos1 + mCurrentBand.yPos2)/2;
+        double addDelta = ADD_MIN_DIST * Math.max(rect.width(), rect.height());
+        boolean moved = true;
+        int count = 0;
+        int toMove =  mBands.indexOf(mCurrentBand);
+
+        while (moved) {
+            moved = false;
+            count++;
+            if (count > 14) {
+                break;
+            }
+
+            for (Band point : mBands) {
+                if (point.mask) {
+                    break;
+                }
+            }
+
+            for (Band point : mBands) {
+                if (point.mask) {
+                    break;
+                }
+                int index = mBands.indexOf(point);
+
+                if (toMove != index) {
+                    double dist = Math.hypot(point.xPos1 - x, point.yPos1 - y);
+                    if (dist < addDelta) {
+                        moved = true;
+                        mCurrentBand.xPos1 += addDelta;
+                        mCurrentBand.yPos1 += addDelta;
+                        mCurrentBand.xPos2 += addDelta;
+                        mCurrentBand.yPos2 += addDelta;
+                        x = (mCurrentBand.xPos1 + mCurrentBand.xPos2)/2;
+                        y = (mCurrentBand.yPos1 + mCurrentBand.yPos2)/2;
+
+                        if (mCurrentBand.yPos1 > rect.bottom) {
+                            mCurrentBand.yPos1 = (int) (rect.top + addDelta);
+                        }
+                        if (mCurrentBand.xPos1 > rect.right) {
+                            mCurrentBand.xPos1 = (int) (rect.left + addDelta);
+                        }
+                    }
+                }
+            }
+        }
+        trimVector();
+        return 0;
+    }
+
+    public void deleteCurrentBand() {
+        int index = mBands.indexOf(mCurrentBand);
+        mBands.remove(mCurrentBand);
+        trimVector();
+        if (getNumberOfBands() == 0) {
+            addBand(MasterImage.getImage().getOriginalBounds());
+        }
+        mCurrentBand = mBands.get(0);
+    }
+
+    public void  nextPoint(){
+        int index = mBands.indexOf(mCurrentBand);
+        int tmp = index;
+        Band point;
+        int k = 0;
+        do  {
+            index =   (index+1)% mBands.size();
+            point = mBands.get(index);
+            if (k++ >= mBands.size()) {
+                break;
+            }
+        }
+        while (point.mask == true);
+        mCurrentBand = mBands.get(index);
+    }
+
+    public void setSelectedPoint(int pos) {
+        mCurrentBand = mBands.get(pos);
+    }
+
+    public int getSelectedPoint() {
+        return mBands.indexOf(mCurrentBand);
+    }
+
+    public boolean[] getMask() {
+        boolean[] ret = new boolean[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = !point.mask;
+        }
+        return ret;
+    }
+
+    public int[] getXPos1() {
+        int[] ret = new int[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = point.xPos1;
+        }
+        return ret;
+    }
+
+    public int[] getYPos1() {
+        int[] ret = new int[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = point.yPos1;
+        }
+        return ret;
+    }
+
+    public int[] getXPos2() {
+        int[] ret = new int[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = point.xPos2;
+        }
+        return ret;
+    }
+
+    public int[] getYPos2() {
+        int[] ret = new int[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = point.yPos2;
+        }
+        return ret;
+    }
+
+    public int[] getBrightness() {
+        int[] ret = new int[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = point.brightness;
+        }
+        return ret;
+    }
+
+    public int[] getContrast() {
+        int[] ret = new int[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = point.contrast;
+        }
+        return ret;
+    }
+
+    public int[] getSaturation() {
+        int[] ret = new int[mBands.size()];
+        int i = 0;
+        for (Band point : mBands) {
+            ret[i++] = point.saturation;
+        }
+        return ret;
+    }
+
+    public int getParameter(int type) {
+        switch (type){
+            case PARAM_BRIGHTNESS:
+                return mCurrentBand.brightness;
+            case PARAM_SATURATION:
+                return mCurrentBand.saturation;
+            case PARAM_CONTRAST:
+                return mCurrentBand.contrast;
+        }
+        throw new IllegalArgumentException("no such type " + type);
+    }
+
+    public int getParameterMax(int type) {
+        switch (type) {
+            case PARAM_BRIGHTNESS:
+                return 100;
+            case PARAM_SATURATION:
+                return 100;
+            case PARAM_CONTRAST:
+                return 100;
+        }
+        throw new IllegalArgumentException("no such type " + type);
+    }
+
+    public int getParameterMin(int type) {
+        switch (type) {
+            case PARAM_BRIGHTNESS:
+                return -100;
+            case PARAM_SATURATION:
+                return -100;
+            case PARAM_CONTRAST:
+                return -100;
+        }
+        throw new IllegalArgumentException("no such type " + type);
+    }
+
+    public void setParameter(int type, int value) {
+        mCurrentBand.mask = false;
+        switch (type) {
+            case PARAM_BRIGHTNESS:
+                mCurrentBand.brightness = value;
+                break;
+            case PARAM_SATURATION:
+                mCurrentBand.saturation = value;
+                break;
+            case PARAM_CONTRAST:
+                mCurrentBand.contrast = value;
+                break;
+            default:
+                throw new IllegalArgumentException("no such type " + type);
+        }
+    }
+
+    @Override
+    public void setPoint1(float x, float y) {
+        mCurrentBand.xPos1 = (int)x;
+        mCurrentBand.yPos1 = (int)y;
+    }
+
+    @Override
+    public void setPoint2(float x, float y) {
+        mCurrentBand.xPos2 = (int)x;
+        mCurrentBand.yPos2 = (int)y;
+    }
+
+    @Override
+    public float getPoint1X() {
+        return mCurrentBand.xPos1;
+    }
+
+    @Override
+    public float getPoint1Y() {
+        return mCurrentBand.yPos1;
+    }
+    @Override
+    public float getPoint2X() {
+        return mCurrentBand.xPos2;
+    }
+
+    @Override
+    public float getPoint2Y() {
+        return mCurrentBand.yPos2;
+    }
+
+    @Override
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+        int len = mBands.size();
+        int count = 0;
+
+        for (int i = 0; i < len; i++) {
+            Band point = mBands.get(i);
+            if (point.mask) {
+                continue;
+            }
+            writer.name(LINE_NAME + count);
+            count++;
+            writer.beginArray();
+            writer.value(point.xPos1);
+            writer.value(point.yPos1);
+            writer.value(point.xPos2);
+            writer.value(point.yPos2);
+            writer.value(point.brightness);
+            writer.value(point.contrast);
+            writer.value(point.saturation);
+            writer.endArray();
+        }
+        writer.endObject();
+    }
+
+    @Override
+    public void deSerializeRepresentation(JsonReader sreader) throws IOException {
+        sreader.beginObject();
+        Vector<Band> points = new Vector<Band>();
+
+        while (sreader.hasNext()) {
+            String name = sreader.nextName();
+            if (name.startsWith(LINE_NAME)) {
+                int pointNo = Integer.parseInt(name.substring(LINE_NAME.length()));
+                sreader.beginArray();
+                Band p = new Band();
+                p.mask = false;
+                sreader.hasNext();
+                p.xPos1 = sreader.nextInt();
+                sreader.hasNext();
+                p.yPos1 = sreader.nextInt();
+                sreader.hasNext();
+                p.xPos2 = sreader.nextInt();
+                sreader.hasNext();
+                p.yPos2 = sreader.nextInt();
+                sreader.hasNext();
+                p.brightness = sreader.nextInt();
+                sreader.hasNext();
+                p.contrast = sreader.nextInt();
+                sreader.hasNext();
+                p.saturation = sreader.nextInt();
+                sreader.hasNext();
+                sreader.endArray();
+                points.add(p);
+
+            } else {
+                sreader.skipValue();
+            }
+        }
+        mBands = points;
+        trimVector();
+        mCurrentBand = mBands.get(0);
+        sreader.endObject();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java
new file mode 100644
index 0000000..f310a2b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class FilterImageBorderRepresentation extends FilterRepresentation {
+    private int mDrawableResource = 0;
+
+    public FilterImageBorderRepresentation(int drawableResource) {
+        super("ImageBorder");
+        setFilterClass(ImageFilterBorder.class);
+        mDrawableResource = drawableResource;
+        setFilterType(FilterRepresentation.TYPE_BORDER);
+        setTextId(R.string.borders);
+        setEditorId(ImageOnlyEditor.ID);
+        setShowParameterValue(false);
+    }
+
+    public String toString() {
+        return "FilterBorder: " + getName();
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterImageBorderRepresentation representation =
+                new FilterImageBorderRepresentation(mDrawableResource);
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    public void useParametersFrom(FilterRepresentation a) {
+        if (a instanceof FilterImageBorderRepresentation) {
+            FilterImageBorderRepresentation representation = (FilterImageBorderRepresentation) a;
+            setName(representation.getName());
+            setDrawableResource(representation.getDrawableResource());
+        }
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+        if (representation instanceof FilterImageBorderRepresentation) {
+            FilterImageBorderRepresentation border = (FilterImageBorderRepresentation) representation;
+            if (border.mDrawableResource == mDrawableResource) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int getTextId() {
+        return R.string.none;
+    }
+
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+
+    public int getDrawableResource() {
+        return mDrawableResource;
+    }
+
+    public void setDrawableResource(int drawableResource) {
+        mDrawableResource = drawableResource;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java
new file mode 100644
index 0000000..8dcff0d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+
+import java.io.IOException;
+
+public class FilterMirrorRepresentation extends FilterRepresentation {
+    public static final String SERIALIZATION_NAME = "MIRROR";
+    private static final String SERIALIZATION_MIRROR_VALUE = "value";
+    private static final String TAG = FilterMirrorRepresentation.class.getSimpleName();
+
+    Mirror mMirror;
+
+    public enum Mirror {
+        NONE('N'), VERTICAL('V'), HORIZONTAL('H'), BOTH('B');
+        char mValue;
+
+        private Mirror(char value) {
+            mValue = value;
+        }
+
+        public char value() {
+            return mValue;
+        }
+
+        public static Mirror fromValue(char value) {
+            switch (value) {
+                case 'N':
+                    return NONE;
+                case 'V':
+                    return VERTICAL;
+                case 'H':
+                    return HORIZONTAL;
+                case 'B':
+                    return BOTH;
+                default:
+                    return null;
+            }
+        }
+    }
+
+    public FilterMirrorRepresentation(Mirror mirror) {
+        super(FilterMirrorRepresentation.class.getSimpleName());
+        setSerializationName(SERIALIZATION_NAME);
+        setShowParameterValue(true);
+        setFilterClass(FilterMirrorRepresentation.class);
+        setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+        setTextId(R.string.mirror);
+        setEditorId(EditorMirror.ID);
+        setMirror(mirror);
+    }
+
+    public FilterMirrorRepresentation(FilterMirrorRepresentation m) {
+        this(m.getMirror());
+    }
+
+    public FilterMirrorRepresentation() {
+        this(getNil());
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation rep) {
+        if (!(rep instanceof FilterMirrorRepresentation)) {
+            return false;
+        }
+        FilterMirrorRepresentation mirror = (FilterMirrorRepresentation) rep;
+        if (mMirror != mirror.mMirror) {
+            return false;
+        }
+        return true;
+    }
+
+    public Mirror getMirror() {
+        return mMirror;
+    }
+
+    public void set(FilterMirrorRepresentation r) {
+        mMirror = r.mMirror;
+    }
+
+    public void setMirror(Mirror mirror) {
+        if (mirror == null) {
+            throw new IllegalArgumentException("Argument to setMirror is null");
+        }
+        mMirror = mirror;
+    }
+
+    public void cycle() {
+        switch (mMirror) {
+            case NONE:
+                mMirror = Mirror.HORIZONTAL;
+                break;
+            case HORIZONTAL:
+                mMirror = Mirror.VERTICAL;
+                break;
+            case VERTICAL:
+                mMirror = Mirror.BOTH;
+                break;
+            case BOTH:
+                mMirror = Mirror.NONE;
+                break;
+        }
+    }
+
+    @Override
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        return new FilterMirrorRepresentation(this);
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        if (!(representation instanceof FilterMirrorRepresentation)) {
+            throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+        }
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (!(a instanceof FilterMirrorRepresentation)) {
+            throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+        }
+        setMirror(((FilterMirrorRepresentation) a).getMirror());
+    }
+
+    @Override
+    public boolean isNil() {
+        return mMirror == getNil();
+    }
+
+    public static Mirror getNil() {
+        return Mirror.NONE;
+    }
+
+    @Override
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+        writer.name(SERIALIZATION_MIRROR_VALUE).value(mMirror.value());
+        writer.endObject();
+    }
+
+    @Override
+    public void deSerializeRepresentation(JsonReader reader) throws IOException {
+        boolean unset = true;
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (SERIALIZATION_MIRROR_VALUE.equals(name)) {
+                Mirror r = Mirror.fromValue((char) reader.nextInt());
+                if (r != null) {
+                    setMirror(r);
+                    unset = false;
+                }
+            } else {
+                reader.skipValue();
+            }
+        }
+        if (unset) {
+            Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME);
+        }
+        reader.endObject();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterPoint.java b/src/com/android/gallery3d/filtershow/filters/FilterPoint.java
new file mode 100644
index 0000000..4520717
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterPoint.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+public interface FilterPoint {
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java
new file mode 100644
index 0000000..9bd1699
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import java.util.Vector;
+
+public abstract class FilterPointRepresentation extends FilterRepresentation {
+    private static final String LOGTAG = "FilterPointRepresentation";
+    private Vector<FilterPoint> mCandidates = new Vector<FilterPoint>();
+
+    public FilterPointRepresentation(String type, int textid, int editorID) {
+        super(type);
+        setFilterClass(ImageFilterRedEye.class);
+        setFilterType(FilterRepresentation.TYPE_NORMAL);
+        setTextId(textid);
+        setEditorId(editorID);
+    }
+
+    @Override
+    public abstract FilterRepresentation copy();
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    public boolean hasCandidates() {
+        return mCandidates != null;
+    }
+
+    public Vector<FilterPoint> getCandidates() {
+        return mCandidates;
+    }
+
+    @Override
+    public boolean isNil() {
+        if (getCandidates() != null && getCandidates().size() > 0) {
+            return false;
+        }
+        return true;
+    }
+
+    public Object getCandidate(int index) {
+        return this.mCandidates.get(index);
+    }
+
+    public void addCandidate(FilterPoint c) {
+        this.mCandidates.add(c);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (a instanceof FilterPointRepresentation) {
+            FilterPointRepresentation representation = (FilterPointRepresentation) a;
+            mCandidates.clear();
+            for (FilterPoint redEyeCandidate : representation.mCandidates) {
+                mCandidates.add(redEyeCandidate);
+            }
+        }
+    }
+
+    public void removeCandidate(RedEyeCandidate c) {
+        this.mCandidates.remove(c);
+    }
+
+    public void clearCandidates() {
+        this.mCandidates.clear();
+    }
+
+    public int getNumberOfCandidates() {
+        return mCandidates.size();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
new file mode 100644
index 0000000..dd06a97
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.RectF;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorRedEye;
+
+import java.util.Vector;
+
+public class FilterRedEyeRepresentation extends FilterPointRepresentation {
+    private static final String LOGTAG = "FilterRedEyeRepresentation";
+
+    public FilterRedEyeRepresentation() {
+        super("RedEye",R.string.redeye,EditorRedEye.ID);
+        setSerializationName("REDEYE");
+        setFilterClass(ImageFilterRedEye.class);
+        setOverlayId(R.drawable.photoeditor_effect_redeye);
+        setOverlayOnly(true);
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterRedEyeRepresentation representation = new FilterRedEyeRepresentation();
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    public void addRect(RectF rect, RectF bounds) {
+        Vector<RedEyeCandidate> intersects = new Vector<RedEyeCandidate>();
+        for (int i = 0; i < getCandidates().size(); i++) {
+            RedEyeCandidate r = (RedEyeCandidate) getCandidate(i);
+            if (r.intersect(rect)) {
+                intersects.add(r);
+            }
+        }
+        for (int i = 0; i < intersects.size(); i++) {
+            RedEyeCandidate r = intersects.elementAt(i);
+            rect.union(r.mRect);
+            bounds.union(r.mBounds);
+            removeCandidate(r);
+        }
+        addCandidate(new RedEyeCandidate(rect, bounds));
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
new file mode 100644
index 0000000..5b33ffb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class FilterRepresentation {
+    private static final String LOGTAG = "FilterRepresentation";
+    private static final boolean DEBUG = false;
+    private String mName;
+    private int mPriority = TYPE_NORMAL;
+    private Class<?> mFilterClass;
+    private boolean mSupportsPartialRendering = false;
+    private int mTextId = 0;
+    private int mEditorId = BasicEditor.ID;
+    private int mButtonId = 0;
+    private int mOverlayId = 0;
+    private boolean mOverlayOnly = false;
+    private boolean mShowParameterValue = true;
+    private String mSerializationName;
+    public static final byte TYPE_BORDER = 1;
+    public static final byte TYPE_FX = 2;
+    public static final byte TYPE_WBALANCE = 3;
+    public static final byte TYPE_VIGNETTE = 4;
+    public static final byte TYPE_NORMAL = 5;
+    public static final byte TYPE_TINYPLANET = 6;
+    public static final byte TYPE_GEOMETRY = 7;
+    protected static final String NAME_TAG = "Name";
+
+    public FilterRepresentation(String name) {
+        mName = name;
+    }
+
+    public FilterRepresentation copy(){
+        FilterRepresentation representation = new FilterRepresentation(mName);
+        representation.useParametersFrom(this);
+        return representation;
+    }
+
+    protected void copyAllParameters(FilterRepresentation representation) {
+        representation.setName(getName());
+        representation.setFilterClass(getFilterClass());
+        representation.setFilterType(getFilterType());
+        representation.setSupportsPartialRendering(supportsPartialRendering());
+        representation.setTextId(getTextId());
+        representation.setEditorId(getEditorId());
+        representation.setOverlayId(getOverlayId());
+        representation.setOverlayOnly(getOverlayOnly());
+        representation.setShowParameterValue(showParameterValue());
+        representation.mSerializationName = mSerializationName;
+
+    }
+
+    public boolean equals(FilterRepresentation representation) {
+        if (representation == null) {
+            return false;
+        }
+        if (representation.mFilterClass == mFilterClass
+                && representation.mName.equalsIgnoreCase(mName)
+                && representation.mPriority == mPriority
+                // TODO: After we enable partial rendering, we can switch back
+                // to use member variable here.
+                && representation.supportsPartialRendering() == supportsPartialRendering()
+                && representation.mTextId == mTextId
+                && representation.mEditorId == mEditorId
+                && representation.mButtonId == mButtonId
+                && representation.mOverlayId == mOverlayId
+                && representation.mOverlayOnly == mOverlayOnly
+                && representation.mShowParameterValue == mShowParameterValue) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return mName;
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void setSerializationName(String sname) {
+        mSerializationName = sname;
+    }
+
+    public String getSerializationName() {
+        return mSerializationName;
+    }
+
+    public void setFilterType(int priority) {
+        mPriority = priority;
+    }
+
+    public int getFilterType() {
+        return mPriority;
+    }
+
+    public boolean isNil() {
+        return false;
+    }
+
+    public boolean supportsPartialRendering() {
+        return false && mSupportsPartialRendering; // disable for now
+    }
+
+    public void setSupportsPartialRendering(boolean value) {
+        mSupportsPartialRendering = value;
+    }
+
+    public void useParametersFrom(FilterRepresentation a) {
+    }
+
+    public boolean allowsSingleInstanceOnly() {
+        return false;
+    }
+
+    public Class<?> getFilterClass() {
+        return mFilterClass;
+    }
+
+    public void setFilterClass(Class<?> filterClass) {
+        mFilterClass = filterClass;
+    }
+
+    // This same() function is different from equals(), basically it checks
+    // whether 2 FilterRepresentations are the same type. It doesn't care about
+    // the values.
+    public boolean same(FilterRepresentation b) {
+        if (b == null) {
+            return false;
+        }
+        return getFilterClass() == b.getFilterClass();
+    }
+
+    public int getTextId() {
+        return mTextId;
+    }
+
+    public void setTextId(int textId) {
+        mTextId = textId;
+    }
+
+    public int getOverlayId() {
+        return mOverlayId;
+    }
+
+    public void setOverlayId(int overlayId) {
+        mOverlayId = overlayId;
+    }
+
+    public boolean getOverlayOnly() {
+        return mOverlayOnly;
+    }
+
+    public void setOverlayOnly(boolean value) {
+        mOverlayOnly = value;
+    }
+
+    final public int getEditorId() {
+        return mEditorId;
+    }
+
+    public int[] getEditorIds() {
+        return new int[] {
+        mEditorId };
+    }
+
+    public void setEditorId(int editorId) {
+        mEditorId = editorId;
+    }
+
+    public boolean showParameterValue() {
+        return mShowParameterValue;
+    }
+
+    public void setShowParameterValue(boolean showParameterValue) {
+        mShowParameterValue = showParameterValue;
+    }
+
+    public String getStateRepresentation() {
+        return "";
+    }
+
+    /**
+     * Method must "beginObject()" add its info and "endObject()"
+     * @param writer
+     * @throws IOException
+     */
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+        {
+            String[][] rep = serializeRepresentation();
+            for (int k = 0; k < rep.length; k++) {
+                writer.name(rep[k][0]);
+                writer.value(rep[k][1]);
+            }
+        }
+        writer.endObject();
+    }
+
+    // this is the old way of doing this and will be removed soon
+    public String[][] serializeRepresentation() {
+        String[][] ret = {{NAME_TAG, getName()}};
+        return ret;
+    }
+
+    public void deSerializeRepresentation(JsonReader reader) throws IOException {
+        ArrayList<String[]> al = new ArrayList<String[]>();
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String[] kv = {reader.nextName(), reader.nextString()};
+            al.add(kv);
+
+        }
+        reader.endObject();
+        String[][] oldFormat = al.toArray(new String[al.size()][]);
+
+        deSerializeRepresentation(oldFormat);
+    }
+
+    // this is the old way of doing this and will be removed soon
+    public void deSerializeRepresentation(String[][] rep) {
+        for (int i = 0; i < rep.length; i++) {
+            if (NAME_TAG.equals(rep[i][0])) {
+                mName = rep[i][1];
+                break;
+            }
+        }
+    }
+
+    // Override this in subclasses
+    public int getStyle() {
+        return -1;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java
new file mode 100644
index 0000000..eb89de0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+
+import java.io.IOException;
+
+public class FilterRotateRepresentation extends FilterRepresentation {
+    public static final String SERIALIZATION_NAME = "ROTATION";
+    public static final String SERIALIZATION_ROTATE_VALUE = "value";
+    private static final String TAG = FilterRotateRepresentation.class.getSimpleName();
+
+    Rotation mRotation;
+
+    public enum Rotation {
+        ZERO(0), NINETY(90), ONE_EIGHTY(180), TWO_SEVENTY(270);
+        private final int mValue;
+
+        private Rotation(int value) {
+            mValue = value;
+        }
+
+        public int value() {
+            return mValue;
+        }
+
+        public static Rotation fromValue(int value) {
+            switch (value) {
+                case 0:
+                    return ZERO;
+                case 90:
+                    return NINETY;
+                case 180:
+                    return ONE_EIGHTY;
+                case 270:
+                    return TWO_SEVENTY;
+                default:
+                    return null;
+            }
+        }
+    }
+
+    public FilterRotateRepresentation(Rotation rotation) {
+        super(FilterRotateRepresentation.class.getSimpleName());
+        setSerializationName(SERIALIZATION_NAME);
+        setShowParameterValue(true);
+        setFilterClass(FilterRotateRepresentation.class);
+        setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+        setTextId(R.string.rotate);
+        setEditorId(EditorRotate.ID);
+        setRotation(rotation);
+    }
+
+    public FilterRotateRepresentation(FilterRotateRepresentation r) {
+        this(r.getRotation());
+    }
+
+    public FilterRotateRepresentation() {
+        this(getNil());
+    }
+
+    public Rotation getRotation() {
+        return mRotation;
+    }
+
+    public void rotateCW() {
+        switch(mRotation) {
+            case ZERO:
+                mRotation = Rotation.NINETY;
+                break;
+            case NINETY:
+                mRotation = Rotation.ONE_EIGHTY;
+                break;
+            case ONE_EIGHTY:
+                mRotation = Rotation.TWO_SEVENTY;
+                break;
+            case TWO_SEVENTY:
+                mRotation = Rotation.ZERO;
+                break;
+        }
+    }
+
+    public void set(FilterRotateRepresentation r) {
+        mRotation = r.mRotation;
+    }
+
+    public void setRotation(Rotation rotation) {
+        if (rotation == null) {
+            throw new IllegalArgumentException("Argument to setRotation is null");
+        }
+        mRotation = rotation;
+    }
+
+    @Override
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        return new FilterRotateRepresentation(this);
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        if (!(representation instanceof FilterRotateRepresentation)) {
+            throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+        }
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (!(a instanceof FilterRotateRepresentation)) {
+            throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+        }
+        setRotation(((FilterRotateRepresentation) a).getRotation());
+    }
+
+    @Override
+    public boolean isNil() {
+        return mRotation == getNil();
+    }
+
+    public static Rotation getNil() {
+        return Rotation.ZERO;
+    }
+
+    @Override
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+        writer.name(SERIALIZATION_ROTATE_VALUE).value(mRotation.value());
+        writer.endObject();
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation rep) {
+        if (!(rep instanceof FilterRotateRepresentation)) {
+            return false;
+        }
+        FilterRotateRepresentation rotate = (FilterRotateRepresentation) rep;
+        if (rotate.mRotation.value() != mRotation.value()) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void deSerializeRepresentation(JsonReader reader) throws IOException {
+        boolean unset = true;
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (SERIALIZATION_ROTATE_VALUE.equals(name)) {
+                Rotation r = Rotation.fromValue(reader.nextInt());
+                if (r != null) {
+                    setRotation(r);
+                    unset = false;
+                }
+            } else {
+                reader.skipValue();
+            }
+        }
+        if (unset) {
+            Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME);
+        }
+        reader.endObject();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java
new file mode 100644
index 0000000..94c9497
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+
+import java.io.IOException;
+
+public class FilterStraightenRepresentation extends FilterRepresentation {
+    public static final String SERIALIZATION_NAME = "STRAIGHTEN";
+    public static final String SERIALIZATION_STRAIGHTEN_VALUE = "value";
+    private static final String TAG = FilterStraightenRepresentation.class.getSimpleName();
+    public static final int MAX_STRAIGHTEN_ANGLE = 45;
+    public static final int MIN_STRAIGHTEN_ANGLE = -45;
+
+    float mStraighten;
+
+    public FilterStraightenRepresentation(float straighten) {
+        super(FilterStraightenRepresentation.class.getSimpleName());
+        setSerializationName(SERIALIZATION_NAME);
+        setShowParameterValue(true);
+        setFilterClass(FilterStraightenRepresentation.class);
+        setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+        setTextId(R.string.straighten);
+        setEditorId(EditorStraighten.ID);
+        setStraighten(straighten);
+    }
+
+    public FilterStraightenRepresentation(FilterStraightenRepresentation s) {
+        this(s.getStraighten());
+    }
+
+    public FilterStraightenRepresentation() {
+        this(getNil());
+    }
+
+    public void set(FilterStraightenRepresentation r) {
+        mStraighten = r.mStraighten;
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation rep) {
+        if (!(rep instanceof FilterStraightenRepresentation)) {
+            return false;
+        }
+        FilterStraightenRepresentation straighten = (FilterStraightenRepresentation) rep;
+        if (straighten.mStraighten != mStraighten) {
+            return false;
+        }
+        return true;
+    }
+
+    public float getStraighten() {
+        return mStraighten;
+    }
+
+    public void setStraighten(float straighten) {
+        if (!rangeCheck(straighten)) {
+            straighten = Math.min(Math.max(straighten, MIN_STRAIGHTEN_ANGLE), MAX_STRAIGHTEN_ANGLE);
+        }
+        mStraighten = straighten;
+    }
+
+    @Override
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        return new FilterStraightenRepresentation(this);
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        if (!(representation instanceof FilterStraightenRepresentation)) {
+            throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+        }
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        if (!(a instanceof FilterStraightenRepresentation)) {
+            throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+        }
+        setStraighten(((FilterStraightenRepresentation) a).getStraighten());
+    }
+
+    @Override
+    public boolean isNil() {
+        return mStraighten == getNil();
+    }
+
+    public static float getNil() {
+        return 0;
+    }
+
+    @Override
+    public void serializeRepresentation(JsonWriter writer) throws IOException {
+        writer.beginObject();
+        writer.name(SERIALIZATION_STRAIGHTEN_VALUE).value(mStraighten);
+        writer.endObject();
+    }
+
+    @Override
+    public void deSerializeRepresentation(JsonReader reader) throws IOException {
+        boolean unset = true;
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (SERIALIZATION_STRAIGHTEN_VALUE.equals(name)) {
+                float s = (float) reader.nextDouble();
+                if (rangeCheck(s)) {
+                    setStraighten(s);
+                    unset = false;
+                }
+            } else {
+                reader.skipValue();
+            }
+        }
+        if (unset) {
+            Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME);
+        }
+        reader.endObject();
+    }
+
+    private boolean rangeCheck(double s) {
+        if (s < -45 || s > 45) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
new file mode 100644
index 0000000..be18129
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
+
+public class FilterTinyPlanetRepresentation extends FilterBasicRepresentation {
+    private static final String SERIALIZATION_NAME = "TINYPLANET";
+    private static final String LOGTAG = "FilterTinyPlanetRepresentation";
+    private static final String SERIAL_ANGLE = "Angle";
+    private float mAngle = 0;
+
+    public FilterTinyPlanetRepresentation() {
+        super("TinyPlanet", 0, 50, 100);
+        setSerializationName(SERIALIZATION_NAME);
+        setShowParameterValue(true);
+        setFilterClass(ImageFilterTinyPlanet.class);
+        setFilterType(FilterRepresentation.TYPE_TINYPLANET);
+        setTextId(R.string.tinyplanet);
+        setEditorId(EditorTinyPlanet.ID);
+        setMinimum(1);
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterTinyPlanetRepresentation representation = new FilterTinyPlanetRepresentation();
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        FilterTinyPlanetRepresentation representation = (FilterTinyPlanetRepresentation) a;
+        super.useParametersFrom(a);
+        mAngle = representation.mAngle;
+        setZoom(representation.getZoom());
+    }
+
+    public void setAngle(float angle) {
+        mAngle = angle;
+    }
+
+    public float getAngle() {
+        return mAngle;
+    }
+
+    public int getZoom() {
+        return getValue();
+    }
+
+    public void setZoom(int zoom) {
+        setValue(zoom);
+    }
+
+    public boolean isNil() {
+        // TinyPlanet always has an effect
+        return false;
+    }
+
+    @Override
+    public String[][] serializeRepresentation() {
+        String[][] ret = {
+                {SERIAL_NAME  , getName() },
+                {SERIAL_VALUE , Integer.toString(getValue())},
+                {SERIAL_ANGLE , Float.toString(mAngle)}};
+        return ret;
+    }
+
+    @Override
+    public void deSerializeRepresentation(String[][] rep) {
+        super.deSerializeRepresentation(rep);
+        for (int i = 0; i < rep.length; i++) {
+            if (SERIAL_VALUE.equals(rep[i][0])) {
+                setValue(Integer.parseInt(rep[i][1]));
+            } else if (SERIAL_ANGLE.equals(rep[i][0])) {
+                setAngle(Float.parseFloat(rep[i][1]));
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java
new file mode 100644
index 0000000..dfdb6fc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class FilterUserPresetRepresentation extends FilterRepresentation {
+
+    private ImagePreset mPreset;
+    private int mId;
+
+    public FilterUserPresetRepresentation(String name, ImagePreset preset, int id) {
+        super(name);
+        setEditorId(ImageOnlyEditor.ID);
+        setFilterType(FilterRepresentation.TYPE_FX);
+        mPreset = preset;
+        mId = id;
+    }
+
+    public ImagePreset getImagePreset() {
+        return mPreset;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    public FilterRepresentation copy(){
+        FilterRepresentation representation = new FilterUserPresetRepresentation(getName(),
+                new ImagePreset(mPreset), mId);
+        return representation;
+    }
+
+    @Override
+    public boolean allowsSingleInstanceOnly() {
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
new file mode 100644
index 0000000..42a7406
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorVignette;
+import com.android.gallery3d.filtershow.imageshow.Oval;
+
+public class FilterVignetteRepresentation extends FilterBasicRepresentation implements Oval {
+    private static final String LOGTAG = "FilterVignetteRepresentation";
+    private float mCenterX = Float.NaN;
+    private float mCenterY;
+    private float mRadiusX = Float.NaN;
+    private float mRadiusY;
+
+    public FilterVignetteRepresentation() {
+        super("Vignette", -100, 50, 100);
+        setSerializationName("VIGNETTE");
+        setShowParameterValue(true);
+        setFilterType(FilterRepresentation.TYPE_VIGNETTE);
+        setTextId(R.string.vignette);
+        setEditorId(EditorVignette.ID);
+        setName("Vignette");
+        setFilterClass(ImageFilterVignette.class);
+        setMinimum(-100);
+        setMaximum(100);
+        setDefaultValue(0);
+    }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        super.useParametersFrom(a);
+        mCenterX = ((FilterVignetteRepresentation) a).mCenterX;
+        mCenterY = ((FilterVignetteRepresentation) a).mCenterY;
+        mRadiusX = ((FilterVignetteRepresentation) a).mRadiusX;
+        mRadiusY = ((FilterVignetteRepresentation) a).mRadiusY;
+    }
+
+    @Override
+    public FilterRepresentation copy() {
+        FilterVignetteRepresentation representation = new FilterVignetteRepresentation();
+        copyAllParameters(representation);
+        return representation;
+    }
+
+    @Override
+    protected void copyAllParameters(FilterRepresentation representation) {
+        super.copyAllParameters(representation);
+        representation.useParametersFrom(this);
+    }
+
+    @Override
+    public void setCenter(float centerX, float centerY) {
+        mCenterX = centerX;
+        mCenterY = centerY;
+    }
+
+    @Override
+    public float getCenterX() {
+        return mCenterX;
+    }
+
+    @Override
+    public float getCenterY() {
+        return mCenterY;
+    }
+
+    @Override
+    public void setRadius(float radiusX, float radiusY) {
+        mRadiusX = radiusX;
+        mRadiusY = radiusY;
+    }
+
+    @Override
+    public void setRadiusX(float radiusX) {
+        mRadiusX = radiusX;
+    }
+
+    @Override
+    public void setRadiusY(float radiusY) {
+        mRadiusY = radiusY;
+    }
+
+    @Override
+    public float getRadiusX() {
+        return mRadiusX;
+    }
+
+    @Override
+    public float getRadiusY() {
+        return mRadiusY;
+    }
+
+    public boolean isCenterSet() {
+        return mCenterX != Float.NaN;
+    }
+
+    @Override
+    public boolean isNil() {
+        return getValue() == 0;
+    }
+
+    @Override
+    public boolean equals(FilterRepresentation representation) {
+        if (!super.equals(representation)) {
+            return false;
+        }
+        if (representation instanceof FilterVignetteRepresentation) {
+            FilterVignetteRepresentation rep = (FilterVignetteRepresentation) representation;
+            if (rep.getCenterX() == getCenterX()
+                    && rep.getCenterY() == getCenterY()
+                    && rep.getRadiusX() == getRadiusX()
+                    && rep.getRadiusY() == getRadiusY()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static final String[] sParams = {
+            "Name", "value", "mCenterX", "mCenterY", "mRadiusX",
+            "mRadiusY"
+    };
+
+    @Override
+    public String[][] serializeRepresentation() {
+        String[][] ret = {
+                { sParams[0], getName() },
+                { sParams[1], Integer.toString(getValue()) },
+                { sParams[2], Float.toString(mCenterX) },
+                { sParams[3], Float.toString(mCenterY) },
+                { sParams[4], Float.toString(mRadiusX) },
+                { sParams[5], Float.toString(mRadiusY) }
+        };
+        return ret;
+    }
+
+    @Override
+    public void deSerializeRepresentation(String[][] rep) {
+        super.deSerializeRepresentation(rep);
+        for (int i = 0; i < rep.length; i++) {
+            String key = rep[i][0];
+            String value = rep[i][1];
+            if (sParams[0].equals(key)) {
+                setName(value);
+            } else if (sParams[1].equals(key)) {
+               setValue(Integer.parseInt(value));
+            } else if (sParams[2].equals(key)) {
+                mCenterX = Float.parseFloat(value);
+            } else if (sParams[3].equals(key)) {
+                mCenterY = Float.parseFloat(value);
+            } else if (sParams[4].equals(key)) {
+                mRadiusX = Float.parseFloat(value);
+            } else if (sParams[5].equals(key)) {
+                mRadiusY = Float.parseFloat(value);
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java
new file mode 100644
index 0000000..710128f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+public interface FiltersManagerInterface {
+   ImageFilter getFilterForRepresentation(FilterRepresentation representation);
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/IconUtilities.java b/src/com/android/gallery3d/filtershow/filters/IconUtilities.java
new file mode 100644
index 0000000..e2a0147
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/IconUtilities.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.android.gallery3d.R;
+
+public class IconUtilities {
+    public static final int PUNCH = R.drawable.filtershow_fx_0005_punch;
+    public static final int VINTAGE = R.drawable.filtershow_fx_0000_vintage;
+    public static final int BW_CONTRAST = R.drawable.filtershow_fx_0004_bw_contrast;
+    public static final int BLEACH = R.drawable.filtershow_fx_0002_bleach;
+    public static final int INSTANT = R.drawable.filtershow_fx_0001_instant;
+    public static final int WASHOUT = R.drawable.filtershow_fx_0007_washout;
+    public static final int BLUECRUSH = R.drawable.filtershow_fx_0003_blue_crush;
+    public static final int WASHOUT_COLOR = R.drawable.filtershow_fx_0008_washout_color;
+    public static final int X_PROCESS = R.drawable.filtershow_fx_0006_x_process;
+
+    public static Bitmap getFXBitmap(Resources res, int id) {
+        Bitmap ret;
+        BitmapFactory.Options o = new BitmapFactory.Options();
+        o.inScaled = false;
+
+        if (id != 0) {
+            return BitmapFactory.decodeResource(res, id, o);
+        }
+        return null;
+    }
+
+    public static Bitmap loadBitmap(Resources res, int resource) {
+
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        Bitmap bitmap = BitmapFactory.decodeResource(
+                res,
+                resource, options);
+
+        return bitmap;
+    }
+
+    public static Bitmap applyFX(Bitmap bitmap, final Bitmap fxBitmap) {
+        ImageFilterFx fx = new ImageFilterFx() {
+            @Override
+            public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+
+                int w = bitmap.getWidth();
+                int h = bitmap.getHeight();
+                int fxw = fxBitmap.getWidth();
+                int fxh = fxBitmap.getHeight();
+                int start = 0;
+                int end = w * h * 4;
+                nativeApplyFilter(bitmap, w, h, fxBitmap, fxw, fxh, start, end);
+                return bitmap;
+            }
+        };
+        return fx.apply(bitmap, 0, 0);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
new file mode 100644
index 0000000..4371374
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.support.v8.renderscript.Allocation;
+import android.widget.Toast;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public abstract class ImageFilter implements Cloneable {
+    private FilterEnvironment mEnvironment = null;
+
+    protected String mName = "Original";
+    private final String LOGTAG = "ImageFilter";
+    protected static final boolean SIMPLE_ICONS = true;
+    // TODO: Temporary, for dogfood note memory issues with toasts for better
+    // feedback. Remove this when filters actually work in low memory
+    // situations.
+    private static Activity sActivity = null;
+
+    public static void setActivityForMemoryToasts(Activity activity) {
+        sActivity = activity;
+    }
+
+    public static void resetStatics() {
+        sActivity = null;
+    }
+
+    public void freeResources() {}
+
+    public void displayLowMemoryToast() {
+        if (sActivity != null) {
+            sActivity.runOnUiThread(new Runnable() {
+                public void run() {
+                    Toast.makeText(sActivity, "Memory too low for filter " + getName() +
+                            ", please file a bug report", Toast.LENGTH_SHORT).show();
+                }
+            });
+        }
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public boolean supportsAllocationInput() { return false; }
+
+    public void apply(Allocation in, Allocation out) {
+        setGeneralParameters();
+    }
+
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        // do nothing here, subclasses will implement filtering here
+        setGeneralParameters();
+        return bitmap;
+    }
+
+    public abstract void useRepresentation(FilterRepresentation representation);
+
+    native protected void nativeApplyGradientFilter(Bitmap bitmap, int w, int h,
+            int[] redGradient, int[] greenGradient, int[] blueGradient);
+
+    public FilterRepresentation getDefaultRepresentation() {
+        return null;
+    }
+
+    protected Matrix getOriginalToScreenMatrix(int w, int h) {
+        return GeometryMathUtils.getImageToScreenMatrix(getEnvironment().getImagePreset()
+                .getGeometryFilters(), true, MasterImage.getImage().getOriginalBounds(), w, h);
+    }
+
+    public void setEnvironment(FilterEnvironment environment) {
+        mEnvironment = environment;
+    }
+
+    public FilterEnvironment getEnvironment() {
+        return mEnvironment;
+    }
+
+    public void setGeneralParameters() {
+        // should implement in subclass which like to transport
+        // some information to other filters. (like the style setting from RetroLux
+        // and Film to FixedFrame)
+        mEnvironment.clearGeneralParameters();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
new file mode 100644
index 0000000..a7286f0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+import java.util.HashMap;
+
+public class ImageFilterBorder extends ImageFilter {
+    private static final float NINEPATCH_ICON_SCALING = 10;
+    private static final float BITMAP_ICON_SCALING = 1 / 3.0f;
+    private FilterImageBorderRepresentation mParameters = null;
+    private Resources mResources = null;
+
+    private HashMap<Integer, Drawable> mDrawables = new HashMap<Integer, Drawable>();
+
+    public ImageFilterBorder() {
+        mName = "Border";
+    }
+
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterImageBorderRepresentation parameters = (FilterImageBorderRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    public FilterImageBorderRepresentation getParameters() {
+        return mParameters;
+    }
+
+    public void freeResources() {
+       mDrawables.clear();
+    }
+
+    public Bitmap applyHelper(Bitmap bitmap, float scale1, float scale2 ) {
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        Rect bounds = new Rect(0, 0, (int) (w * scale1), (int) (h * scale1));
+        Canvas canvas = new Canvas(bitmap);
+        canvas.scale(scale2, scale2);
+        Drawable drawable = getDrawable(getParameters().getDrawableResource());
+        drawable.setBounds(bounds);
+        drawable.draw(canvas);
+        return bitmap;
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null || getParameters().getDrawableResource() == 0) {
+            return bitmap;
+        }
+        float scale2 = scaleFactor * 2.0f;
+        float scale1 = 1 / scale2;
+        return applyHelper(bitmap, scale1, scale2);
+    }
+
+    public void setResources(Resources resources) {
+        if (mResources != resources) {
+            mResources = resources;
+            mDrawables.clear();
+        }
+    }
+
+    public Drawable getDrawable(int rsc) {
+        Drawable drawable = mDrawables.get(rsc);
+        if (drawable == null && mResources != null && rsc != 0) {
+            drawable = new BitmapDrawable(mResources, BitmapFactory.decodeResource(mResources, rsc));
+            mDrawables.put(rsc, drawable);
+        }
+        return drawable;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
new file mode 100644
index 0000000..50837ca
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+
+public class ImageFilterBwFilter extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "BWFILTER";
+
+    public ImageFilterBwFilter() {
+        mName = "BW Filter";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("BW Filter");
+        representation.setSerializationName(SERIALIZATION_NAME);
+
+        representation.setFilterClass(ImageFilterBwFilter.class);
+        representation.setMaximum(180);
+        representation.setMinimum(-180);
+        representation.setTextId(R.string.bwfilter);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, int r, int g, int b);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float[] hsv = new float[] {
+                180 + getParameters().getValue(), 1, 1
+        };
+        int rgb = Color.HSVToColor(hsv);
+        int r = 0xFF & (rgb >> 16);
+        int g = 0xFF & (rgb >> 8);
+        int b = 0xFF & (rgb >> 0);
+        nativeApplyFilter(bitmap, w, h, r, g, b);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java
new file mode 100644
index 0000000..1ea8edf
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.Element;
+import android.support.v8.renderscript.RenderScript;
+import android.support.v8.renderscript.Script.LaunchOptions;
+import android.support.v8.renderscript.Type;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public class ImageFilterChanSat extends ImageFilterRS {
+    private static final String LOGTAG = "ImageFilterChanSat";
+    private ScriptC_saturation mScript;
+    private Bitmap mSourceBitmap;
+
+    private static final int STRIP_SIZE = 64;
+
+    FilterChanSatRepresentation mParameters = new FilterChanSatRepresentation();
+    private Bitmap mOverlayBitmap;
+
+    public ImageFilterChanSat() {
+        mName = "ChannelSat";
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return new FilterChanSatRepresentation();
+    }
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+        mParameters = (FilterChanSatRepresentation) representation;
+    }
+
+    @Override
+    protected void resetAllocations() {
+
+    }
+
+    @Override
+    public void resetScripts() {
+        if (mScript != null) {
+            mScript.destroy();
+            mScript = null;
+        }
+    }
+    @Override
+    protected void createFilter(android.content.res.Resources res, float scaleFactor,
+                                int quality) {
+        createFilter(res, scaleFactor, quality, getInPixelsAllocation());
+    }
+
+    @Override
+    protected void createFilter(android.content.res.Resources res, float scaleFactor,
+                                int quality, Allocation in) {
+        RenderScript rsCtx = getRenderScriptContext();
+
+        Type.Builder tb_float = new Type.Builder(rsCtx, Element.F32_4(rsCtx));
+        tb_float.setX(in.getType().getX());
+        tb_float.setY(in.getType().getY());
+        mScript = new ScriptC_saturation(rsCtx, res, R.raw.saturation);
+    }
+
+
+    private Bitmap getSourceBitmap() {
+        assert (mSourceBitmap != null);
+        return mSourceBitmap;
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) {
+            return bitmap;
+        }
+
+        mSourceBitmap = bitmap;
+        Bitmap ret = super.apply(bitmap, scaleFactor, quality);
+        mSourceBitmap = null;
+
+        return ret;
+    }
+
+    @Override
+    protected void bindScriptValues() {
+        int width = getInPixelsAllocation().getType().getX();
+        int height = getInPixelsAllocation().getType().getY();
+    }
+
+
+
+    @Override
+    protected void runFilter() {
+        int []sat = new int[7];
+        for(int i = 0;i<sat.length ;i ++){
+          sat[i] =   mParameters.getValue(i);
+        }
+
+
+        int width = getInPixelsAllocation().getType().getX();
+        int height = getInPixelsAllocation().getType().getY();
+        Matrix m = getOriginalToScreenMatrix(width, height);
+
+
+        mScript.set_saturation(sat);
+
+        mScript.invoke_setupGradParams();
+        runSelectiveAdjust(
+                getInPixelsAllocation(), getOutPixelsAllocation());
+
+    }
+
+    private void runSelectiveAdjust(Allocation in, Allocation out) {
+        int width = in.getType().getX();
+        int height = in.getType().getY();
+
+        LaunchOptions options = new LaunchOptions();
+        int ty;
+        options.setX(0, width);
+
+        for (ty = 0; ty < height; ty += STRIP_SIZE) {
+            int endy = ty + STRIP_SIZE;
+            if (endy > height) {
+                endy = height;
+            }
+            options.setY(ty, endy);
+            mScript.forEach_selectiveAdjust(in, out, options);
+            if (checkStop()) {
+                return;
+            }
+        }
+    }
+
+    private boolean checkStop() {
+        RenderScript rsCtx = getRenderScriptContext();
+        rsCtx.finish();
+        if (getEnvironment().needsStop()) {
+            return true;
+        }
+        return false;
+    }
+}
+
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
new file mode 100644
index 0000000..27c0e08
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterContrast extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "CONTRAST";
+
+    public ImageFilterContrast() {
+        mName = "Contrast";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation =
+                (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Contrast");
+        representation.setSerializationName(SERIALIZATION_NAME);
+
+        representation.setFilterClass(ImageFilterContrast.class);
+        representation.setTextId(R.string.contrast);
+        representation.setMinimum(-100);
+        representation.setMaximum(100);
+        representation.setDefaultValue(0);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float strength);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float value = getParameters().getValue();
+        nativeApplyFilter(bitmap, w, h, value);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
new file mode 100644
index 0000000..61b60d2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.filtershow.imageshow.Spline;
+
+public class ImageFilterCurves extends ImageFilter {
+
+    private static final String LOGTAG = "ImageFilterCurves";
+    FilterCurvesRepresentation mParameters = new FilterCurvesRepresentation();
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return new FilterCurvesRepresentation();
+    }
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterCurvesRepresentation parameters = (FilterCurvesRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    public ImageFilterCurves() {
+        mName = "Curves";
+        reset();
+    }
+
+    public void populateArray(int[] array, int curveIndex) {
+        Spline spline = mParameters.getSpline(curveIndex);
+        if (spline == null) {
+            return;
+        }
+        float[] curve = spline.getAppliedCurve();
+        for (int i = 0; i < 256; i++) {
+            array[i] = (int) (curve[i] * 255);
+        }
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (!mParameters.getSpline(Spline.RGB).isOriginal()) {
+            int[] rgbGradient = new int[256];
+            populateArray(rgbGradient, Spline.RGB);
+            nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(),
+                    rgbGradient, rgbGradient, rgbGradient);
+        }
+
+        int[] redGradient = null;
+        if (!mParameters.getSpline(Spline.RED).isOriginal()) {
+            redGradient = new int[256];
+            populateArray(redGradient, Spline.RED);
+        }
+        int[] greenGradient = null;
+        if (!mParameters.getSpline(Spline.GREEN).isOriginal()) {
+            greenGradient = new int[256];
+            populateArray(greenGradient, Spline.GREEN);
+        }
+        int[] blueGradient = null;
+        if (!mParameters.getSpline(Spline.BLUE).isOriginal()) {
+            blueGradient = new int[256];
+            populateArray(blueGradient, Spline.BLUE);
+        }
+
+        nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(),
+                redGradient, greenGradient, blueGradient);
+        return bitmap;
+    }
+
+    public void setSpline(Spline spline, int splineIndex) {
+        mParameters.setSpline(splineIndex, new Spline(spline));
+    }
+
+    public Spline getSpline(int splineIndex) {
+        return mParameters.getSpline(splineIndex);
+    }
+
+    public void reset() {
+        Spline spline = new Spline();
+
+        spline.addPoint(0.0f, 1.0f);
+        spline.addPoint(1.0f, 0.0f);
+
+        for (int i = 0; i < 4; i++) {
+            mParameters.setSpline(i, new Spline(spline));
+        }
+    }
+
+    public void useFilter(ImageFilter a) {
+        ImageFilterCurves c = (ImageFilterCurves) a;
+        for (int i = 0; i < 4; i++) {
+            if (c.mParameters.getSpline(i) != null) {
+                setSpline(c.mParameters.getSpline(i), i);
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java
new file mode 100644
index 0000000..efb9cde
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class ImageFilterDownsample extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "DOWNSAMPLE";
+    private static final int ICON_DOWNSAMPLE_FRACTION = 8;
+    private ImageLoader mImageLoader;
+
+    public ImageFilterDownsample(ImageLoader loader) {
+        mName = "Downsample";
+        mImageLoader = loader;
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Downsample");
+        representation.setSerializationName(SERIALIZATION_NAME);
+
+        representation.setFilterClass(ImageFilterDownsample.class);
+        representation.setMaximum(100);
+        representation.setMinimum(1);
+        representation.setValue(50);
+        representation.setDefaultValue(50);
+        representation.setPreviewValue(3);
+        representation.setTextId(R.string.downsample);
+        return representation;
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        int p = getParameters().getValue();
+
+        // size of original precached image
+        Rect size = MasterImage.getImage().getOriginalBounds();
+        int orig_w = size.width();
+        int orig_h = size.height();
+
+        if (p > 0 && p < 100) {
+            // scale preview to same size as the resulting bitmap from a "save"
+            int newWidth = orig_w * p / 100;
+            int newHeight = orig_h * p / 100;
+
+            // only scale preview if preview isn't already scaled enough
+            if (newWidth <= 0 || newHeight <= 0 || newWidth >= w || newHeight >= h) {
+                return bitmap;
+            }
+            Bitmap ret = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
+            if (ret != bitmap) {
+                bitmap.recycle();
+            }
+            return ret;
+        }
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
new file mode 100644
index 0000000..7df5ffb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation.StrokeData;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+import java.util.Vector;
+
+public class ImageFilterDraw extends ImageFilter {
+    private static final String LOGTAG = "ImageFilterDraw";
+    public final static byte SIMPLE_STYLE = 0;
+    public final static byte BRUSH_STYLE_SPATTER = 1;
+    public final static byte BRUSH_STYLE_MARKER = 2;
+    public final static int NUMBER_OF_STYLES = 3;
+    Bitmap mOverlayBitmap; // this accelerates interaction
+    int mCachedStrokes = -1;
+    int mCurrentStyle = 0;
+
+    FilterDrawRepresentation mParameters = new FilterDrawRepresentation();
+
+    public ImageFilterDraw() {
+        mName = "Image Draw";
+    }
+
+    DrawStyle[] mDrawingsTypes = new DrawStyle[] {
+            new SimpleDraw(),
+            new Brush(R.drawable.brush_marker),
+            new Brush(R.drawable.brush_spatter)
+    };
+    {
+        for (int i = 0; i < mDrawingsTypes.length; i++) {
+            mDrawingsTypes[i].setType((byte) i);
+        }
+
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return new FilterDrawRepresentation();
+    }
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterDrawRepresentation parameters = (FilterDrawRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    public void setStyle(byte style) {
+        mCurrentStyle = style % mDrawingsTypes.length;
+    }
+
+    public int getStyle() {
+        return mCurrentStyle;
+    }
+
+    public static interface DrawStyle {
+        public void setType(byte type);
+        public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix,
+                int quality);
+    }
+
+    class SimpleDraw implements DrawStyle {
+        byte mType;
+
+        @Override
+        public void setType(byte type) {
+            mType = type;
+        }
+
+        @Override
+        public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix,
+                int quality) {
+            if (sd == null) {
+                return;
+            }
+            if (sd.mPath == null) {
+                return;
+            }
+            Paint paint = new Paint();
+
+            paint.setStyle(Style.STROKE);
+            paint.setColor(sd.mColor);
+            paint.setStrokeWidth(toScrMatrix.mapRadius(sd.mRadius));
+
+            // done this way because of a bug in path.transform(matrix)
+            Path mCacheTransPath = new Path();
+            mCacheTransPath.addPath(sd.mPath, toScrMatrix);
+
+            canvas.drawPath(mCacheTransPath, paint);
+        }
+    }
+
+    class Brush implements DrawStyle {
+        int mBrushID;
+        Bitmap mBrush;
+        byte mType;
+
+        public Brush(int brushID) {
+            mBrushID = brushID;
+        }
+
+        public Bitmap getBrush() {
+            if (mBrush == null) {
+                BitmapFactory.Options opt = new BitmapFactory.Options();
+                opt.inPreferredConfig = Bitmap.Config.ALPHA_8;
+                mBrush = BitmapFactory.decodeResource(MasterImage.getImage().getActivity()
+                        .getResources(), mBrushID, opt);
+                mBrush = mBrush.extractAlpha();
+            }
+            return mBrush;
+        }
+
+        @Override
+        public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas,
+                Matrix toScrMatrix,
+                int quality) {
+            if (sd == null || sd.mPath == null) {
+                return;
+            }
+            Paint paint = new Paint();
+            paint.setStyle(Style.STROKE);
+            paint.setAntiAlias(true);
+            Path mCacheTransPath = new Path();
+            mCacheTransPath.addPath(sd.mPath, toScrMatrix);
+            draw(canvas, paint, sd.mColor, toScrMatrix.mapRadius(sd.mRadius) * 2,
+                    mCacheTransPath);
+        }
+
+        public Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
+        {
+            Matrix m = new Matrix();
+            m.setScale(dstWidth / (float) src.getWidth(), dstHeight / (float) src.getHeight());
+            Bitmap result = Bitmap.createBitmap(dstWidth, dstHeight, src.getConfig());
+            Canvas canvas = new Canvas(result);
+
+            Paint paint = new Paint();
+            paint.setFilterBitmap(filter);
+            canvas.drawBitmap(src, m, paint);
+
+            return result;
+
+        }
+        void draw(Canvas canvas, Paint paint, int color, float size, Path path) {
+            PathMeasure mPathMeasure = new PathMeasure();
+            float[] mPosition = new float[2];
+            float[] mTan = new float[2];
+
+            mPathMeasure.setPath(path, false);
+
+            paint.setAntiAlias(true);
+            paint.setColor(color);
+
+            paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
+            Bitmap brush;
+            // done this way because of a bug in
+            // Bitmap.createScaledBitmap(getBrush(),(int) size,(int) size,true);
+            brush = createScaledBitmap(getBrush(), (int) size, (int) size, true);
+            float len = mPathMeasure.getLength();
+            float s2 = size / 2;
+            float step = s2 / 8;
+            for (float i = 0; i < len; i += step) {
+                mPathMeasure.getPosTan(i, mPosition, mTan);
+                //                canvas.drawCircle(pos[0], pos[1], size, paint);
+                canvas.drawBitmap(brush, mPosition[0] - s2, mPosition[1] - s2, paint);
+            }
+        }
+
+        @Override
+        public void setType(byte type) {
+            mType = type;
+        }
+    }
+
+    void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix,
+            int quality) {
+        mDrawingsTypes[sd.mType].paint(sd, canvas, toScrMatrix, quality);
+    }
+
+    public void drawData(Canvas canvas, Matrix originalRotateToScreen, int quality) {
+        Paint paint = new Paint();
+        if (quality == FilterEnvironment.QUALITY_FINAL) {
+            paint.setAntiAlias(true);
+        }
+        paint.setStyle(Style.STROKE);
+        paint.setColor(Color.RED);
+        paint.setStrokeWidth(40);
+
+        if (mParameters.getDrawing().isEmpty() && mParameters.getCurrentDrawing() == null) {
+            return;
+        }
+        if (quality == FilterEnvironment.QUALITY_FINAL) {
+            for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) {
+                paint(strokeData, canvas, originalRotateToScreen, quality);
+            }
+            return;
+        }
+
+        if (mOverlayBitmap == null ||
+                mOverlayBitmap.getWidth() != canvas.getWidth() ||
+                mOverlayBitmap.getHeight() != canvas.getHeight() ||
+                mParameters.getDrawing().size() < mCachedStrokes) {
+
+            mOverlayBitmap = Bitmap.createBitmap(
+                    canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
+            mCachedStrokes = 0;
+        }
+
+        if (mCachedStrokes < mParameters.getDrawing().size()) {
+            fillBuffer(originalRotateToScreen);
+        }
+        canvas.drawBitmap(mOverlayBitmap, 0, 0, paint);
+
+        StrokeData stroke = mParameters.getCurrentDrawing();
+        if (stroke != null) {
+            paint(stroke, canvas, originalRotateToScreen, quality);
+        }
+    }
+
+    public void fillBuffer(Matrix originalRotateToScreen) {
+        Canvas drawCache = new Canvas(mOverlayBitmap);
+        Vector<FilterDrawRepresentation.StrokeData> v = mParameters.getDrawing();
+        int n = v.size();
+
+        for (int i = mCachedStrokes; i < n; i++) {
+            paint(v.get(i), drawCache, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
+        }
+        mCachedStrokes = n;
+    }
+
+    public void draw(Canvas canvas, Matrix originalRotateToScreen) {
+        for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) {
+            paint(strokeData, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
+        }
+        mDrawingsTypes[mCurrentStyle].paint(
+                null, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+
+        Matrix m = getOriginalToScreenMatrix(w, h);
+        drawData(new Canvas(bitmap), m, quality);
+        return bitmap;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
new file mode 100644
index 0000000..2d0d765
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterEdge extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "EDGE";
+    public ImageFilterEdge() {
+        mName = "Edge";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterRepresentation representation = super.getDefaultRepresentation();
+        representation.setName("Edge");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterEdge.class);
+        representation.setTextId(R.string.edge);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float p);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float p = getParameters().getValue() + 101;
+        p = (float) p / 100;
+        nativeApplyFilter(bitmap, w, h, p);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
new file mode 100644
index 0000000..69eab73
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterExposure extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "EXPOSURE";
+    public ImageFilterExposure() {
+        mName = "Exposure";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation =
+                (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Exposure");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterExposure.class);
+        representation.setTextId(R.string.exposure);
+        representation.setMinimum(-100);
+        representation.setMaximum(100);
+        representation.setDefaultValue(0);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float bright);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float value = getParameters().getValue();
+        nativeApplyFilter(bitmap, w, h, value);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
new file mode 100644
index 0000000..19bea59
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import com.android.gallery3d.app.Log;
+
+public class ImageFilterFx extends ImageFilter {
+    private static final String LOGTAG = "ImageFilterFx";
+    private FilterFxRepresentation mParameters = null;
+    private Bitmap mFxBitmap = null;
+    private Resources mResources = null;
+    private int mFxBitmapId = 0;
+
+    public ImageFilterFx() {
+    }
+
+    @Override
+    public void freeResources() {
+        if (mFxBitmap != null) mFxBitmap.recycle();
+        mFxBitmap = null;
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return null;
+    }
+
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterFxRepresentation parameters = (FilterFxRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    public FilterFxRepresentation getParameters() {
+        return mParameters;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h,
+                                            Bitmap fxBitmap, int fxw, int fxh,
+                                            int start, int end);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null || mResources == null) {
+            return bitmap;
+        }
+
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+
+        int bitmapResourceId = getParameters().getBitmapResource();
+        if (bitmapResourceId == 0) { // null filter fx
+            return bitmap;
+        }
+
+        if (mFxBitmap == null || mFxBitmapId != bitmapResourceId) {
+            BitmapFactory.Options o = new BitmapFactory.Options();
+            o.inScaled = false;
+            mFxBitmapId = bitmapResourceId;
+            if (mFxBitmapId != 0) {
+                mFxBitmap = BitmapFactory.decodeResource(mResources, mFxBitmapId, o);
+            } else {
+                Log.w(LOGTAG, "bad resource for filter: " + mName);
+            }
+        }
+
+        if (mFxBitmap == null) {
+            return bitmap;
+        }
+
+        int fxw = mFxBitmap.getWidth();
+        int fxh = mFxBitmap.getHeight();
+
+        int stride = w * 4;
+        int max = stride * h;
+        int increment = stride * 256; // 256 lines
+        for (int i = 0; i < max; i += increment) {
+            int start = i;
+            int end = i + increment;
+            if (end > max) {
+                end = max;
+            }
+            if (!getEnvironment().needsStop()) {
+                nativeApplyFilter(bitmap, w, h, mFxBitmap, fxw, fxh, start, end);
+            }
+        }
+
+        return bitmap;
+    }
+
+    public void setResources(Resources resources) {
+        mResources = resources;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java
new file mode 100644
index 0000000..cbdfaa6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.Element;
+import android.support.v8.renderscript.RenderScript;
+import android.support.v8.renderscript.Script.LaunchOptions;
+import android.support.v8.renderscript.Type;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public class ImageFilterGrad extends ImageFilterRS {
+    private static final String LOGTAG = "ImageFilterGrad";
+    private ScriptC_grad mScript;
+    private Bitmap mSourceBitmap;
+    private static final int RADIUS_SCALE_FACTOR = 160;
+
+    private static final int STRIP_SIZE = 64;
+
+    FilterGradRepresentation mParameters = new FilterGradRepresentation();
+    private Bitmap mOverlayBitmap;
+
+    public ImageFilterGrad() {
+        mName = "grad";
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return new FilterGradRepresentation();
+    }
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+        mParameters = (FilterGradRepresentation) representation;
+    }
+
+    @Override
+    protected void resetAllocations() {
+
+    }
+
+    @Override
+    public void resetScripts() {
+        if (mScript != null) {
+            mScript.destroy();
+            mScript = null;
+        }
+    }
+    @Override
+    protected void createFilter(android.content.res.Resources res, float scaleFactor,
+                                int quality) {
+        createFilter(res, scaleFactor, quality, getInPixelsAllocation());
+    }
+
+    @Override
+    protected void createFilter(android.content.res.Resources res, float scaleFactor,
+                                int quality, Allocation in) {
+        RenderScript rsCtx = getRenderScriptContext();
+
+        Type.Builder tb_float = new Type.Builder(rsCtx, Element.F32_4(rsCtx));
+        tb_float.setX(in.getType().getX());
+        tb_float.setY(in.getType().getY());
+        mScript = new ScriptC_grad(rsCtx, res, R.raw.grad);
+    }
+
+
+    private Bitmap getSourceBitmap() {
+        assert (mSourceBitmap != null);
+        return mSourceBitmap;
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) {
+            return bitmap;
+        }
+
+        mSourceBitmap = bitmap;
+        Bitmap ret = super.apply(bitmap, scaleFactor, quality);
+        mSourceBitmap = null;
+
+        return ret;
+    }
+
+    @Override
+    protected void bindScriptValues() {
+        int width = getInPixelsAllocation().getType().getX();
+        int height = getInPixelsAllocation().getType().getY();
+        mScript.set_inputWidth(width);
+        mScript.set_inputHeight(height);
+    }
+
+    @Override
+    protected void runFilter() {
+        int[] x1 = mParameters.getXPos1();
+        int[] y1 = mParameters.getYPos1();
+        int[] x2 = mParameters.getXPos2();
+        int[] y2 = mParameters.getYPos2();
+
+        int width = getInPixelsAllocation().getType().getX();
+        int height = getInPixelsAllocation().getType().getY();
+        Matrix m = getOriginalToScreenMatrix(width, height);
+        float[] coord = new float[2];
+        for (int i = 0; i < x1.length; i++) {
+            coord[0] = x1[i];
+            coord[1] = y1[i];
+            m.mapPoints(coord);
+            x1[i] = (int) coord[0];
+            y1[i] = (int) coord[1];
+            coord[0] = x2[i];
+            coord[1] = y2[i];
+            m.mapPoints(coord);
+            x2[i] = (int) coord[0];
+            y2[i] = (int) coord[1];
+        }
+
+        mScript.set_mask(mParameters.getMask());
+        mScript.set_xPos1(x1);
+        mScript.set_yPos1(y1);
+        mScript.set_xPos2(x2);
+        mScript.set_yPos2(y2);
+
+        mScript.set_brightness(mParameters.getBrightness());
+        mScript.set_contrast(mParameters.getContrast());
+        mScript.set_saturation(mParameters.getSaturation());
+
+        mScript.invoke_setupGradParams();
+        runSelectiveAdjust(
+                getInPixelsAllocation(), getOutPixelsAllocation());
+
+    }
+
+    private void runSelectiveAdjust(Allocation in, Allocation out) {
+        int width = in.getType().getX();
+        int height = in.getType().getY();
+
+        LaunchOptions options = new LaunchOptions();
+        int ty;
+        options.setX(0, width);
+
+        for (ty = 0; ty < height; ty += STRIP_SIZE) {
+            int endy = ty + STRIP_SIZE;
+            if (endy > height) {
+                endy = height;
+            }
+            options.setY(ty, endy);
+            mScript.forEach_selectiveAdjust(in, out, options);
+            if (checkStop()) {
+                return;
+            }
+        }
+    }
+
+    private boolean checkStop() {
+        RenderScript rsCtx = getRenderScriptContext();
+        rsCtx.finish();
+        if (getEnvironment().needsStop()) {
+            return true;
+        }
+        return false;
+    }
+}
+
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
new file mode 100644
index 0000000..4c837e0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterHighlights extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "HIGHLIGHTS";
+    private static final String LOGTAG = "ImageFilterVignette";
+
+    public ImageFilterHighlights() {
+        mName = "Highlights";
+    }
+
+    SplineMath mSpline = new SplineMath(5);
+    double[] mHighlightCurve = { 0.0, 0.32, 0.418, 0.476, 0.642 };
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation =
+                (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Highlights");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterHighlights.class);
+        representation.setTextId(R.string.highlight_recovery);
+        representation.setMinimum(-100);
+        representation.setMaximum(100);
+        representation.setDefaultValue(0);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float[] luminanceMap);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        float p = getParameters().getValue();
+        double t = p/100.;
+        for (int i = 0; i < 5; i++) {
+            double x = i / 4.;
+            double y = mHighlightCurve[i] *t+x*(1-t);
+            mSpline.setPoint(i, x, y);
+        }
+
+        float[][] curve = mSpline.calculatetCurve(256);
+        float[] luminanceMap = new float[curve.length];
+        for (int i = 0; i < luminanceMap.length; i++) {
+            luminanceMap[i] = curve[i][1];
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+
+        nativeApplyFilter(bitmap, w, h, luminanceMap);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
new file mode 100644
index 0000000..b87c254
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterHue extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "HUE";
+    private ColorSpaceMatrix cmatrix = null;
+
+    public ImageFilterHue() {
+        mName = "Hue";
+        cmatrix = new ColorSpaceMatrix();
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation =
+                (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Hue");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterHue.class);
+        representation.setMinimum(-180);
+        representation.setMaximum(180);
+        representation.setTextId(R.string.hue);
+        representation.setEditorId(BasicEditor.ID);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float []matrix);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float value = getParameters().getValue();
+        cmatrix.identity();
+        cmatrix.setHue(value);
+
+        nativeApplyFilter(bitmap, w, h, cmatrix.getMatrix());
+
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
new file mode 100644
index 0000000..77cdf47
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.text.format.Time;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterKMeans extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "KMEANS";
+    private int mSeed = 0;
+
+    public ImageFilterKMeans() {
+        mName = "KMeans";
+
+        // set random seed for session
+        Time t = new Time();
+        t.setToNow();
+        mSeed = (int) t.toMillis(false);
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("KMeans");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterKMeans.class);
+        representation.setMaximum(20);
+        representation.setMinimum(2);
+        representation.setValue(4);
+        representation.setDefaultValue(4);
+        representation.setPreviewValue(4);
+        representation.setTextId(R.string.kmeans);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int width, int height,
+            Bitmap large_ds_bm, int lwidth, int lheight, Bitmap small_ds_bm,
+            int swidth, int sheight, int p, int seed);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+
+        Bitmap large_bm_ds = bitmap;
+        Bitmap small_bm_ds = bitmap;
+
+        // find width/height for larger downsampled bitmap
+        int lw = w;
+        int lh = h;
+        while (lw > 256 && lh > 256) {
+            lw /= 2;
+            lh /= 2;
+        }
+        if (lw != w) {
+            large_bm_ds = Bitmap.createScaledBitmap(bitmap, lw, lh, true);
+        }
+
+        // find width/height for smaller downsampled bitmap
+        int sw = lw;
+        int sh = lh;
+        while (sw > 64 && sh > 64) {
+            sw /= 2;
+            sh /= 2;
+        }
+        if (sw != lw) {
+            small_bm_ds = Bitmap.createScaledBitmap(large_bm_ds, sw, sh, true);
+        }
+
+        if (getParameters() != null) {
+            int p = Math.max(getParameters().getValue(), getParameters().getMinimum()) % (getParameters().getMaximum() + 1);
+            nativeApplyFilter(bitmap, w, h, large_bm_ds, lw, lh, small_bm_ds, sw, sh, p, mSeed);
+        }
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java
new file mode 100644
index 0000000..9849759
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java
@@ -0,0 +1,39 @@
+package com.android.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class ImageFilterNegative extends ImageFilter {
+    private static final String SERIALIZATION_NAME = "NEGATIVE";
+    public ImageFilterNegative() {
+        mName = "Negative";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterRepresentation representation = new FilterDirectRepresentation("Negative");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterNegative.class);
+        representation.setTextId(R.string.negative);
+        representation.setShowParameterValue(false);
+        representation.setEditorId(ImageOnlyEditor.ID);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h);
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        nativeApplyFilter(bitmap, w, h);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
new file mode 100644
index 0000000..25e5d14
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+
+public class ImageFilterParametricBorder extends ImageFilter {
+    private FilterColorBorderRepresentation mParameters = null;
+
+    public ImageFilterParametricBorder() {
+        mName = "Border";
+    }
+
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterColorBorderRepresentation parameters = (FilterColorBorderRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    public FilterColorBorderRepresentation getParameters() {
+        return mParameters;
+    }
+
+    private void applyHelper(Canvas canvas, int w, int h) {
+        if (getParameters() == null) {
+            return;
+        }
+        Path border = new Path();
+        border.moveTo(0, 0);
+        float bs = getParameters().getBorderSize() / 100.0f * w;
+        float r = getParameters().getBorderRadius() / 100.0f * w;
+        border.lineTo(0, h);
+        border.lineTo(w, h);
+        border.lineTo(w, 0);
+        border.lineTo(0, 0);
+        border.addRoundRect(new RectF(bs, bs, w - bs, h - bs),
+                r, r, Path.Direction.CW);
+
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setColor(getParameters().getColor());
+        canvas.drawPath(border, paint);
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+       Canvas canvas = new Canvas(bitmap);
+       applyHelper(canvas, bitmap.getWidth(), bitmap.getHeight());
+       return bitmap;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
new file mode 100644
index 0000000..5695ef5
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.v8.renderscript.*;
+import android.util.Log;
+import android.content.res.Resources;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.PipelineInterface;
+
+public abstract class ImageFilterRS extends ImageFilter {
+    private static final String LOGTAG = "ImageFilterRS";
+    private boolean DEBUG = false;
+    private int mLastInputWidth = 0;
+    private int mLastInputHeight = 0;
+    private long mLastTimeCalled;
+
+    public static boolean PERF_LOGGING = false;
+
+    private static ScriptC_grey mGreyConvert = null;
+    private static RenderScript mRScache = null;
+
+    private volatile boolean mResourcesLoaded = false;
+
+    protected abstract void createFilter(android.content.res.Resources res,
+            float scaleFactor, int quality);
+
+    protected void createFilter(android.content.res.Resources res,
+    float scaleFactor, int quality, Allocation in) {}
+    protected void bindScriptValues(Allocation in) {}
+
+    protected abstract void runFilter();
+
+    protected void update(Bitmap bitmap) {
+        getOutPixelsAllocation().copyTo(bitmap);
+    }
+
+    protected RenderScript getRenderScriptContext() {
+        PipelineInterface pipeline = getEnvironment().getPipeline();
+        return pipeline.getRSContext();
+    }
+
+    protected Allocation getInPixelsAllocation() {
+        PipelineInterface pipeline = getEnvironment().getPipeline();
+        return pipeline.getInPixelsAllocation();
+    }
+
+    protected Allocation getOutPixelsAllocation() {
+        PipelineInterface pipeline = getEnvironment().getPipeline();
+        return pipeline.getOutPixelsAllocation();
+    }
+
+    @Override
+    public void apply(Allocation in, Allocation out) {
+        long startOverAll = System.nanoTime();
+        if (PERF_LOGGING) {
+            long delay = (startOverAll - mLastTimeCalled) / 1000;
+            String msg = String.format("%s; image size %dx%d; ", getName(),
+                    in.getType().getX(), in.getType().getY());
+            msg += String.format("called after %.2f ms (%.2f FPS); ",
+                    delay / 1000.f, 1000000.f / delay);
+            Log.i(LOGTAG, msg);
+        }
+        mLastTimeCalled = startOverAll;
+        long startFilter = 0;
+        long endFilter = 0;
+        if (!mResourcesLoaded) {
+            PipelineInterface pipeline = getEnvironment().getPipeline();
+            createFilter(pipeline.getResources(), getEnvironment().getScaleFactor(),
+                    getEnvironment().getQuality(), in);
+            mResourcesLoaded = true;
+        }
+        startFilter = System.nanoTime();
+        bindScriptValues(in);
+        run(in, out);
+        if (PERF_LOGGING) {
+            getRenderScriptContext().finish();
+            endFilter = System.nanoTime();
+            long endOverAll = System.nanoTime();
+            String msg = String.format("%s; image size %dx%d; ", getName(),
+                    in.getType().getX(), in.getType().getY());
+            long timeOverAll = (endOverAll - startOverAll) / 1000;
+            long timeFilter = (endFilter - startFilter) / 1000;
+            msg += String.format("over all %.2f ms (%.2f FPS); ",
+                    timeOverAll / 1000.f, 1000000.f / timeOverAll);
+            msg += String.format("run filter %.2f ms (%.2f FPS)",
+                    timeFilter / 1000.f, 1000000.f / timeFilter);
+            Log.i(LOGTAG, msg);
+        }
+    }
+
+    protected void run(Allocation in, Allocation out) {}
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
+            return bitmap;
+        }
+        try {
+            PipelineInterface pipeline = getEnvironment().getPipeline();
+            if (DEBUG) {
+                Log.v(LOGTAG, "apply filter " + getName() + " in pipeline " + pipeline.getName());
+            }
+            Resources rsc = pipeline.getResources();
+            boolean sizeChanged = false;
+            if (getInPixelsAllocation() != null
+                    && ((getInPixelsAllocation().getType().getX() != mLastInputWidth)
+                    || (getInPixelsAllocation().getType().getY() != mLastInputHeight))) {
+                sizeChanged = true;
+            }
+            if (pipeline.prepareRenderscriptAllocations(bitmap)
+                    || !isResourcesLoaded() || sizeChanged) {
+                freeResources();
+                createFilter(rsc, scaleFactor, quality);
+                setResourcesLoaded(true);
+                mLastInputWidth = getInPixelsAllocation().getType().getX();
+                mLastInputHeight = getInPixelsAllocation().getType().getY();
+            }
+            bindScriptValues();
+            runFilter();
+            update(bitmap);
+            if (DEBUG) {
+                Log.v(LOGTAG, "DONE apply filter " + getName() + " in pipeline " + pipeline.getName());
+            }
+        } catch (android.renderscript.RSIllegalArgumentException e) {
+            Log.e(LOGTAG, "Illegal argument? " + e);
+        } catch (android.renderscript.RSRuntimeException e) {
+            Log.e(LOGTAG, "RS runtime exception ? " + e);
+        } catch (java.lang.OutOfMemoryError e) {
+            // Many of the renderscript filters allocated large (>16Mb resources) in order to apply.
+            System.gc();
+            displayLowMemoryToast();
+            Log.e(LOGTAG, "not enough memory for filter " + getName(), e);
+        }
+        return bitmap;
+    }
+
+    protected static Allocation convertBitmap(RenderScript RS, Bitmap bitmap) {
+        return Allocation.createFromBitmap(RS, bitmap,
+                Allocation.MipmapControl.MIPMAP_NONE,
+                Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE);
+    }
+
+    private static Allocation convertRGBAtoA(RenderScript RS, Bitmap bitmap) {
+        if (RS != mRScache || mGreyConvert == null) {
+            mGreyConvert = new ScriptC_grey(RS, RS.getApplicationContext().getResources(),
+                                            R.raw.grey);
+            mRScache = RS;
+        }
+
+        Type.Builder tb_a8 = new Type.Builder(RS, Element.A_8(RS));
+
+        Allocation bitmapTemp = convertBitmap(RS, bitmap);
+        if (bitmapTemp.getType().getElement().isCompatible(Element.A_8(RS))) {
+            return bitmapTemp;
+        }
+
+        tb_a8.setX(bitmapTemp.getType().getX());
+        tb_a8.setY(bitmapTemp.getType().getY());
+        Allocation bitmapAlloc = Allocation.createTyped(RS, tb_a8.create(),
+                                                        Allocation.MipmapControl.MIPMAP_NONE,
+                                                        Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE);
+        mGreyConvert.forEach_RGBAtoA(bitmapTemp, bitmapAlloc);
+        bitmapTemp.destroy();
+        return bitmapAlloc;
+    }
+
+    public Allocation loadScaledResourceAlpha(int resource, int inSampleSize) {
+        Resources res = getEnvironment().getPipeline().getResources();
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ALPHA_8;
+        options.inSampleSize      = inSampleSize;
+        Bitmap bitmap = BitmapFactory.decodeResource(
+                res,
+                resource, options);
+        Allocation ret = convertRGBAtoA(getRenderScriptContext(), bitmap);
+        bitmap.recycle();
+        return ret;
+    }
+
+    public Allocation loadScaledResourceAlpha(int resource, int w, int h, int inSampleSize) {
+        Resources res = getEnvironment().getPipeline().getResources();
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ALPHA_8;
+        options.inSampleSize      = inSampleSize;
+        Bitmap bitmap = BitmapFactory.decodeResource(
+                res,
+                resource, options);
+        Bitmap resizeBitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
+        Allocation ret = convertRGBAtoA(getRenderScriptContext(), resizeBitmap);
+        resizeBitmap.recycle();
+        bitmap.recycle();
+        return ret;
+    }
+
+    public Allocation loadResourceAlpha(int resource) {
+        return loadScaledResourceAlpha(resource, 1);
+    }
+
+    public Allocation loadResource(int resource) {
+        Resources res = getEnvironment().getPipeline().getResources();
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        Bitmap bitmap = BitmapFactory.decodeResource(
+                res,
+                resource, options);
+        Allocation ret = convertBitmap(getRenderScriptContext(), bitmap);
+        bitmap.recycle();
+        return ret;
+    }
+
+    private boolean isResourcesLoaded() {
+        return mResourcesLoaded;
+    }
+
+    private void setResourcesLoaded(boolean resourcesLoaded) {
+        mResourcesLoaded = resourcesLoaded;
+    }
+
+    /**
+     *  Bitmaps and RS Allocations should be cleared here
+     */
+    abstract protected void resetAllocations();
+
+    /**
+     * RS Script objects (and all other RS objects) should be cleared here
+     */
+    public abstract void resetScripts();
+
+    /**
+     * Scripts values should be bound here
+     */
+    abstract protected void bindScriptValues();
+
+    public void freeResources() {
+        if (!isResourcesLoaded()) {
+            return;
+        }
+        resetAllocations();
+        mLastInputWidth = 0;
+        mLastInputHeight = 0;
+        setResourcesLoaded(false);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java
new file mode 100644
index 0000000..511f9e9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+import java.util.Vector;
+
+public class ImageFilterRedEye extends ImageFilter {
+    private static final String LOGTAG = "ImageFilterRedEye";
+    FilterRedEyeRepresentation mParameters = new FilterRedEyeRepresentation();
+
+    public ImageFilterRedEye() {
+        mName = "Red Eye";
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return new FilterRedEyeRepresentation();
+    }
+
+    public boolean isNil() {
+        return mParameters.isNil();
+    }
+
+    public Vector<FilterPoint> getCandidates() {
+        return mParameters.getCandidates();
+    }
+
+    public void clear() {
+        mParameters.clearCandidates();
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, short[] matrix);
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterRedEyeRepresentation parameters = (FilterRedEyeRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        short[] rect = new short[4];
+
+        int size = mParameters.getNumberOfCandidates();
+        Matrix originalToScreen = getOriginalToScreenMatrix(w, h);
+        for (int i = 0; i < size; i++) {
+            RectF r = new RectF(((RedEyeCandidate) (mParameters.getCandidate(i))).mRect);
+            originalToScreen.mapRect(r);
+            if (r.intersect(0, 0, w, h)) {
+                rect[0] = (short) r.left;
+                rect[1] = (short) r.top;
+                rect[2] = (short) r.width();
+                rect[3] = (short) r.height();
+                nativeApplyFilter(bitmap, w, h, rect);
+            }
+        }
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
new file mode 100644
index 0000000..c3124ff
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterSaturated extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "SATURATED";
+    public ImageFilterSaturated() {
+        mName = "Saturated";
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation =
+                (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Saturated");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterSaturated.class);
+        representation.setTextId(R.string.saturation);
+        representation.setMinimum(-100);
+        representation.setMaximum(100);
+        representation.setDefaultValue(0);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float saturation);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        int p = getParameters().getValue();
+        float value = 1 +  p / 100.0f;
+        nativeApplyFilter(bitmap, w, h, value);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
new file mode 100644
index 0000000..bd119bb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterShadows extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "SHADOWS";
+    public ImageFilterShadows() {
+        mName = "Shadows";
+
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation =
+                (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Shadows");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterShadows.class);
+        representation.setTextId(R.string.shadow_recovery);
+        representation.setMinimum(-100);
+        representation.setMaximum(100);
+        representation.setDefaultValue(0);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float  factor);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float p = getParameters().getValue();
+
+        nativeApplyFilter(bitmap, w, h, p);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
new file mode 100644
index 0000000..3bd7944
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterSharpen extends ImageFilterRS {
+    private static final String SERIALIZATION_NAME = "SHARPEN";
+    private static final String LOGTAG = "ImageFilterSharpen";
+    private ScriptC_convolve3x3 mScript;
+
+    private FilterBasicRepresentation mParameters;
+
+    public ImageFilterSharpen() {
+        mName = "Sharpen";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterRepresentation representation = new FilterBasicRepresentation("Sharpen", 0, 0, 100);
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setShowParameterValue(true);
+        representation.setFilterClass(ImageFilterSharpen.class);
+        representation.setTextId(R.string.sharpness);
+        representation.setOverlayId(R.drawable.filtershow_button_colors_sharpen);
+        representation.setEditorId(R.id.imageShow);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterBasicRepresentation parameters = (FilterBasicRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    @Override
+    protected void resetAllocations() {
+        // nothing to do
+    }
+
+    @Override
+    public void resetScripts() {
+        if (mScript != null) {
+            mScript.destroy();
+            mScript = null;
+        }
+    }
+
+    @Override
+    protected void createFilter(android.content.res.Resources res, float scaleFactor,
+            int quality) {
+        if (mScript == null) {
+            mScript = new ScriptC_convolve3x3(getRenderScriptContext(), res, R.raw.convolve3x3);
+        }
+    }
+
+    private void computeKernel() {
+        float scaleFactor = getEnvironment().getScaleFactor();
+        float p1 = mParameters.getValue() * scaleFactor;
+        float value = p1 / 100.0f;
+        float f[] = new float[9];
+        float p = value;
+        f[0] = -p;
+        f[1] = -p;
+        f[2] = -p;
+        f[3] = -p;
+        f[4] = 8 * p + 1;
+        f[5] = -p;
+        f[6] = -p;
+        f[7] = -p;
+        f[8] = -p;
+        mScript.set_gCoeffs(f);
+    }
+
+    @Override
+    protected void bindScriptValues() {
+        int w = getInPixelsAllocation().getType().getX();
+        int h = getInPixelsAllocation().getType().getY();
+        mScript.set_gWidth(w);
+        mScript.set_gHeight(h);
+    }
+
+    @Override
+    protected void runFilter() {
+        if (mParameters == null) {
+            return;
+        }
+        computeKernel();
+        mScript.set_gIn(getInPixelsAllocation());
+        mScript.bind_gPixels(getInPixelsAllocation());
+        mScript.forEach_root(getInPixelsAllocation(), getOutPixelsAllocation());
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
new file mode 100644
index 0000000..77250bd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.android.gallery3d.app.Log;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+/**
+ * An image filter which creates a tiny planet projection.
+ */
+public class ImageFilterTinyPlanet extends SimpleImageFilter {
+
+
+    private static final String LOGTAG = ImageFilterTinyPlanet.class.getSimpleName();
+    public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/";
+    FilterTinyPlanetRepresentation mParameters = new FilterTinyPlanetRepresentation();
+
+    public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS =
+            "CroppedAreaImageWidthPixels";
+    public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS =
+            "CroppedAreaImageHeightPixels";
+    public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS =
+            "FullPanoWidthPixels";
+    public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS =
+            "FullPanoHeightPixels";
+    public static final String CROPPED_AREA_LEFT =
+            "CroppedAreaLeftPixels";
+    public static final String CROPPED_AREA_TOP =
+            "CroppedAreaTopPixels";
+
+    public ImageFilterTinyPlanet() {
+        mName = "TinyPlanet";
+    }
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterTinyPlanetRepresentation parameters = (FilterTinyPlanetRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        return new FilterTinyPlanetRepresentation();
+    }
+
+
+    native protected void nativeApplyFilter(
+            Bitmap bitmapIn, int width, int height, Bitmap bitmapOut, int outSize, float scale,
+            float angle);
+
+
+    @Override
+    public Bitmap apply(Bitmap bitmapIn, float scaleFactor, int quality) {
+        int w = bitmapIn.getWidth();
+        int h = bitmapIn.getHeight();
+        int outputSize = (int) (w / 2f);
+        ImagePreset preset = getEnvironment().getImagePreset();
+        Bitmap mBitmapOut = null;
+        if (preset != null) {
+            XMPMeta xmp = ImageLoader.getXmpObject(MasterImage.getImage().getActivity());
+            // Do nothing, just use bitmapIn as is if we don't have XMP.
+            if(xmp != null) {
+                bitmapIn = applyXmp(bitmapIn, xmp, w);
+            }
+        }
+        if (mBitmapOut != null) {
+            if (outputSize != mBitmapOut.getHeight()) {
+                mBitmapOut = null;
+            }
+        }
+        while (mBitmapOut == null) {
+            try {
+                mBitmapOut = getEnvironment().getBitmap(outputSize, outputSize);
+            } catch (java.lang.OutOfMemoryError e) {
+                System.gc();
+                outputSize /= 2;
+                Log.v(LOGTAG, "No memory to create Full Tiny Planet create half");
+            }
+        }
+        nativeApplyFilter(bitmapIn, bitmapIn.getWidth(), bitmapIn.getHeight(), mBitmapOut,
+                outputSize, mParameters.getZoom() / 100f, mParameters.getAngle());
+
+        return mBitmapOut;
+    }
+
+    private Bitmap applyXmp(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
+        try {
+            int croppedAreaWidth =
+                    getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
+            int croppedAreaHeight =
+                    getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
+            int fullPanoWidth =
+                    getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
+            int fullPanoHeight =
+                    getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
+            int left = getInt(xmp, CROPPED_AREA_LEFT);
+            int top = getInt(xmp, CROPPED_AREA_TOP);
+
+            if (fullPanoWidth == 0 || fullPanoHeight == 0) {
+                return bitmapIn;
+            }
+            // Make sure the intermediate image has the similar size to the
+            // input.
+            Bitmap paddedBitmap = null;
+            float scale = intermediateWidth / (float) fullPanoWidth;
+            while (paddedBitmap == null) {
+                try {
+                    paddedBitmap = Bitmap.createBitmap(
+                            (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
+                            Bitmap.Config.ARGB_8888);
+                } catch (java.lang.OutOfMemoryError e) {
+                    System.gc();
+                    scale /= 2;
+                }
+            }
+            Canvas paddedCanvas = new Canvas(paddedBitmap);
+
+            int right = left + croppedAreaWidth;
+            int bottom = top + croppedAreaHeight;
+            RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
+            paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
+            bitmapIn = paddedBitmap;
+        } catch (XMPException ex) {
+            // Do nothing, just use bitmapIn as is.
+        }
+        return bitmapIn;
+    }
+
+    private static int getInt(XMPMeta xmp, String key) throws XMPException {
+        if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
+            return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
new file mode 100644
index 0000000..86be9a1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterVibrance extends SimpleImageFilter {
+    private static final String SERIALIZATION_NAME = "VIBRANCE";
+    public ImageFilterVibrance() {
+        mName = "Vibrance";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterBasicRepresentation representation =
+                (FilterBasicRepresentation) super.getDefaultRepresentation();
+        representation.setName("Vibrance");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterVibrance.class);
+        representation.setTextId(R.string.vibrance);
+        representation.setMinimum(-100);
+        representation.setMaximum(100);
+        representation.setDefaultValue(0);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float bright);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (getParameters() == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float value = getParameters().getValue();
+        nativeApplyFilter(bitmap, w, h, value);
+
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
new file mode 100644
index 0000000..7e0a452
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public class ImageFilterVignette extends SimpleImageFilter {
+    private static final String LOGTAG = "ImageFilterVignette";
+    private Bitmap mOverlayBitmap;
+
+    public ImageFilterVignette() {
+        mName = "Vignette";
+    }
+
+    @Override
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterVignetteRepresentation representation = new FilterVignetteRepresentation();
+        return representation;
+    }
+
+    native protected void nativeApplyFilter(
+            Bitmap bitmap, int w, int h, int cx, int cy, float radx, float rady, float strength);
+
+    private float calcRadius(float cx, float cy, int w, int h) {
+        float d = cx;
+        if (d < (w - cx)) {
+            d = w - cx;
+        }
+        if (d < cy) {
+            d = cy;
+        }
+        if (d < (h - cy)) {
+            d = h - cy;
+        }
+        return d * d * 2.0f;
+    }
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) {
+            if (mOverlayBitmap == null) {
+                Resources res = getEnvironment().getPipeline().getResources();
+                mOverlayBitmap = IconUtilities.getFXBitmap(res,
+                        R.drawable.filtershow_icon_vignette);
+            }
+            Canvas c = new Canvas(bitmap);
+            int dim = Math.max(bitmap.getWidth(), bitmap.getHeight());
+            Rect r = new Rect(0, 0, dim, dim);
+            c.drawBitmap(mOverlayBitmap, null, r, null);
+            return bitmap;
+        }
+        FilterVignetteRepresentation rep = (FilterVignetteRepresentation) getParameters();
+        if (rep == null) {
+            return bitmap;
+        }
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        float value = rep.getValue() / 100.0f;
+        float cx = w / 2;
+        float cy = h / 2;
+        float r = calcRadius(cx, cy, w, h);
+        float rx = r;
+        float ry = r;
+        if (rep.isCenterSet()) {
+            Matrix m = getOriginalToScreenMatrix(w, h);
+            cx = rep.getCenterX();
+            cy = rep.getCenterY();
+            float[] center = new float[] { cx, cy };
+            m.mapPoints(center);
+            cx = center[0];
+            cy = center[1];
+            rx = m.mapRadius(rep.getRadiusX());
+            ry = m.mapRadius(rep.getRadiusY());
+         }
+        nativeApplyFilter(bitmap, w, h, (int) cx, (int) cy, rx, ry, value);
+        return bitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
new file mode 100644
index 0000000..6bb88ec
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterWBalance extends ImageFilter {
+    private static final String SERIALIZATION_NAME = "WBALANCE";
+    private static final String TAG = "ImageFilterWBalance";
+
+    public ImageFilterWBalance() {
+        mName = "WBalance";
+    }
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterRepresentation representation = new FilterDirectRepresentation("WBalance");
+        representation.setSerializationName(SERIALIZATION_NAME);
+        representation.setFilterClass(ImageFilterWBalance.class);
+        representation.setFilterType(FilterRepresentation.TYPE_WBALANCE);
+        representation.setTextId(R.string.wbalance);
+        representation.setShowParameterValue(false);
+        representation.setEditorId(ImageOnlyEditor.ID);
+        representation.setSupportsPartialRendering(true);
+        return representation;
+    }
+
+    @Override
+    public void useRepresentation(FilterRepresentation representation) {
+
+    }
+
+    native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, int locX, int locY);
+
+    @Override
+    public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+        nativeApplyFilter(bitmap, w, h, -1, -1);
+        return bitmap;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java b/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java
new file mode 100644
index 0000000..a40d4fa
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.graphics.RectF;
+
+public class RedEyeCandidate implements FilterPoint {
+    RectF mRect = new RectF();
+    RectF mBounds = new RectF();
+
+    public RedEyeCandidate(RedEyeCandidate candidate) {
+        mRect.set(candidate.mRect);
+        mBounds.set(candidate.mBounds);
+    }
+
+    public RedEyeCandidate(RectF rect, RectF bounds) {
+        mRect.set(rect);
+        mBounds.set(bounds);
+    }
+
+    public boolean equals(RedEyeCandidate candidate) {
+        if (candidate.mRect.equals(mRect)
+                && candidate.mBounds.equals(mBounds)) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean intersect(RectF rect) {
+        return mRect.intersect(rect);
+    }
+
+    public RectF getRect() {
+        return mRect;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java b/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java
new file mode 100644
index 0000000..c891d20
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+public class SimpleImageFilter extends ImageFilter {
+
+    private FilterBasicRepresentation mParameters;
+
+    public FilterRepresentation getDefaultRepresentation() {
+        FilterRepresentation representation = new FilterBasicRepresentation("Default", 0, 50, 100);
+        representation.setShowParameterValue(true);
+        return representation;
+    }
+
+    public void useRepresentation(FilterRepresentation representation) {
+        FilterBasicRepresentation parameters = (FilterBasicRepresentation) representation;
+        mParameters = parameters;
+    }
+
+    public FilterBasicRepresentation getParameters() {
+        return mParameters;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/SplineMath.java b/src/com/android/gallery3d/filtershow/filters/SplineMath.java
new file mode 100644
index 0000000..5b12d0a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/SplineMath.java
@@ -0,0 +1,166 @@
+package com.android.gallery3d.filtershow.filters;
+
+
+public class SplineMath {
+    double[][] mPoints = new double[6][2];
+    double[] mDerivatives;
+    SplineMath(int n) {
+        mPoints = new double[n][2];
+    }
+
+    public void setPoint(int index, double x, double y) {
+        mPoints[index][0] = x;
+        mPoints[index][1] = y;
+        mDerivatives = null;
+    }
+
+    public float[][] calculatetCurve(int n) {
+        float[][] curve = new float[n][2];
+        double[][] points = new double[mPoints.length][2];
+        for (int i = 0; i < mPoints.length; i++) {
+
+            points[i][0] = mPoints[i][0];
+            points[i][1] = mPoints[i][1];
+
+        }
+        double[] derivatives = solveSystem(points);
+        float start = (float) points[0][0];
+        float end = (float) (points[points.length - 1][0]);
+
+        curve[0][0] = (float) (points[0][0]);
+        curve[0][1] = (float) (points[0][1]);
+        int last = curve.length - 1;
+        curve[last][0] = (float) (points[points.length - 1][0]);
+        curve[last][1] = (float) (points[points.length - 1][1]);
+
+        for (int i = 0; i < curve.length; i++) {
+
+            double[] cur = null;
+            double[] next = null;
+            double x = start + i * (end - start) / (curve.length - 1);
+            int pivot = 0;
+            for (int j = 0; j < points.length - 1; j++) {
+                if (x >= points[j][0] && x <= points[j + 1][0]) {
+                    pivot = j;
+                }
+            }
+            cur = points[pivot];
+            next = points[pivot + 1];
+            if (x <= next[0]) {
+                double x1 = cur[0];
+                double x2 = next[0];
+                double y1 = cur[1];
+                double y2 = next[1];
+
+                // Use the second derivatives to apply the cubic spline
+                // equation:
+                double delta = (x2 - x1);
+                double delta2 = delta * delta;
+                double b = (x - x1) / delta;
+                double a = 1 - b;
+                double ta = a * y1;
+                double tb = b * y2;
+                double tc = (a * a * a - a) * derivatives[pivot];
+                double td = (b * b * b - b) * derivatives[pivot + 1];
+                double y = ta + tb + (delta2 / 6) * (tc + td);
+
+                curve[i][0] = (float) (x);
+                curve[i][1] = (float) (y);
+            } else {
+                curve[i][0] = (float) (next[0]);
+                curve[i][1] = (float) (next[1]);
+            }
+        }
+        return curve;
+    }
+
+    public double getValue(double x) {
+        double[] cur = null;
+        double[] next = null;
+        if (mDerivatives == null)
+            mDerivatives = solveSystem(mPoints);
+        int pivot = 0;
+        for (int j = 0; j < mPoints.length - 1; j++) {
+            pivot = j;
+            if (x <= mPoints[j][0]) {
+                break;
+            }
+        }
+        cur = mPoints[pivot];
+        next = mPoints[pivot + 1];
+        double x1 = cur[0];
+        double x2 = next[0];
+        double y1 = cur[1];
+        double y2 = next[1];
+
+        // Use the second derivatives to apply the cubic spline
+        // equation:
+        double delta = (x2 - x1);
+        double delta2 = delta * delta;
+        double b = (x - x1) / delta;
+        double a = 1 - b;
+        double ta = a * y1;
+        double tb = b * y2;
+        double tc = (a * a * a - a) * mDerivatives[pivot];
+        double td = (b * b * b - b) * mDerivatives[pivot + 1];
+        double y = ta + tb + (delta2 / 6) * (tc + td);
+
+        return y;
+
+    }
+
+    double[] solveSystem(double[][] points) {
+        int n = points.length;
+        double[][] system = new double[n][3];
+        double[] result = new double[n]; // d
+        double[] solution = new double[n]; // returned coefficients
+        system[0][1] = 1;
+        system[n - 1][1] = 1;
+        double d6 = 1.0 / 6.0;
+        double d3 = 1.0 / 3.0;
+
+        // let's create a tridiagonal matrix representing the
+        // system, and apply the TDMA algorithm to solve it
+        // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
+        for (int i = 1; i < n - 1; i++) {
+            double deltaPrevX = points[i][0] - points[i - 1][0];
+            double deltaX = points[i + 1][0] - points[i - 1][0];
+            double deltaNextX = points[i + 1][0] - points[i][0];
+            double deltaNextY = points[i + 1][1] - points[i][1];
+            double deltaPrevY = points[i][1] - points[i - 1][1];
+            system[i][0] = d6 * deltaPrevX; // a_i
+            system[i][1] = d3 * deltaX; // b_i
+            system[i][2] = d6 * deltaNextX; // c_i
+            result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i
+        }
+
+        // Forward sweep
+        for (int i = 1; i < n; i++) {
+            // m = a_i/b_i-1
+            double m = system[i][0] / system[i - 1][1];
+            // b_i = b_i - m(c_i-1)
+            system[i][1] = system[i][1] - m * system[i - 1][2];
+            // d_i = d_i - m(d_i-1)
+            result[i] = result[i] - m * result[i - 1];
+        }
+
+        // Back substitution
+        solution[n - 1] = result[n - 1] / system[n - 1][1];
+        for (int i = n - 2; i >= 0; --i) {
+            solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1];
+        }
+        return solution;
+    }
+
+    public static void main(String[] args) {
+        SplineMath s = new SplineMath(10);
+        for (int i = 0; i < 10; i++) {
+            s.setPoint(i, i, i);
+        }
+        float[][] curve = s.calculatetCurve(40);
+
+        for (int j = 0; j < curve.length; j++) {
+            System.out.println(curve[j][0] + "," + curve[j][1]);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs b/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs
new file mode 100644
index 0000000..2acffab
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+#pragma rs_fp_relaxed
+
+int32_t gWidth;
+int32_t gHeight;
+const uchar4 *gPixels;
+rs_allocation gIn;
+
+float gCoeffs[9];
+
+void root(const uchar4 *in, uchar4 *out, const void *usrData, uint32_t x, uint32_t y) {
+    uint32_t x1 = min((int32_t)x+1, gWidth-1);
+    uint32_t x2 = max((int32_t)x-1, 0);
+    uint32_t y1 = min((int32_t)y+1, gHeight-1);
+    uint32_t y2 = max((int32_t)y-1, 0);
+
+    float4 p00 = rsUnpackColor8888(gPixels[x1 + gWidth * y1]);
+    float4 p01 = rsUnpackColor8888(gPixels[x + gWidth * y1]);
+    float4 p02 = rsUnpackColor8888(gPixels[x2 + gWidth * y1]);
+    float4 p10 = rsUnpackColor8888(gPixels[x1 + gWidth * y]);
+    float4 p11 = rsUnpackColor8888(gPixels[x + gWidth * y]);
+    float4 p12 = rsUnpackColor8888(gPixels[x2 + gWidth * y]);
+    float4 p20 = rsUnpackColor8888(gPixels[x1 + gWidth * y2]);
+    float4 p21 = rsUnpackColor8888(gPixels[x + gWidth * y2]);
+    float4 p22 = rsUnpackColor8888(gPixels[x2 + gWidth * y2]);
+
+    p00 *= gCoeffs[0];
+    p01 *= gCoeffs[1];
+    p02 *= gCoeffs[2];
+    p10 *= gCoeffs[3];
+    p11 *= gCoeffs[4];
+    p12 *= gCoeffs[5];
+    p20 *= gCoeffs[6];
+    p21 *= gCoeffs[7];
+    p22 *= gCoeffs[8];
+
+    p00 += p01;
+    p02 += p10;
+    p11 += p12;
+    p20 += p21;
+
+    p22 += p00;
+    p02 += p11;
+
+    p20 += p22;
+    p20 += p02;
+
+    p20 = clamp(p20, 0.f, 1.f);
+    *out = rsPackColorTo8888(p20.r, p20.g, p20.b);
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/grad.rs b/src/com/android/gallery3d/filtershow/filters/grad.rs
new file mode 100644
index 0000000..ddbafd3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/grad.rs
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Unknown
+ *
+ * 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+
+#define MAX_POINTS 16
+
+uint32_t inputWidth;
+uint32_t inputHeight;
+static const float Rf = 0.2999f;
+static const float Gf = 0.587f;
+static const float Bf = 0.114f;
+//static const float size_scale = 0.01f;
+
+typedef struct {
+    rs_matrix3x3 colorMatrix;
+    float rgbOff;
+    float dx;
+    float dy;
+    float off;
+} UPointData;
+int mNumberOfLines;
+// input data
+bool mask[MAX_POINTS];
+int xPos1[MAX_POINTS];
+int yPos1[MAX_POINTS];
+int xPos2[MAX_POINTS];
+int yPos2[MAX_POINTS];
+int size[MAX_POINTS];
+int brightness[MAX_POINTS];
+int contrast[MAX_POINTS];
+int saturation[MAX_POINTS];
+
+// generated data
+static UPointData grads[MAX_POINTS];
+
+void setupGradParams() {
+    int k = 0;
+    for (int i = 0; i < MAX_POINTS; i++) {
+      if (!mask[i]) {
+         continue;
+      }
+      float x1 = xPos1[i];
+      float y1 = yPos1[i];
+      float x2 = xPos2[i];
+      float y2 = yPos2[i];
+
+      float denom = (y2 * y2 - 2 * y1 * y2 + x2 * x2 - 2 * x1 * x2 + y1 * y1 + x1 * x1);
+      if (denom == 0) {
+         continue;
+      }
+      grads[k].dy = (y1 - y2) / denom;
+      grads[k].dx = (x1 - x2) / denom;
+      grads[k].off = (y2 * y2 + x2 * x2 - x1 * x2 - y1 * y2) / denom;
+
+      float S = 1+saturation[i]/100.f;
+      float MS = 1-S;
+      float Rt = Rf * MS;
+      float Gt = Gf * MS;
+      float Bt = Bf * MS;
+
+      float b = 1+brightness[i]/100.f;
+      float c = 1+contrast[i]/100.f;
+      b *= c;
+      grads[k].rgbOff = .5f - c/2.f;
+      rsMatrixSet(&grads[i].colorMatrix, 0, 0, b * (Rt + S));
+      rsMatrixSet(&grads[i].colorMatrix, 1, 0, b * Gt);
+      rsMatrixSet(&grads[i].colorMatrix, 2, 0, b * Bt);
+      rsMatrixSet(&grads[i].colorMatrix, 0, 1, b * Rt);
+      rsMatrixSet(&grads[i].colorMatrix, 1, 1, b * (Gt + S));
+      rsMatrixSet(&grads[i].colorMatrix, 2, 1, b * Bt);
+      rsMatrixSet(&grads[i].colorMatrix, 0, 2, b * Rt);
+      rsMatrixSet(&grads[i].colorMatrix, 1, 2, b * Gt);
+      rsMatrixSet(&grads[i].colorMatrix, 2, 2, b * (Bt + S));
+
+      k++;
+    }
+    mNumberOfLines = k;
+}
+
+void init() {
+
+}
+
+uchar4 __attribute__((kernel)) selectiveAdjust(const uchar4 in, uint32_t x,
+    uint32_t y) {
+    float4 pixel = rsUnpackColor8888(in);
+
+    float4 wsum = pixel;
+    wsum.a = 0.f;
+    for (int i = 0; i < mNumberOfLines; i++) {
+        UPointData* grad = &grads[i];
+        float t = clamp(x*grad->dx+y*grad->dy+grad->off,0.f,1.0f);
+        wsum.xyz = wsum.xyz*(1-t)+
+            t*(rsMatrixMultiply(&grad->colorMatrix ,wsum.xyz)+grad->rgbOff);
+
+    }
+
+    pixel.rgb = wsum.rgb;
+    pixel.a = 1.0f;
+
+    uchar4 out = rsPackColorTo8888(clamp(pixel, 0.f, 1.0f));
+    return out;
+}
+
+
+
diff --git a/src/com/android/gallery3d/filtershow/filters/grey.rs b/src/com/android/gallery3d/filtershow/filters/grey.rs
new file mode 100644
index 0000000..e018803
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/grey.rs
@@ -0,0 +1,22 @@
+  /*
+ * Copyright (C) 2013 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+
+uchar __attribute__((kernel)) RGBAtoA(uchar4 in) {
+    return in.r;
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/saturation.rs b/src/com/android/gallery3d/filtershow/filters/saturation.rs
new file mode 100644
index 0000000..5210e34
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/saturation.rs
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2012 Unknown
+ *
+ * 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+
+#define MAX_CHANELS 7
+#define MAX_HUE 4096
+static const int ABITS = 4;
+static const int HSCALE = 256;
+static const int k1=255 << ABITS;
+static const int k2=HSCALE << ABITS;
+
+static const float Rf = 0.2999f;
+static const float Gf = 0.587f;
+static const float Bf = 0.114f;
+
+rs_matrix3x3 colorMatrix_min;
+rs_matrix3x3 colorMatrix_max;
+
+int mNumberOfLines;
+// input data
+int saturation[MAX_CHANELS];
+float sat[MAX_CHANELS];
+
+float satLut[MAX_HUE];
+// generated data
+
+
+void setupGradParams() {
+
+    int master = saturation[0];
+    int max = master+saturation[1];
+    int min = max;
+
+    // calculate the minimum and maximum saturation
+    for (int i = 1; i < MAX_CHANELS; i++) {
+       int v = master+saturation[i];
+       if (max < v) {
+         max = v;
+       }
+       else if (min > v) {
+         min = v;
+       }
+    }
+    // generate a lookup table for all hue 0 to 4K  which goes from 0 to 1 0=min sat 1 = max sat
+    min = min - 1;
+    for(int i = 0; i < MAX_HUE ; i++) {
+       float p =  i * 6 / (float)MAX_HUE;
+       int ip = ((int)(p + .5f)) % 6;
+       int v = master + saturation[ip + 1];
+       satLut[i] = (v - min)/(float)(max - min);
+    }
+
+    float S = 1 + max / 100.f;
+    float MS = 1 - S;
+    float Rt = Rf * MS;
+    float Gt = Gf * MS;
+    float Bt = Bf * MS;
+    float b = 1.f;
+
+    // Generate 2 color matrix one at min sat and one at max
+    rsMatrixSet(&colorMatrix_max, 0, 0, b * (Rt + S));
+    rsMatrixSet(&colorMatrix_max, 1, 0, b * Gt);
+    rsMatrixSet(&colorMatrix_max, 2, 0, b * Bt);
+    rsMatrixSet(&colorMatrix_max, 0, 1, b * Rt);
+    rsMatrixSet(&colorMatrix_max, 1, 1, b * (Gt + S));
+    rsMatrixSet(&colorMatrix_max, 2, 1, b * Bt);
+    rsMatrixSet(&colorMatrix_max, 0, 2, b * Rt);
+    rsMatrixSet(&colorMatrix_max, 1, 2, b * Gt);
+    rsMatrixSet(&colorMatrix_max, 2, 2, b * (Bt + S));
+
+    S = 1 + min / 100.f;
+    MS = 1-S;
+    Rt = Rf * MS;
+    Gt = Gf * MS;
+    Bt = Bf * MS;
+    b = 1;
+
+    rsMatrixSet(&colorMatrix_min, 0, 0, b * (Rt + S));
+    rsMatrixSet(&colorMatrix_min, 1, 0, b * Gt);
+    rsMatrixSet(&colorMatrix_min, 2, 0, b * Bt);
+    rsMatrixSet(&colorMatrix_min, 0, 1, b * Rt);
+    rsMatrixSet(&colorMatrix_min, 1, 1, b * (Gt + S));
+    rsMatrixSet(&colorMatrix_min, 2, 1, b * Bt);
+    rsMatrixSet(&colorMatrix_min, 0, 2, b * Rt);
+    rsMatrixSet(&colorMatrix_min, 1, 2, b * Gt);
+    rsMatrixSet(&colorMatrix_min, 2, 2, b * (Bt + S));
+}
+
+static ushort rgb2hue( uchar4 rgb)
+{
+    int iMin,iMax,chroma;
+
+    int ri = rgb.r;
+    int gi = rgb.g;
+    int bi = rgb.b;
+    short rv,rs,rh;
+
+    if (ri > gi) {
+        iMax = max (ri, bi);
+        iMin = min (gi, bi);
+    } else {
+        iMax = max (gi, bi);
+        iMin = min (ri, bi);
+    }
+
+    rv = (short) (iMax << ABITS);
+
+    if (rv == 0) {
+        return 0;
+    }
+
+    chroma = iMax - iMin;
+    rs = (short) ((k1 * chroma) / iMax);
+    if (rs == 0) {
+        return 0;
+    }
+
+    if ( ri == iMax ) {
+        rh  = (short) ((k2 * (6 * chroma + gi - bi))/(6 * chroma));
+        if (rh >= k2) {
+           rh -= k2;
+        }
+        return rh;
+    }
+
+    if (gi  == iMax) {
+        return(short) ((k2 * (2 * chroma + bi - ri)) / (6 * chroma));
+    }
+
+    return (short) ((k2 * (4 * chroma + ri - gi)) / (6 * chroma));
+}
+
+uchar4 __attribute__((kernel)) selectiveAdjust(const uchar4 in, uint32_t x,
+    uint32_t y) {
+    float4 pixel = rsUnpackColor8888(in);
+
+    float4 wsum = pixel;
+    int hue = rgb2hue(in);
+
+    float t = satLut[hue];
+        pixel.xyz = rsMatrixMultiply(&colorMatrix_min ,pixel.xyz) * (1 - t) +
+            t * (rsMatrixMultiply(&colorMatrix_max ,pixel.xyz));
+
+    pixel.a = 1.0f;
+    return rsPackColorTo8888(clamp(pixel, 0.f, 1.0f));
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/filtershow/history/HistoryItem.java b/src/com/android/gallery3d/filtershow/history/HistoryItem.java
new file mode 100644
index 0000000..2baaac3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/history/HistoryItem.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.history;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class HistoryItem {
+    private static final String LOGTAG = "HistoryItem";
+    private ImagePreset mImagePreset;
+    private FilterRepresentation mFilterRepresentation;
+    private Bitmap mPreviewImage;
+
+    public HistoryItem(ImagePreset preset, FilterRepresentation representation) {
+        mImagePreset = new ImagePreset(preset);
+        if (representation != null) {
+            mFilterRepresentation = representation.copy();
+        }
+    }
+
+    public ImagePreset getImagePreset() {
+        return mImagePreset;
+    }
+
+    public FilterRepresentation getFilterRepresentation() {
+        return mFilterRepresentation;
+    }
+
+    public Bitmap getPreviewImage() {
+        return mPreviewImage;
+    }
+
+    public void setPreviewImage(Bitmap previewImage) {
+        mPreviewImage = previewImage;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/history/HistoryManager.java b/src/com/android/gallery3d/filtershow/history/HistoryManager.java
new file mode 100644
index 0000000..755e2ea
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/history/HistoryManager.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.history;
+
+import android.graphics.drawable.Drawable;
+import android.view.MenuItem;
+
+import java.util.Vector;
+
+public class HistoryManager {
+    private static final String LOGTAG = "HistoryManager";
+
+    private Vector<HistoryItem> mHistoryItems = new Vector<HistoryItem>();
+    private int mCurrentPresetPosition = 0;
+    private MenuItem mUndoMenuItem = null;
+    private MenuItem mRedoMenuItem = null;
+    private MenuItem mResetMenuItem = null;
+
+    public void setMenuItems(MenuItem undoItem, MenuItem redoItem, MenuItem resetItem) {
+        mUndoMenuItem = undoItem;
+        mRedoMenuItem = redoItem;
+        mResetMenuItem = resetItem;
+        updateMenuItems();
+    }
+
+    private int getCount() {
+        return mHistoryItems.size();
+    }
+
+    public HistoryItem getItem(int position) {
+        return mHistoryItems.elementAt(position);
+    }
+
+    private void clear() {
+        mHistoryItems.clear();
+    }
+
+    private void add(HistoryItem item) {
+        mHistoryItems.add(item);
+    }
+
+    private void notifyDataSetChanged() {
+        // TODO
+    }
+
+    public boolean canReset() {
+        if (getCount() <= 1) {
+            return false;
+        }
+        return true;
+    }
+
+    public boolean canUndo() {
+        if (mCurrentPresetPosition == getCount() - 1) {
+            return false;
+        }
+        return true;
+    }
+
+    public boolean canRedo() {
+        if (mCurrentPresetPosition == 0) {
+            return false;
+        }
+        return true;
+    }
+
+    public void updateMenuItems() {
+        if (mUndoMenuItem != null) {
+            setEnabled(mUndoMenuItem, canUndo());
+        }
+        if (mRedoMenuItem != null) {
+            setEnabled(mRedoMenuItem, canRedo());
+        }
+        if (mResetMenuItem != null) {
+            setEnabled(mResetMenuItem, canReset());
+        }
+    }
+
+    private void setEnabled(MenuItem item, boolean enabled) {
+        item.setEnabled(enabled);
+        Drawable drawable = item.getIcon();
+        if (drawable != null) {
+            drawable.setAlpha(enabled ? 255 : 80);
+        }
+    }
+
+    public void setCurrentPreset(int n) {
+        mCurrentPresetPosition = n;
+        updateMenuItems();
+        notifyDataSetChanged();
+    }
+
+    public void reset() {
+        if (getCount() == 0) {
+            return;
+        }
+        HistoryItem first = getItem(getCount() - 1);
+        clear();
+        addHistoryItem(first);
+        updateMenuItems();
+    }
+
+    public HistoryItem getLast() {
+        if (getCount() == 0) {
+            return null;
+        }
+        return getItem(0);
+    }
+
+    public HistoryItem getCurrent() {
+        return getItem(mCurrentPresetPosition);
+    }
+
+    public void addHistoryItem(HistoryItem preset) {
+        insert(preset, 0);
+        updateMenuItems();
+    }
+
+    private void insert(HistoryItem preset, int position) {
+        if (mCurrentPresetPosition != 0) {
+            // in this case, let's discount the presets before the current one
+            Vector<HistoryItem> oldItems = new Vector<HistoryItem>();
+            for (int i = mCurrentPresetPosition; i < getCount(); i++) {
+                oldItems.add(getItem(i));
+            }
+            clear();
+            for (int i = 0; i < oldItems.size(); i++) {
+                add(oldItems.elementAt(i));
+            }
+            mCurrentPresetPosition = position;
+            notifyDataSetChanged();
+        }
+        mHistoryItems.insertElementAt(preset, position);
+        mCurrentPresetPosition = position;
+        notifyDataSetChanged();
+    }
+
+    public int redo() {
+        mCurrentPresetPosition--;
+        if (mCurrentPresetPosition < 0) {
+            mCurrentPresetPosition = 0;
+        }
+        notifyDataSetChanged();
+        updateMenuItems();
+        return mCurrentPresetPosition;
+    }
+
+    public int undo() {
+        mCurrentPresetPosition++;
+        if (mCurrentPresetPosition >= getCount()) {
+            mCurrentPresetPosition = getCount() - 1;
+        }
+        notifyDataSetChanged();
+        updateMenuItems();
+        return mCurrentPresetPosition;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java
new file mode 100644
index 0000000..aaec728
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+public class ControlPoint implements Comparable {
+    public float x;
+    public float y;
+
+    public ControlPoint(float px, float py) {
+        x = px;
+        y = py;
+    }
+
+    public ControlPoint(ControlPoint point) {
+        x = point.x;
+        y = point.y;
+    }
+
+    public boolean sameValues(ControlPoint other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null) {
+            return false;
+        }
+
+        if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) {
+            return false;
+        }
+        if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) {
+            return false;
+        }
+        return true;
+    }
+
+    public ControlPoint copy() {
+        return new ControlPoint(x, y);
+    }
+
+    @Override
+    public int compareTo(Object another) {
+        ControlPoint p = (ControlPoint) another;
+        if (p.x < x) {
+            return 1;
+        } else if (p.x > x) {
+            return -1;
+        }
+        return 0;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java
new file mode 100644
index 0000000..8ceb375
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.RectF;
+import android.graphics.Shader;
+
+import com.android.gallery3d.R;
+
+public class EclipseControl {
+    private float mCenterX = Float.NaN;
+    private float mCenterY = 0;
+    private float mRadiusX = 200;
+    private float mRadiusY = 300;
+    private static int MIN_TOUCH_DIST = 80;// should be a resource & in dips
+
+    private float[] handlex = new float[9];
+    private float[] handley = new float[9];
+    private int mSliderColor;
+    private int mCenterDotSize = 40;
+    private float mDownX;
+    private float mDownY;
+    private float mDownCenterX;
+    private float mDownCenterY;
+    private float mDownRadiusX;
+    private float mDownRadiusY;
+    private Matrix mScrToImg;
+
+    private boolean mShowReshapeHandles = true;
+    public final static int HAN_CENTER = 0;
+    public final static int HAN_NORTH = 7;
+    public final static int HAN_NE = 8;
+    public final static int HAN_EAST = 1;
+    public final static int HAN_SE = 2;
+    public final static int HAN_SOUTH = 3;
+    public final static int HAN_SW = 4;
+    public final static int HAN_WEST = 5;
+    public final static int HAN_NW = 6;
+
+    public EclipseControl(Context context) {
+        mSliderColor = Color.WHITE;
+    }
+
+    public void setRadius(float x, float y) {
+        mRadiusX = x;
+        mRadiusY = y;
+    }
+
+    public void setCenter(float x, float y) {
+        mCenterX = x;
+        mCenterY = y;
+    }
+
+    public int getCloseHandle(float x, float y) {
+        float min = Float.MAX_VALUE;
+        int handle = -1;
+        for (int i = 0; i < handlex.length; i++) {
+            float dx = handlex[i] - x;
+            float dy = handley[i] - y;
+            float dist = dx * dx + dy * dy;
+            if (dist < min) {
+                min = dist;
+                handle = i;
+            }
+        }
+
+        if (min < MIN_TOUCH_DIST * MIN_TOUCH_DIST) {
+            return handle;
+        }
+        for (int i = 0; i < handlex.length; i++) {
+            float dx = handlex[i] - x;
+            float dy = handley[i] - y;
+            float dist = (float) Math.sqrt(dx * dx + dy * dy);
+        }
+
+        return -1;
+    }
+
+    public void setScrToImageMatrix(Matrix scrToImg) {
+        mScrToImg = scrToImg;
+    }
+
+    public void actionDown(float x, float y, Oval oval) {
+        float[] point = new float[] {
+                x, y };
+        mScrToImg.mapPoints(point);
+        mDownX = point[0];
+        mDownY = point[1];
+        mDownCenterX = oval.getCenterX();
+        mDownCenterY = oval.getCenterY();
+        mDownRadiusX = oval.getRadiusX();
+        mDownRadiusY = oval.getRadiusY();
+    }
+
+    public void actionMove(int handle, float x, float y, Oval oval) {
+        float[] point = new float[] {
+                x, y };
+        mScrToImg.mapPoints(point);
+        x = point[0];
+        y = point[1];
+
+        // Test if the matrix is swapping x and y
+        point[0] = 0;
+        point[1] = 1;
+        mScrToImg.mapVectors(point);
+        boolean swapxy = (point[0] > 0.0f);
+
+        int sign = 1;
+        switch (handle) {
+            case HAN_CENTER:
+                float ctrdx = mDownX - mDownCenterX;
+                float ctrdy = mDownY - mDownCenterY;
+                oval.setCenter(x - ctrdx, y - ctrdy);
+                // setRepresentation(mVignetteRep);
+                break;
+            case HAN_NORTH:
+                sign = -1;
+            case HAN_SOUTH:
+                if (swapxy) {
+                    float raddx = mDownRadiusY - Math.abs(mDownX - mDownCenterY);
+                    oval.setRadiusY(Math.abs(x - oval.getCenterY() + sign * raddx));
+                } else {
+                    float raddy = mDownRadiusY - Math.abs(mDownY - mDownCenterY);
+                    oval.setRadiusY(Math.abs(y - oval.getCenterY() + sign * raddy));
+                }
+                break;
+            case HAN_EAST:
+                sign = -1;
+            case HAN_WEST:
+                if (swapxy) {
+                    float raddy = mDownRadiusX - Math.abs(mDownY - mDownCenterX);
+                    oval.setRadiusX(Math.abs(y - oval.getCenterX() + sign * raddy));
+                } else {
+                    float raddx = mDownRadiusX - Math.abs(mDownX - mDownCenterX);
+                    oval.setRadiusX(Math.abs(x - oval.getCenterX() - sign * raddx));
+                }
+                break;
+            case HAN_SE:
+            case HAN_NE:
+            case HAN_SW:
+            case HAN_NW:
+                float sin45 = (float) Math.sin(45);
+                float dr = (mDownRadiusX + mDownRadiusY) * sin45;
+                float ctr_dx = mDownX - mDownCenterX;
+                float ctr_dy = mDownY - mDownCenterY;
+                float downRad = Math.abs(ctr_dx) + Math.abs(ctr_dy) - dr;
+                float rx = oval.getRadiusX();
+                float ry = oval.getRadiusY();
+                float r = (Math.abs(rx) + Math.abs(ry)) * sin45;
+                float dx = x - oval.getCenterX();
+                float dy = y - oval.getCenterY();
+                float nr = Math.abs(Math.abs(dx) + Math.abs(dy) - downRad);
+                oval.setRadius(rx * nr / r, ry * nr / r);
+
+                break;
+        }
+    }
+
+    public void paintGrayPoint(Canvas canvas, float x, float y) {
+        if (x == Float.NaN) {
+            return;
+        }
+
+        Paint paint = new Paint();
+
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(Color.BLUE);
+        int[] colors3 = new int[] {
+                Color.GRAY, Color.LTGRAY, 0x66000000, 0 };
+        RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] {
+                0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+        paint.setShader(g);
+        canvas.drawCircle(x, y, mCenterDotSize, paint);
+    }
+
+    public void paintPoint(Canvas canvas, float x, float y) {
+        if (x == Float.NaN) {
+            return;
+        }
+
+        Paint paint = new Paint();
+
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(Color.BLUE);
+        int[] colors3 = new int[] {
+                mSliderColor, mSliderColor, 0x66000000, 0 };
+        RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] {
+                0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+        paint.setShader(g);
+        canvas.drawCircle(x, y, mCenterDotSize, paint);
+    }
+
+    void paintRadius(Canvas canvas, float cx, float cy, float rx, float ry) {
+        if (cx == Float.NaN) {
+            return;
+        }
+        int mSliderColor = 0xFF33B5E5;
+        Paint paint = new Paint();
+        RectF rect = new RectF(cx - rx, cy - ry, cx + rx, cy + ry);
+        paint.setAntiAlias(true);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(6);
+        paint.setColor(Color.BLACK);
+        paintOvallines(canvas, rect, paint, cx, cy, rx, ry);
+
+        paint.setStrokeWidth(3);
+        paint.setColor(Color.WHITE);
+        paintOvallines(canvas, rect, paint, cx, cy, rx, ry);
+    }
+
+    public void paintOvallines(
+            Canvas canvas, RectF rect, Paint paint, float cx, float cy, float rx, float ry) {
+        canvas.drawOval(rect, paint);
+        float da = 4;
+        float arclen = da + da;
+        if (mShowReshapeHandles) {
+            paint.setStyle(Paint.Style.STROKE);
+
+            for (int i = 0; i < 361; i += 90) {
+                float dx = rx + 10;
+                float dy = ry + 10;
+                rect.left = cx - dx;
+                rect.top = cy - dy;
+                rect.right = cx + dx;
+                rect.bottom = cy + dy;
+                canvas.drawArc(rect, i - da, arclen, false, paint);
+                dx = rx - 10;
+                dy = ry - 10;
+                rect.left = cx - dx;
+                rect.top = cy - dy;
+                rect.right = cx + dx;
+                rect.bottom = cy + dy;
+                canvas.drawArc(rect, i - da, arclen, false, paint);
+            }
+        }
+        da *= 2;
+        paint.setStyle(Paint.Style.FILL);
+
+        for (int i = 45; i < 361; i += 90) {
+            double angle = Math.PI * i / 180.;
+            float x = cx + (float) (rx * Math.cos(angle));
+            float y = cy + (float) (ry * Math.sin(angle));
+            canvas.drawRect(x - da, y - da, x + da, y + da, paint);
+        }
+        paint.setStyle(Paint.Style.STROKE);
+        rect.left = cx - rx;
+        rect.top = cy - ry;
+        rect.right = cx + rx;
+        rect.bottom = cy + ry;
+    }
+
+    public void fillHandles(Canvas canvas, float cx, float cy, float rx, float ry) {
+        handlex[0] = cx;
+        handley[0] = cy;
+        int k = 1;
+
+        for (int i = 0; i < 360; i += 45) {
+            double angle = Math.PI * i / 180.;
+
+            float x = cx + (float) (rx * Math.cos(angle));
+            float y = cy + (float) (ry * Math.sin(angle));
+            handlex[k] = x;
+            handley[k] = y;
+
+            k++;
+        }
+    }
+
+    public void draw(Canvas canvas) {
+        paintRadius(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY);
+        fillHandles(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY);
+        paintPoint(canvas, mCenterX, mCenterY);
+    }
+
+    public boolean isUndefined() {
+        return Float.isNaN(mCenterX);
+    }
+
+    public void setShowReshapeHandles(boolean showReshapeHandles) {
+        this.mShowReshapeHandles = showReshapeHandles;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java
new file mode 100644
index 0000000..81394f1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation.Mirror;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation.Rotation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+public final class GeometryMathUtils {
+    private GeometryMathUtils() {};
+
+    // Holder class for Geometry data.
+    public static final class GeometryHolder {
+        public Rotation rotation = FilterRotateRepresentation.getNil();
+        public float straighten = FilterStraightenRepresentation.getNil();
+        public RectF crop = FilterCropRepresentation.getNil();
+        public Mirror mirror = FilterMirrorRepresentation.getNil();
+
+        public void set(GeometryHolder h) {
+            rotation = h.rotation;
+            straighten = h.straighten;
+            crop.set(h.crop);
+            mirror = h.mirror;
+        }
+
+        public void wipe() {
+            rotation = FilterRotateRepresentation.getNil();
+            straighten = FilterStraightenRepresentation.getNil();
+            crop = FilterCropRepresentation.getNil();
+            mirror = FilterMirrorRepresentation.getNil();
+        }
+
+        public boolean isNil() {
+            return rotation == FilterRotateRepresentation.getNil() &&
+                    straighten == FilterStraightenRepresentation.getNil() &&
+                    crop.equals(FilterCropRepresentation.getNil()) &&
+                    mirror == FilterMirrorRepresentation.getNil();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof GeometryHolder)) {
+                return false;
+            }
+            GeometryHolder h = (GeometryHolder) o;
+            return rotation == h.rotation && straighten == h.straighten &&
+                    ((crop == null && h.crop == null) || (crop != null && crop.equals(h.crop))) &&
+                    mirror == h.mirror;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[" + "rotation:" + rotation.value()
+                    + ",straighten:" + straighten + ",crop:" + crop.toString()
+                    + ",mirror:" + mirror.value() + "]";
+        }
+    }
+
+    // Math operations for 2d vectors
+    public static float clamp(float i, float low, float high) {
+        return Math.max(Math.min(i, high), low);
+    }
+
+    public static float[] lineIntersect(float[] line1, float[] line2) {
+        float a0 = line1[0];
+        float a1 = line1[1];
+        float b0 = line1[2];
+        float b1 = line1[3];
+        float c0 = line2[0];
+        float c1 = line2[1];
+        float d0 = line2[2];
+        float d1 = line2[3];
+        float t0 = a0 - b0;
+        float t1 = a1 - b1;
+        float t2 = b0 - d0;
+        float t3 = d1 - b1;
+        float t4 = c0 - d0;
+        float t5 = c1 - d1;
+
+        float denom = t1 * t4 - t0 * t5;
+        if (denom == 0)
+            return null;
+        float u = (t3 * t4 + t5 * t2) / denom;
+        float[] intersect = {
+                b0 + u * t0, b1 + u * t1
+        };
+        return intersect;
+    }
+
+    public static float[] shortestVectorFromPointToLine(float[] point, float[] line) {
+        float x1 = line[0];
+        float x2 = line[2];
+        float y1 = line[1];
+        float y2 = line[3];
+        float xdelt = x2 - x1;
+        float ydelt = y2 - y1;
+        if (xdelt == 0 && ydelt == 0)
+            return null;
+        float u = ((point[0] - x1) * xdelt + (point[1] - y1) * ydelt)
+                / (xdelt * xdelt + ydelt * ydelt);
+        float[] ret = {
+                (x1 + u * (x2 - x1)), (y1 + u * (y2 - y1))
+        };
+        float[] vec = {
+                ret[0] - point[0], ret[1] - point[1]
+        };
+        return vec;
+    }
+
+    // A . B
+    public static float dotProduct(float[] a, float[] b) {
+        return a[0] * b[0] + a[1] * b[1];
+    }
+
+    public static float[] normalize(float[] a) {
+        float length = (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]);
+        float[] b = {
+                a[0] / length, a[1] / length
+        };
+        return b;
+    }
+
+    // A onto B
+    public static float scalarProjection(float[] a, float[] b) {
+        float length = (float) Math.sqrt(b[0] * b[0] + b[1] * b[1]);
+        return dotProduct(a, b) / length;
+    }
+
+    public static float[] getVectorFromPoints(float[] point1, float[] point2) {
+        float[] p = {
+                point2[0] - point1[0], point2[1] - point1[1]
+        };
+        return p;
+    }
+
+    public static float[] getUnitVectorFromPoints(float[] point1, float[] point2) {
+        float[] p = {
+                point2[0] - point1[0], point2[1] - point1[1]
+        };
+        float length = (float) Math.sqrt(p[0] * p[0] + p[1] * p[1]);
+        p[0] = p[0] / length;
+        p[1] = p[1] / length;
+        return p;
+    }
+
+    public static void scaleRect(RectF r, float scale) {
+        r.set(r.left * scale, r.top * scale, r.right * scale, r.bottom * scale);
+    }
+
+    // A - B
+    public static float[] vectorSubtract(float[] a, float[] b) {
+        int len = a.length;
+        if (len != b.length)
+            return null;
+        float[] ret = new float[len];
+        for (int i = 0; i < len; i++) {
+            ret[i] = a[i] - b[i];
+        }
+        return ret;
+    }
+
+    public static float vectorLength(float[] a) {
+        return (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]);
+    }
+
+    public static float scale(float oldWidth, float oldHeight, float newWidth, float newHeight) {
+        if (oldHeight == 0 || oldWidth == 0 || (oldWidth == newWidth && oldHeight == newHeight)) {
+            return 1;
+        }
+        return Math.min(newWidth / oldWidth, newHeight / oldHeight);
+    }
+
+    public static Rect roundNearest(RectF r) {
+        Rect q = new Rect(Math.round(r.left), Math.round(r.top), Math.round(r.right),
+                Math.round(r.bottom));
+        return q;
+    }
+
+    private static void concatMirrorMatrix(Matrix m, Mirror type) {
+        if (type == Mirror.HORIZONTAL) {
+            m.postScale(-1, 1);
+        } else if (type == Mirror.VERTICAL) {
+            m.postScale(1, -1);
+        } else if (type == Mirror.BOTH) {
+            m.postScale(1, -1);
+            m.postScale(-1, 1);
+        }
+    }
+
+    private static int getRotationForOrientation(int orientation) {
+        switch (orientation) {
+            case ImageLoader.ORI_ROTATE_90:
+                return 90;
+            case ImageLoader.ORI_ROTATE_180:
+                return 180;
+            case ImageLoader.ORI_ROTATE_270:
+                return 270;
+            default:
+                return 0;
+        }
+    }
+
+    public static GeometryHolder unpackGeometry(Collection<FilterRepresentation> geometry) {
+        GeometryHolder holder = new GeometryHolder();
+        unpackGeometry(holder, geometry);
+        return holder;
+    }
+
+    public static void unpackGeometry(GeometryHolder out,
+            Collection<FilterRepresentation> geometry) {
+        out.wipe();
+        // Get geometry data from filters
+        for (FilterRepresentation r : geometry) {
+            if (r.isNil()) {
+                continue;
+            }
+            if (r.getSerializationName() == FilterRotateRepresentation.SERIALIZATION_NAME) {
+                out.rotation = ((FilterRotateRepresentation) r).getRotation();
+            } else if (r.getSerializationName() ==
+                    FilterStraightenRepresentation.SERIALIZATION_NAME) {
+                out.straighten = ((FilterStraightenRepresentation) r).getStraighten();
+            } else if (r.getSerializationName() == FilterCropRepresentation.SERIALIZATION_NAME) {
+                ((FilterCropRepresentation) r).getCrop(out.crop);
+            } else if (r.getSerializationName() == FilterMirrorRepresentation.SERIALIZATION_NAME) {
+                out.mirror = ((FilterMirrorRepresentation) r).getMirror();
+            }
+        }
+    }
+
+    public static void replaceInstances(Collection<FilterRepresentation> geometry,
+            FilterRepresentation rep) {
+        Iterator<FilterRepresentation> iter = geometry.iterator();
+        while (iter.hasNext()) {
+            FilterRepresentation r = iter.next();
+            if (ImagePreset.sameSerializationName(rep, r)) {
+                iter.remove();
+            }
+        }
+        if (!rep.isNil()) {
+            geometry.add(rep);
+        }
+    }
+
+    public static void initializeHolder(GeometryHolder outHolder,
+            FilterRepresentation currentLocal) {
+        Collection<FilterRepresentation> geometry = MasterImage.getImage().getPreset()
+                .getGeometryFilters();
+        replaceInstances(geometry, currentLocal);
+        unpackGeometry(outHolder, geometry);
+    }
+
+    private static Bitmap applyFullGeometryMatrix(Bitmap image, GeometryHolder holder) {
+        int width = image.getWidth();
+        int height = image.getHeight();
+        RectF crop = getTrueCropRect(holder, width, height);
+        Rect frame = new Rect();
+        crop.roundOut(frame);
+        Matrix m = getCropSelectionToScreenMatrix(null, holder, width, height, frame.width(),
+                frame.height());
+        Bitmap temp = Bitmap.createBitmap(frame.width(), frame.height(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(temp);
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setFilterBitmap(true);
+        paint.setDither(true);
+        canvas.drawBitmap(image, m, paint);
+        return temp;
+    }
+
+    public static Matrix getImageToScreenMatrix(Collection<FilterRepresentation> geometry,
+            boolean reflectRotation, Rect bmapDimens, float viewWidth, float viewHeight) {
+        GeometryHolder h = unpackGeometry(geometry);
+        return GeometryMathUtils.getOriginalToScreen(h, reflectRotation, bmapDimens.width(),
+                bmapDimens.height(), viewWidth, viewHeight);
+    }
+
+    public static Matrix getOriginalToScreen(GeometryHolder holder, boolean rotate,
+            float originalWidth,
+            float originalHeight, float viewWidth, float viewHeight) {
+        int orientation = MasterImage.getImage().getZoomOrientation();
+        int rotation = getRotationForOrientation(orientation);
+        Rotation prev = holder.rotation;
+        rotation = (rotation + prev.value()) % 360;
+        holder.rotation = Rotation.fromValue(rotation);
+        Matrix m = getCropSelectionToScreenMatrix(null, holder, (int) originalWidth,
+                (int) originalHeight, (int) viewWidth, (int) viewHeight);
+        holder.rotation = prev;
+        return m;
+    }
+
+    public static Bitmap applyGeometryRepresentations(Collection<FilterRepresentation> res,
+            Bitmap image) {
+        GeometryHolder holder = unpackGeometry(res);
+        Bitmap bmap = image;
+        // If there are geometry changes, apply them to the image
+        if (!holder.isNil()) {
+            bmap = applyFullGeometryMatrix(bmap, holder);
+        }
+        return bmap;
+    }
+
+    public static RectF drawTransformedCropped(GeometryHolder holder, Canvas canvas,
+            Bitmap photo, int viewWidth, int viewHeight) {
+        if (photo == null) {
+            return null;
+        }
+        RectF crop = new RectF();
+        Matrix m = getCropSelectionToScreenMatrix(crop, holder, photo.getWidth(), photo.getHeight(),
+                viewWidth, viewHeight);
+        canvas.save();
+        canvas.clipRect(crop);
+        Paint p = new Paint();
+        p.setAntiAlias(true);
+        canvas.drawBitmap(photo, m, p);
+        canvas.restore();
+        return crop;
+    }
+
+    public static boolean needsDimensionSwap(Rotation rotation) {
+        switch (rotation) {
+            case NINETY:
+            case TWO_SEVENTY:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    // Gives matrix for rotated, straightened, mirrored bitmap centered at 0,0.
+    private static Matrix getFullGeometryMatrix(GeometryHolder holder, int bitmapWidth,
+            int bitmapHeight) {
+        float centerX = bitmapWidth / 2f;
+        float centerY = bitmapHeight / 2f;
+        Matrix m = new Matrix();
+        m.setTranslate(-centerX, -centerY);
+        m.postRotate(holder.straighten + holder.rotation.value());
+        concatMirrorMatrix(m, holder.mirror);
+        return m;
+    }
+
+    public static Matrix getFullGeometryToScreenMatrix(GeometryHolder holder, int bitmapWidth,
+            int bitmapHeight, int viewWidth, int viewHeight) {
+        float scale = GeometryMathUtils.scale(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
+        Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight);
+        m.postScale(scale, scale);
+        m.postTranslate(viewWidth / 2f, viewHeight / 2f);
+        return m;
+    }
+
+    public static RectF getTrueCropRect(GeometryHolder holder, int bitmapWidth, int bitmapHeight) {
+        RectF r = new RectF(holder.crop);
+        FilterCropRepresentation.findScaledCrop(r, bitmapWidth, bitmapHeight);
+        float s = holder.straighten;
+        holder.straighten = 0;
+        Matrix m1 = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight);
+        holder.straighten = s;
+        m1.mapRect(r);
+        return r;
+    }
+
+    public static Matrix getCropSelectionToScreenMatrix(RectF outCrop, GeometryHolder holder,
+            int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) {
+        Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight);
+        RectF crop = getTrueCropRect(holder, bitmapWidth, bitmapHeight);
+        float scale = GeometryMathUtils.scale(crop.width(), crop.height(), viewWidth, viewHeight);
+        m.postScale(scale, scale);
+        GeometryMathUtils.scaleRect(crop, scale);
+        m.postTranslate(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY());
+        if (outCrop != null) {
+            crop.offset(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY());
+            outCrop.set(crop);
+        }
+        return m;
+    }
+
+    public static Matrix getCropSelectionToScreenMatrix(RectF outCrop,
+            Collection<FilterRepresentation> res, int bitmapWidth, int bitmapHeight, int viewWidth,
+            int viewHeight) {
+        GeometryHolder holder = unpackGeometry(res);
+        return getCropSelectionToScreenMatrix(outCrop, holder, bitmapWidth, bitmapHeight,
+                viewWidth, viewHeight);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GradControl.java b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java
new file mode 100644
index 0000000..964da99
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+
+import com.android.gallery3d.R;
+
+public class GradControl {
+    private float mPoint1X = Float.NaN; // used to flag parameters have not been set
+    private float mPoint1Y = 0;
+    private float mPoint2X = 200;
+    private float mPoint2Y = 300;
+    private int mMinTouchDist = 80;// should be a resource & in dips
+
+    private float[] handlex = new float[3];
+    private float[] handley = new float[3];
+    private int mSliderColor;
+    private int mCenterDotSize;
+    private float mDownX;
+    private float mDownY;
+    private float mDownPoint1X;
+    private float mDownPoint1Y;
+    private float mDownPoint2X;
+    private float mDownPoint2Y;
+    Rect mImageBounds;
+    int mImageHeight;
+    private Matrix mScrToImg;
+    Paint mPaint = new Paint();
+    DashPathEffect mDash = new DashPathEffect(new float[]{30, 30}, 0);
+    private boolean mShowReshapeHandles = true;
+    public final static int HAN_CENTER = 0;
+    public final static int HAN_NORTH = 2;
+    public final static int HAN_SOUTH = 1;
+    private int[] mPointColorPatern;
+    private int[] mGrayPointColorPatern;
+    private float[] mPointRadialPos = new float[]{0, .3f, .31f, 1};
+    private int mLineColor;
+    private int mlineShadowColor;
+
+    public GradControl(Context context) {
+
+        Resources res = context.getResources();
+        mCenterDotSize = (int) res.getDimension(R.dimen.gradcontrol_dot_size);
+        mMinTouchDist = (int) res.getDimension(R.dimen.gradcontrol_min_touch_dist);
+        int grayPointCenterColor = res.getColor(R.color.gradcontrol_graypoint_center);
+        int grayPointEdgeColor = res.getColor(R.color.gradcontrol_graypoint_edge);
+        int pointCenterColor = res.getColor(R.color.gradcontrol_point_center);
+        int pointEdgeColor = res.getColor(R.color.gradcontrol_point_edge);
+        int pointShadowStartColor = res.getColor(R.color.gradcontrol_point_shadow_start);
+        int pointShadowEndColor = res.getColor(R.color.gradcontrol_point_shadow_end);
+        mPointColorPatern = new int[]{
+                pointCenterColor, pointEdgeColor, pointShadowStartColor, pointShadowEndColor};
+        mGrayPointColorPatern = new int[]{
+                grayPointCenterColor, grayPointEdgeColor, pointShadowStartColor, pointShadowEndColor};
+        mSliderColor = Color.WHITE;
+        mLineColor = res.getColor(R.color.gradcontrol_line_color);
+        mlineShadowColor = res.getColor(R.color.gradcontrol_line_shadow);
+    }
+
+    public void setPoint2(float x, float y) {
+        mPoint2X = x;
+        mPoint2Y = y;
+    }
+
+    public void setPoint1(float x, float y) {
+        mPoint1X = x;
+        mPoint1Y = y;
+    }
+
+    public int getCloseHandle(float x, float y) {
+        float min = Float.MAX_VALUE;
+        int handle = -1;
+        for (int i = 0; i < handlex.length; i++) {
+            float dx = handlex[i] - x;
+            float dy = handley[i] - y;
+            float dist = dx * dx + dy * dy;
+            if (dist < min) {
+                min = dist;
+                handle = i;
+            }
+        }
+
+        if (min < mMinTouchDist * mMinTouchDist) {
+            return handle;
+        }
+        for (int i = 0; i < handlex.length; i++) {
+            float dx = handlex[i] - x;
+            float dy = handley[i] - y;
+            float dist = (float) Math.sqrt(dx * dx + dy * dy);
+        }
+
+        return -1;
+    }
+
+    public void setScrImageInfo(Matrix scrToImg, Rect imageBounds) {
+        mScrToImg = scrToImg;
+        mImageBounds = new Rect(imageBounds);
+    }
+
+    private boolean centerIsOutside(float x1, float y1, float x2, float y2) {
+        return (!mImageBounds.contains((int) ((x1 + x2) / 2), (int) ((y1 + y2) / 2)));
+    }
+
+    public void actionDown(float x, float y, Line line) {
+        float[] point = new float[]{
+                x, y};
+        mScrToImg.mapPoints(point);
+        mDownX = point[0];
+        mDownY = point[1];
+        mDownPoint1X = line.getPoint1X();
+        mDownPoint1Y = line.getPoint1Y();
+        mDownPoint2X = line.getPoint2X();
+        mDownPoint2Y = line.getPoint2Y();
+    }
+
+    public void actionMove(int handle, float x, float y, Line line) {
+        float[] point = new float[]{
+                x, y};
+        mScrToImg.mapPoints(point);
+        x = point[0];
+        y = point[1];
+
+        // Test if the matrix is swapping x and y
+        point[0] = 0;
+        point[1] = 1;
+        mScrToImg.mapVectors(point);
+        boolean swapxy = (point[0] > 0.0f);
+
+        int sign = 1;
+
+        float dx = x - mDownX;
+        float dy = y - mDownY;
+        switch (handle) {
+            case HAN_CENTER:
+                if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy,
+                        mDownPoint2X + dx, mDownPoint2Y + dy)) {
+                    break;
+                }
+                line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy);
+                line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy);
+                break;
+            case HAN_SOUTH:
+                if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy,
+                        mDownPoint2X, mDownPoint2Y)) {
+                    break;
+                }
+                line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy);
+                break;
+            case HAN_NORTH:
+                if (centerIsOutside(mDownPoint1X, mDownPoint1Y,
+                        mDownPoint2X + dx, mDownPoint2Y + dy)) {
+                    break;
+                }
+                line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy);
+                break;
+        }
+    }
+
+    public void paintGrayPoint(Canvas canvas, float x, float y) {
+        if (isUndefined()) {
+            return;
+        }
+
+        Paint paint = new Paint();
+        paint.setStyle(Paint.Style.FILL);
+        RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mGrayPointColorPatern,
+                mPointRadialPos, Shader.TileMode.CLAMP);
+        paint.setShader(g);
+        canvas.drawCircle(x, y, mCenterDotSize, paint);
+    }
+
+    public void paintPoint(Canvas canvas, float x, float y) {
+        if (isUndefined()) {
+            return;
+        }
+
+        Paint paint = new Paint();
+        paint.setStyle(Paint.Style.FILL);
+        RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mPointColorPatern,
+                mPointRadialPos, Shader.TileMode.CLAMP);
+        paint.setShader(g);
+        canvas.drawCircle(x, y, mCenterDotSize, paint);
+    }
+
+    void paintLines(Canvas canvas, float p1x, float p1y, float p2x, float p2y) {
+        if (isUndefined()) {
+            return;
+        }
+
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+
+        mPaint.setStrokeWidth(6);
+        mPaint.setColor(mlineShadowColor);
+        mPaint.setPathEffect(mDash);
+        paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y);
+
+        mPaint.setStrokeWidth(3);
+        mPaint.setColor(mLineColor);
+        mPaint.setPathEffect(mDash);
+        paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y);
+    }
+
+    public void paintOvallines(
+            Canvas canvas, Paint paint, float p1x, float p1y, float p2x, float p2y) {
+
+
+
+        canvas.drawLine(p1x, p1y, p2x, p2y, paint);
+
+        float cx = (p1x + p2x) / 2;
+        float cy = (p1y + p2y) / 2;
+        float dx = p1x - p2x;
+        float dy = p1y - p2y;
+        float len = (float) Math.sqrt(dx * dx + dy * dy);
+        dx *= 2048 / len;
+        dy *= 2048 / len;
+
+        canvas.drawLine(p1x + dy, p1y - dx, p1x - dy, p1y + dx, paint);
+        canvas.drawLine(p2x + dy, p2y - dx, p2x - dy, p2y + dx, paint);
+    }
+
+    public void fillHandles(Canvas canvas, float p1x, float p1y, float p2x, float p2y) {
+        float cx = (p1x + p2x) / 2;
+        float cy = (p1y + p2y) / 2;
+        handlex[0] = cx;
+        handley[0] = cy;
+        handlex[1] = p1x;
+        handley[1] = p1y;
+        handlex[2] = p2x;
+        handley[2] = p2y;
+
+    }
+
+    public void draw(Canvas canvas) {
+        paintLines(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y);
+        fillHandles(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y);
+        paintPoint(canvas, mPoint2X, mPoint2Y);
+        paintPoint(canvas, mPoint1X, mPoint1Y);
+        paintPoint(canvas, (mPoint1X + mPoint2X) / 2, (mPoint1Y + mPoint2Y) / 2);
+    }
+
+    public boolean isUndefined() {
+        return Float.isNaN(mPoint1X);
+    }
+
+    public void setShowReshapeHandles(boolean showReshapeHandles) {
+        this.mShowReshapeHandles = showReshapeHandles;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
new file mode 100644
index 0000000..7fee031
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.crop.CropDrawingUtils;
+import com.android.gallery3d.filtershow.crop.CropMath;
+import com.android.gallery3d.filtershow.crop.CropObject;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+public class ImageCrop extends ImageShow {
+    private static final String TAG = ImageCrop.class.getSimpleName();
+    private RectF mImageBounds = new RectF();
+    private RectF mScreenCropBounds = new RectF();
+    private Paint mPaint = new Paint();
+    private CropObject mCropObj = null;
+    private GeometryHolder mGeometry = new GeometryHolder();
+    private GeometryHolder mUpdateHolder = new GeometryHolder();
+    private Drawable mCropIndicator;
+    private int mIndicatorSize;
+    private boolean mMovingBlock = false;
+    private Matrix mDisplayMatrix = null;
+    private Matrix mDisplayCropMatrix = null;
+    private Matrix mDisplayMatrixInverse = null;
+    private float mPrevX = 0;
+    private float mPrevY = 0;
+    private int mMinSideSize = 90;
+    private int mTouchTolerance = 40;
+    private enum Mode {
+        NONE, MOVE
+    }
+    private Mode mState = Mode.NONE;
+    private boolean mValidDraw = false;
+    FilterCropRepresentation mLocalRep = new FilterCropRepresentation();
+    EditorCrop mEditorCrop;
+
+    public ImageCrop(Context context) {
+        super(context);
+        setup(context);
+    }
+
+    public ImageCrop(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setup(context);
+    }
+
+    public ImageCrop(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        setup(context);
+    }
+
+    private void setup(Context context) {
+        Resources rsc = context.getResources();
+        mCropIndicator = rsc.getDrawable(R.drawable.camera_crop);
+        mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size);
+        mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side);
+        mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance);
+    }
+
+    public void setFilterCropRepresentation(FilterCropRepresentation crop) {
+        mLocalRep = (crop == null) ? new FilterCropRepresentation() : crop;
+        GeometryMathUtils.initializeHolder(mUpdateHolder, mLocalRep);
+        mValidDraw = true;
+    }
+
+    public FilterCropRepresentation getFinalRepresentation() {
+        return mLocalRep;
+    }
+
+    private void internallyUpdateLocalRep(RectF crop, RectF image) {
+        FilterCropRepresentation
+                .findNormalizedCrop(crop, (int) image.width(), (int) image.height());
+        mGeometry.crop.set(crop);
+        mUpdateHolder.set(mGeometry);
+        mLocalRep.setCrop(crop);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+            return true;
+        }
+        float[] touchPoint = {
+                x, y
+        };
+        mDisplayMatrixInverse.mapPoints(touchPoint);
+        x = touchPoint[0];
+        y = touchPoint[1];
+        switch (event.getActionMasked()) {
+            case (MotionEvent.ACTION_DOWN):
+                if (mState == Mode.NONE) {
+                    if (!mCropObj.selectEdge(x, y)) {
+                        mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK);
+                    }
+                    mPrevX = x;
+                    mPrevY = y;
+                    mState = Mode.MOVE;
+                }
+                break;
+            case (MotionEvent.ACTION_UP):
+                if (mState == Mode.MOVE) {
+                    mCropObj.selectEdge(CropObject.MOVE_NONE);
+                    mMovingBlock = false;
+                    mPrevX = x;
+                    mPrevY = y;
+                    mState = Mode.NONE;
+                    internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds());
+                }
+                break;
+            case (MotionEvent.ACTION_MOVE):
+                if (mState == Mode.MOVE) {
+                    float dx = x - mPrevX;
+                    float dy = y - mPrevY;
+                    mCropObj.moveCurrentSelection(dx, dy);
+                    mPrevX = x;
+                    mPrevY = y;
+                }
+                break;
+            default:
+                break;
+        }
+        invalidate();
+        return true;
+    }
+
+    private void clearDisplay() {
+        mDisplayMatrix = null;
+        mDisplayMatrixInverse = null;
+        invalidate();
+    }
+
+    public void applyFreeAspect() {
+        mCropObj.unsetAspectRatio();
+        invalidate();
+    }
+
+    public void applyOriginalAspect() {
+        RectF outer = mCropObj.getOuterBounds();
+        float w = outer.width();
+        float h = outer.height();
+        if (w > 0 && h > 0) {
+            applyAspect(w, h);
+            mCropObj.resetBoundsTo(outer, outer);
+            internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds());
+        } else {
+            Log.w(TAG, "failed to set aspect ratio original");
+        }
+        invalidate();
+    }
+
+    public void applyAspect(float x, float y) {
+        if (x <= 0 || y <= 0) {
+            throw new IllegalArgumentException("Bad arguments to applyAspect");
+        }
+        // If we are rotated by 90 degrees from horizontal, swap x and y
+        if (GeometryMathUtils.needsDimensionSwap(mGeometry.rotation)) {
+            float tmp = x;
+            x = y;
+            y = tmp;
+        }
+        if (!mCropObj.setInnerAspectRatio(x, y)) {
+            Log.w(TAG, "failed to set aspect ratio");
+        }
+        internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds());
+        invalidate();
+    }
+
+    /**
+     * Rotates first d bits in integer x to the left some number of times.
+     */
+    private int bitCycleLeft(int x, int times, int d) {
+        int mask = (1 << d) - 1;
+        int mout = x & mask;
+        times %= d;
+        int hi = mout >> (d - times);
+        int low = (mout << times) & mask;
+        int ret = x & ~mask;
+        ret |= low;
+        ret |= hi;
+        return ret;
+    }
+
+    /**
+     * Find the selected edge or corner in screen coordinates.
+     */
+    private int decode(int movingEdges, float rotation) {
+        int rot = CropMath.constrainedRotation(rotation);
+        switch (rot) {
+            case 90:
+                return bitCycleLeft(movingEdges, 1, 4);
+            case 180:
+                return bitCycleLeft(movingEdges, 2, 4);
+            case 270:
+                return bitCycleLeft(movingEdges, 3, 4);
+            default:
+                return movingEdges;
+        }
+    }
+
+    private void forceStateConsistency() {
+        MasterImage master = MasterImage.getImage();
+        Bitmap image = master.getFiltersOnlyImage();
+        int width = image.getWidth();
+        int height = image.getHeight();
+        if (mCropObj == null || !mUpdateHolder.equals(mGeometry)
+                || mImageBounds.width() != width || mImageBounds.height() != height
+                || !mLocalRep.getCrop().equals(mUpdateHolder.crop)) {
+            mImageBounds.set(0, 0, width, height);
+            mGeometry.set(mUpdateHolder);
+            mLocalRep.setCrop(mUpdateHolder.crop);
+            RectF scaledCrop = new RectF(mUpdateHolder.crop);
+            FilterCropRepresentation.findScaledCrop(scaledCrop, width, height);
+            mCropObj = new CropObject(mImageBounds, scaledCrop, (int) mUpdateHolder.straighten);
+            mState = Mode.NONE;
+            clearDisplay();
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        clearDisplay();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        Bitmap bitmap = MasterImage.getImage().getFiltersOnlyImage();
+        if (!mValidDraw || bitmap == null) {
+            return;
+        }
+        forceStateConsistency();
+        mImageBounds.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
+        // If display matrix doesn't exist, create it and its dependencies
+        if (mDisplayCropMatrix == null || mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+            mDisplayMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry,
+                    bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight());
+            float straighten = mGeometry.straighten;
+            mGeometry.straighten = 0;
+            mDisplayCropMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry,
+                    bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight());
+            mGeometry.straighten = straighten;
+            mDisplayMatrixInverse = new Matrix();
+            mDisplayMatrixInverse.reset();
+            if (!mDisplayCropMatrix.invert(mDisplayMatrixInverse)) {
+                Log.w(TAG, "could not invert display matrix");
+                mDisplayMatrixInverse = null;
+                return;
+            }
+            // Scale min side and tolerance by display matrix scale factor
+            mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
+            mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
+        }
+        // Draw actual bitmap
+        mPaint.reset();
+        mPaint.setAntiAlias(true);
+        mPaint.setFilterBitmap(true);
+        canvas.drawBitmap(bitmap, mDisplayMatrix, mPaint);
+        mCropObj.getInnerBounds(mScreenCropBounds);
+        RectF outer = mCropObj.getOuterBounds();
+        FilterCropRepresentation.findNormalizedCrop(mScreenCropBounds, (int) outer.width(),
+                (int) outer.height());
+        FilterCropRepresentation.findScaledCrop(mScreenCropBounds, bitmap.getWidth(),
+                bitmap.getHeight());
+        if (mDisplayCropMatrix.mapRect(mScreenCropBounds)) {
+            // Draw crop rect and markers
+            CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds);
+            CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds);
+            CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize,
+                    mScreenCropBounds, mCropObj.isFixedAspect(),
+                    decode(mCropObj.getSelectState(), mGeometry.rotation.value()));
+        }
+    }
+
+    public void setEditor(EditorCrop editorCrop) {
+        mEditorCrop = editorCrop;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java
new file mode 100644
index 0000000..82c4b2f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.os.AsyncTask;
+import android.util.AttributeSet;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.editors.EditorCurves;
+import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilterCurves;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.HashMap;
+
+public class ImageCurves extends ImageShow {
+
+    private static final String LOGTAG = "ImageCurves";
+    Paint gPaint = new Paint();
+    Path gPathSpline = new Path();
+    HashMap<Integer, String> mIdStrLut;
+
+    private int mCurrentCurveIndex = Spline.RGB;
+    private boolean mDidAddPoint = false;
+    private boolean mDidDelete = false;
+    private ControlPoint mCurrentControlPoint = null;
+    private int mCurrentPick = -1;
+    private ImagePreset mLastPreset = null;
+    int[] redHistogram = new int[256];
+    int[] greenHistogram = new int[256];
+    int[] blueHistogram = new int[256];
+    Path gHistoPath = new Path();
+
+    boolean mDoingTouchMove = false;
+    private EditorCurves mEditorCurves;
+    private FilterCurvesRepresentation mFilterCurvesRepresentation;
+
+    public ImageCurves(Context context) {
+        super(context);
+        setLayerType(LAYER_TYPE_SOFTWARE, gPaint);
+        resetCurve();
+    }
+
+    public ImageCurves(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setLayerType(LAYER_TYPE_SOFTWARE, gPaint);
+        resetCurve();
+    }
+
+    @Override
+    protected boolean enableComparison() {
+        return false;
+    }
+
+    @Override
+    public boolean useUtilityPanel() {
+        return true;
+    }
+
+    private void showPopupMenu(LinearLayout accessoryViewList) {
+        final Button button = (Button) accessoryViewList.findViewById(
+                R.id.applyEffect);
+        if (button == null) {
+            return;
+        }
+        if (mIdStrLut == null){
+            mIdStrLut = new HashMap<Integer, String>();
+            mIdStrLut.put(R.id.curve_menu_rgb,
+                    getContext().getString(R.string.curves_channel_rgb));
+            mIdStrLut.put(R.id.curve_menu_red,
+                    getContext().getString(R.string.curves_channel_red));
+            mIdStrLut.put(R.id.curve_menu_green,
+                    getContext().getString(R.string.curves_channel_green));
+            mIdStrLut.put(R.id.curve_menu_blue,
+                    getContext().getString(R.string.curves_channel_blue));
+        }
+        PopupMenu popupMenu = new PopupMenu(getActivity(), button);
+        popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_curves, popupMenu.getMenu());
+        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                setChannel(item.getItemId());
+                button.setText(mIdStrLut.get(item.getItemId()));
+                return true;
+            }
+        });
+        Editor.hackFixStrings(popupMenu.getMenu());
+        popupMenu.show();
+    }
+
+    @Override
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+        Context context = accessoryViewList.getContext();
+        Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+        view.setText(context.getString(R.string.curves_channel_rgb));
+        view.setVisibility(View.VISIBLE);
+
+        view.setOnClickListener(new OnClickListener() {
+                @Override
+            public void onClick(View arg0) {
+                showPopupMenu(accessoryViewList);
+            }
+        });
+
+        if (view != null) {
+            view.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void nextChannel() {
+        mCurrentCurveIndex = ((mCurrentCurveIndex + 1) % 4);
+        invalidate();
+    }
+
+    private ImageFilterCurves curves() {
+        String filterName = getFilterName();
+        ImagePreset p = getImagePreset();
+        if (p != null) {
+            return (ImageFilterCurves) FiltersManager.getManager().getFilter(ImageFilterCurves.class);
+        }
+        return null;
+    }
+
+    private Spline getSpline(int index) {
+        return mFilterCurvesRepresentation.getSpline(index);
+    }
+
+    @Override
+    public void resetParameter() {
+        super.resetParameter();
+        resetCurve();
+        mLastPreset = null;
+        invalidate();
+    }
+
+    public void resetCurve() {
+        if (mFilterCurvesRepresentation != null) {
+            mFilterCurvesRepresentation.reset();
+            updateCachedImage();
+        }
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mFilterCurvesRepresentation == null) {
+            return;
+        }
+
+        gPaint.setAntiAlias(true);
+
+        if (getImagePreset() != mLastPreset && getFilteredImage() != null) {
+            new ComputeHistogramTask().execute(getFilteredImage());
+            mLastPreset = getImagePreset();
+        }
+
+        if (curves() == null) {
+            return;
+        }
+
+        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.RED) {
+            drawHistogram(canvas, redHistogram, Color.RED, PorterDuff.Mode.SCREEN);
+        }
+        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.GREEN) {
+            drawHistogram(canvas, greenHistogram, Color.GREEN, PorterDuff.Mode.SCREEN);
+        }
+        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.BLUE) {
+            drawHistogram(canvas, blueHistogram, Color.BLUE, PorterDuff.Mode.SCREEN);
+        }
+        // We only display the other channels curves when showing the RGB curve
+        if (mCurrentCurveIndex == Spline.RGB) {
+            for (int i = 0; i < 4; i++) {
+                Spline spline = getSpline(i);
+                if (i != mCurrentCurveIndex && !spline.isOriginal()) {
+                    // And we only display a curve if it has more than two
+                    // points
+                    spline.draw(canvas, Spline.colorForCurve(i), getWidth(),
+                            getHeight(), false, mDoingTouchMove);
+                }
+            }
+        }
+        // ...but we always display the current curve.
+        getSpline(mCurrentCurveIndex)
+                .draw(canvas, Spline.colorForCurve(mCurrentCurveIndex), getWidth(), getHeight(),
+                        true, mDoingTouchMove);
+
+    }
+
+    private int pickControlPoint(float x, float y) {
+        int pick = 0;
+        Spline spline = getSpline(mCurrentCurveIndex);
+        float px = spline.getPoint(0).x;
+        float py = spline.getPoint(0).y;
+        double delta = Math.sqrt((px - x) * (px - x) + (py - y) * (py - y));
+        for (int i = 1; i < spline.getNbPoints(); i++) {
+            px = spline.getPoint(i).x;
+            py = spline.getPoint(i).y;
+            double currentDelta = Math.sqrt((px - x) * (px - x) + (py - y)
+                    * (py - y));
+            if (currentDelta < delta) {
+                delta = currentDelta;
+                pick = i;
+            }
+        }
+
+        if (!mDidAddPoint && (delta * getWidth() > 100)
+                && (spline.getNbPoints() < 10)) {
+            return -1;
+        }
+
+        return pick;
+    }
+
+    private String getFilterName() {
+        return "Curves";
+    }
+
+    @Override
+    public synchronized boolean onTouchEvent(MotionEvent e) {
+        if (e.getPointerCount() != 1) {
+            return true;
+        }
+
+        if (didFinishScalingOperation()) {
+            return true;
+        }
+
+        float margin = Spline.curveHandleSize() / 2;
+        float posX = e.getX();
+        if (posX < margin) {
+            posX = margin;
+        }
+        float posY = e.getY();
+        if (posY < margin) {
+            posY = margin;
+        }
+        if (posX > getWidth() - margin) {
+            posX = getWidth() - margin;
+        }
+        if (posY > getHeight() - margin) {
+            posY = getHeight() - margin;
+        }
+        posX = (posX - margin) / (getWidth() - 2 * margin);
+        posY = (posY - margin) / (getHeight() - 2 * margin);
+
+        if (e.getActionMasked() == MotionEvent.ACTION_UP) {
+            mCurrentControlPoint = null;
+            mCurrentPick = -1;
+            updateCachedImage();
+            mDidAddPoint = false;
+            if (mDidDelete) {
+                mDidDelete = false;
+            }
+            mDoingTouchMove = false;
+            return true;
+        }
+
+        if (mDidDelete) {
+            return true;
+        }
+
+        if (curves() == null) {
+            return true;
+        }
+
+        if (e.getActionMasked() == MotionEvent.ACTION_MOVE) {
+            mDoingTouchMove = true;
+            Spline spline = getSpline(mCurrentCurveIndex);
+            int pick = mCurrentPick;
+            if (mCurrentControlPoint == null) {
+                pick = pickControlPoint(posX, posY);
+                if (pick == -1) {
+                    mCurrentControlPoint = new ControlPoint(posX, posY);
+                    pick = spline.addPoint(mCurrentControlPoint);
+                    mDidAddPoint = true;
+                } else {
+                    mCurrentControlPoint = spline.getPoint(pick);
+                }
+                mCurrentPick = pick;
+            }
+
+            if (spline.isPointContained(posX, pick)) {
+                spline.movePoint(pick, posX, posY);
+            } else if (pick != -1 && spline.getNbPoints() > 2) {
+                spline.deletePoint(pick);
+                mDidDelete = true;
+            }
+            updateCachedImage();
+            invalidate();
+        }
+        return true;
+    }
+
+    public synchronized void updateCachedImage() {
+        if (getImagePreset() != null) {
+            resetImageCaches(this);
+            if (mEditorCurves != null) {
+                mEditorCurves.commitLocalRepresentation();
+            }
+            invalidate();
+        }
+    }
+
+    class ComputeHistogramTask extends AsyncTask<Bitmap, Void, int[]> {
+        @Override
+        protected int[] doInBackground(Bitmap... params) {
+            int[] histo = new int[256 * 3];
+            Bitmap bitmap = params[0];
+            int w = bitmap.getWidth();
+            int h = bitmap.getHeight();
+            int[] pixels = new int[w * h];
+            bitmap.getPixels(pixels, 0, w, 0, 0, w, h);
+            for (int i = 0; i < w; i++) {
+                for (int j = 0; j < h; j++) {
+                    int index = j * w + i;
+                    int r = Color.red(pixels[index]);
+                    int g = Color.green(pixels[index]);
+                    int b = Color.blue(pixels[index]);
+                    histo[r]++;
+                    histo[256 + g]++;
+                    histo[512 + b]++;
+                }
+            }
+            return histo;
+        }
+
+        @Override
+        protected void onPostExecute(int[] result) {
+            System.arraycopy(result, 0, redHistogram, 0, 256);
+            System.arraycopy(result, 256, greenHistogram, 0, 256);
+            System.arraycopy(result, 512, blueHistogram, 0, 256);
+            invalidate();
+        }
+    }
+
+    private void drawHistogram(Canvas canvas, int[] histogram, int color, PorterDuff.Mode mode) {
+        int max = 0;
+        for (int i = 0; i < histogram.length; i++) {
+            if (histogram[i] > max) {
+                max = histogram[i];
+            }
+        }
+        float w = getWidth() - Spline.curveHandleSize();
+        float h = getHeight() - Spline.curveHandleSize() / 2.0f;
+        float dx = Spline.curveHandleSize() / 2.0f;
+        float wl = w / histogram.length;
+        float wh = (0.3f * h) / max;
+        Paint paint = new Paint();
+        paint.setARGB(100, 255, 255, 255);
+        paint.setStrokeWidth((int) Math.ceil(wl));
+
+        Paint paint2 = new Paint();
+        paint2.setColor(color);
+        paint2.setStrokeWidth(6);
+        paint2.setXfermode(new PorterDuffXfermode(mode));
+        gHistoPath.reset();
+        gHistoPath.moveTo(dx, h);
+        boolean firstPointEncountered = false;
+        float prev = 0;
+        float last = 0;
+        for (int i = 0; i < histogram.length; i++) {
+            float x = i * wl + dx;
+            float l = histogram[i] * wh;
+            if (l != 0) {
+                float v = h - (l + prev) / 2.0f;
+                if (!firstPointEncountered) {
+                    gHistoPath.lineTo(x, h);
+                    firstPointEncountered = true;
+                }
+                gHistoPath.lineTo(x, v);
+                prev = l;
+                last = x;
+            }
+        }
+        gHistoPath.lineTo(last, h);
+        gHistoPath.lineTo(w, h);
+        gHistoPath.close();
+        canvas.drawPath(gHistoPath, paint2);
+        paint2.setStrokeWidth(2);
+        paint2.setStyle(Paint.Style.STROKE);
+        paint2.setARGB(255, 200, 200, 200);
+        canvas.drawPath(gHistoPath, paint2);
+    }
+
+    public void setChannel(int itemId) {
+        switch (itemId) {
+            case R.id.curve_menu_rgb: {
+                mCurrentCurveIndex = Spline.RGB;
+                break;
+            }
+            case R.id.curve_menu_red: {
+                mCurrentCurveIndex = Spline.RED;
+                break;
+            }
+            case R.id.curve_menu_green: {
+                mCurrentCurveIndex = Spline.GREEN;
+                break;
+            }
+            case R.id.curve_menu_blue: {
+                mCurrentCurveIndex = Spline.BLUE;
+                break;
+            }
+        }
+        mEditorCurves.commitLocalRepresentation();
+        invalidate();
+    }
+
+    public void setEditor(EditorCurves editorCurves) {
+        mEditorCurves = editorCurves;
+    }
+
+    public void setFilterDrawRepresentation(FilterCurvesRepresentation drawRep) {
+        mFilterCurvesRepresentation = drawRep;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
new file mode 100644
index 0000000..9722034
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
@@ -0,0 +1,139 @@
+
+package com.android.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorDraw;
+import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilterDraw;
+
+public class ImageDraw extends ImageShow {
+
+    private static final String LOGTAG = "ImageDraw";
+    private int mCurrentColor = Color.RED;
+    final static float INITAL_STROKE_RADIUS = 40;
+    private float mCurrentSize = INITAL_STROKE_RADIUS;
+    private byte mType = 0;
+    private FilterDrawRepresentation mFRep;
+    private EditorDraw mEditorDraw;
+
+    public ImageDraw(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        resetParameter();
+    }
+
+    public ImageDraw(Context context) {
+        super(context);
+        resetParameter();
+    }
+
+    public void setEditor(EditorDraw editorDraw) {
+        mEditorDraw = editorDraw;
+    }
+    public void setFilterDrawRepresentation(FilterDrawRepresentation fr) {
+        mFRep = fr;
+    }
+
+    public Drawable getIcon(Context context) {
+
+        return null;
+    }
+
+    @Override
+    public void resetParameter() {
+        if (mFRep != null) {
+            mFRep.clear();
+        }
+    }
+
+    public void setColor(int color) {
+        mCurrentColor = color;
+    }
+
+    public void setSize(int size) {
+        mCurrentSize = size;
+    }
+
+    public void setStyle(byte style) {
+        mType = (byte) (style % ImageFilterDraw.NUMBER_OF_STYLES);
+    }
+
+    public int getStyle() {
+        return mType;
+    }
+
+    public int getSize() {
+        return (int) mCurrentSize;
+    }
+
+    float[] mTmpPoint = new float[2]; // so we do not malloc
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getPointerCount() > 1) {
+            boolean ret = super.onTouchEvent(event);
+            if (mFRep.getCurrentDrawing() != null) {
+                mFRep.clearCurrentSection();
+                mEditorDraw.commitLocalRepresentation();
+            }
+            return ret;
+        }
+        if (event.getAction() != MotionEvent.ACTION_DOWN) {
+            if (mFRep.getCurrentDrawing() == null) {
+                return super.onTouchEvent(event);
+            }
+        }
+
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            calcScreenMapping();
+            mTmpPoint[0] = event.getX();
+            mTmpPoint[1] = event.getY();
+            mToOrig.mapPoints(mTmpPoint);
+            mFRep.startNewSection(mType, mCurrentColor, mCurrentSize, mTmpPoint[0], mTmpPoint[1]);
+        }
+
+        if (event.getAction() == MotionEvent.ACTION_MOVE) {
+
+            int historySize = event.getHistorySize();
+            for (int h = 0; h < historySize; h++) {
+                int p = 0;
+                {
+                    mTmpPoint[0] = event.getHistoricalX(p, h);
+                    mTmpPoint[1] = event.getHistoricalY(p, h);
+                    mToOrig.mapPoints(mTmpPoint);
+                    mFRep.addPoint(mTmpPoint[0], mTmpPoint[1]);
+                }
+            }
+        }
+
+        if (event.getAction() == MotionEvent.ACTION_UP) {
+            mTmpPoint[0] = event.getX();
+            mTmpPoint[1] = event.getY();
+            mToOrig.mapPoints(mTmpPoint);
+            mFRep.endSection(mTmpPoint[0], mTmpPoint[1]);
+        }
+        mEditorDraw.commitLocalRepresentation();
+        invalidate();
+        return true;
+    }
+
+    Matrix mRotateToScreen = new Matrix();
+    Matrix mToOrig;
+    private void calcScreenMapping() {
+        mToOrig = getScreenToImageMatrix(true);
+        mToOrig.invert(mRotateToScreen);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        calcScreenMapping();
+
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java
new file mode 100644
index 0000000..b55cc2b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java
@@ -0,0 +1,215 @@
+package com.android.gallery3d.filtershow.imageshow;
+/*
+ * Copyright (C) 2013 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.
+ */
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorGrad;
+import com.android.gallery3d.filtershow.filters.FilterGradRepresentation;
+
+public class ImageGrad extends ImageShow {
+    private static final String LOGTAG = "ImageGrad";
+    private FilterGradRepresentation mGradRep;
+    private EditorGrad mEditorGrad;
+    private float mMinTouchDist;
+    private int mActiveHandle = -1;
+    private GradControl mEllipse;
+
+    Matrix mToScr = new Matrix();
+    float[] mPointsX = new float[FilterGradRepresentation.MAX_POINTS];
+    float[] mPointsY = new float[FilterGradRepresentation.MAX_POINTS];
+
+    public ImageGrad(Context context) {
+        super(context);
+        Resources res = context.getResources();
+        mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist);
+        mEllipse = new GradControl(context);
+        mEllipse.setShowReshapeHandles(false);
+    }
+
+    public ImageGrad(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        Resources res = context.getResources();
+        mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist);
+        mEllipse = new GradControl(context);
+        mEllipse.setShowReshapeHandles(false);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int mask = event.getActionMasked();
+
+        if (mActiveHandle == -1) {
+            if (MotionEvent.ACTION_DOWN != mask) {
+                return super.onTouchEvent(event);
+            }
+            if (event.getPointerCount() == 1) {
+                mActiveHandle = mEllipse.getCloseHandle(event.getX(), event.getY());
+                if (mActiveHandle == -1) {
+                    float x = event.getX();
+                    float y = event.getY();
+                    float min_d = Float.MAX_VALUE;
+                    int pos = -1;
+                    for (int i = 0; i < mPointsX.length; i++) {
+                        if (mPointsX[i] == -1) {
+                            continue;
+                        }
+                        float d = (float) Math.hypot(x - mPointsX[i], y - mPointsY[i]);
+                        if ( min_d > d) {
+                            min_d = d;
+                            pos = i;
+                        }
+                    }
+                    if (min_d > mMinTouchDist){
+                        pos = -1;
+                    }
+
+                    if (pos != -1) {
+                        mGradRep.setSelectedPoint(pos);
+                        resetImageCaches(this);
+                        mEditorGrad.updateSeekBar(mGradRep);
+                        mEditorGrad.commitLocalRepresentation();
+                        invalidate();
+                    }
+                }
+            }
+            if (mActiveHandle == -1) {
+                return super.onTouchEvent(event);
+            }
+        } else {
+            switch (mask) {
+                case MotionEvent.ACTION_UP: {
+
+                    mActiveHandle = -1;
+                    break;
+                }
+                case MotionEvent.ACTION_DOWN: {
+                    break;
+                }
+            }
+        }
+        float x = event.getX();
+        float y = event.getY();
+
+        mEllipse.setScrImageInfo(getScreenToImageMatrix(true),
+                MasterImage.getImage().getOriginalBounds());
+
+        switch (mask) {
+            case (MotionEvent.ACTION_DOWN): {
+                mEllipse.actionDown(x, y, mGradRep);
+                break;
+            }
+            case (MotionEvent.ACTION_UP):
+            case (MotionEvent.ACTION_MOVE): {
+                mEllipse.actionMove(mActiveHandle, x, y, mGradRep);
+                setRepresentation(mGradRep);
+                break;
+            }
+        }
+        invalidate();
+        mEditorGrad.commitLocalRepresentation();
+        return true;
+    }
+
+    public void setRepresentation(FilterGradRepresentation pointRep) {
+        mGradRep = pointRep;
+        Matrix toImg = getScreenToImageMatrix(false);
+
+        toImg.invert(mToScr);
+
+        float[] c1 = new float[] { mGradRep.getPoint1X(), mGradRep.getPoint1Y() };
+        float[] c2 = new float[] { mGradRep.getPoint2X(), mGradRep.getPoint2Y() };
+
+        if (c1[0] == -1) {
+            float cx = MasterImage.getImage().getOriginalBounds().width() / 2;
+            float cy = MasterImage.getImage().getOriginalBounds().height() / 2;
+            float rx = Math.min(cx, cy) * .4f;
+
+            mGradRep.setPoint1(cx, cy-rx);
+            mGradRep.setPoint2(cx, cy+rx);
+            c1[0] = cx;
+            c1[1] = cy-rx;
+            mToScr.mapPoints(c1);
+            if (getWidth() != 0) {
+                mEllipse.setPoint1(c1[0], c1[1]);
+                c2[0] = cx;
+                c2[1] = cy+rx;
+                mToScr.mapPoints(c2);
+                mEllipse.setPoint2(c2[0], c2[1]);
+            }
+            mEditorGrad.commitLocalRepresentation();
+        } else {
+            mToScr.mapPoints(c1);
+            mToScr.mapPoints(c2);
+            mEllipse.setPoint1(c1[0], c1[1]);
+            mEllipse.setPoint2(c2[0], c2[1]);
+        }
+    }
+
+    public void drawOtherPoints(Canvas canvas) {
+        computCenterLocations();
+        for (int i = 0; i < mPointsX.length; i++) {
+            if (mPointsX[i] != -1) {
+                mEllipse.paintGrayPoint(canvas, mPointsX[i], mPointsY[i]);
+            }
+        }
+    }
+
+    public void computCenterLocations() {
+        int x1[] = mGradRep.getXPos1();
+        int y1[] = mGradRep.getYPos1();
+        int x2[] = mGradRep.getXPos2();
+        int y2[] = mGradRep.getYPos2();
+        int selected = mGradRep.getSelectedPoint();
+        boolean m[] = mGradRep.getMask();
+        float[] c = new float[2];
+        for (int i = 0; i < m.length; i++) {
+            if (selected == i || !m[i]) {
+                mPointsX[i] = -1;
+                continue;
+            }
+
+            c[0] = (x1[i]+x2[i])/2;
+            c[1] = (y1[i]+y2[i])/2;
+            mToScr.mapPoints(c);
+
+            mPointsX[i] = c[0];
+            mPointsY[i] = c[1];
+        }
+    }
+
+    public void setEditor(EditorGrad editorGrad) {
+        mEditorGrad = editorGrad;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mGradRep == null) {
+            return;
+        }
+        setRepresentation(mGradRep);
+        mEllipse.draw(canvas);
+        drawOtherPoints(canvas);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java
new file mode 100644
index 0000000..26c49b1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+public class ImageMirror extends ImageShow {
+    private static final String TAG = ImageMirror.class.getSimpleName();
+    private EditorMirror mEditorMirror;
+    private FilterMirrorRepresentation mLocalRep = new FilterMirrorRepresentation();
+    private GeometryHolder mDrawHolder = new GeometryHolder();
+
+    public ImageMirror(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ImageMirror(Context context) {
+        super(context);
+    }
+
+    public void setFilterMirrorRepresentation(FilterMirrorRepresentation rep) {
+        mLocalRep = (rep == null) ? new FilterMirrorRepresentation() : rep;
+    }
+
+    public void flip() {
+        mLocalRep.cycle();
+        invalidate();
+    }
+
+    public FilterMirrorRepresentation getFinalRepresentation() {
+        return mLocalRep;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Treat event as handled.
+        return true;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        MasterImage master = MasterImage.getImage();
+        Bitmap image = master.getFiltersOnlyImage();
+        if (image == null) {
+            return;
+        }
+        GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep);
+        GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, getWidth(),
+                getHeight());
+    }
+
+    public void setEditor(EditorMirror editorFlip) {
+        mEditorMirror = editorFlip;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java
new file mode 100644
index 0000000..fd57141
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.filtershow.editors.EditorRedEye;
+import com.android.gallery3d.filtershow.filters.FilterPoint;
+import com.android.gallery3d.filtershow.filters.FilterRedEyeRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilterRedEye;
+
+public abstract class ImagePoint extends ImageShow {
+
+    private static final String LOGTAG = "ImageRedEyes";
+    protected EditorRedEye mEditorRedEye;
+    protected FilterRedEyeRepresentation mRedEyeRep;
+    protected static float mTouchPadding = 80;
+
+    public static void setTouchPadding(float padding) {
+        mTouchPadding = padding;
+    }
+
+    public ImagePoint(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ImagePoint(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void resetParameter() {
+        ImageFilterRedEye filter = (ImageFilterRedEye) getCurrentFilter();
+        if (filter != null) {
+            filter.clear();
+        }
+        invalidate();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        Paint paint = new Paint();
+        paint.setStyle(Style.STROKE);
+        paint.setColor(Color.RED);
+        paint.setStrokeWidth(2);
+
+        Matrix originalToScreen = getImageToScreenMatrix(false);
+        Matrix originalRotateToScreen = getImageToScreenMatrix(true);
+
+        if (mRedEyeRep != null) {
+            for (FilterPoint candidate : mRedEyeRep.getCandidates()) {
+                drawPoint(candidate, canvas, originalToScreen, originalRotateToScreen, paint);
+            }
+        }
+    }
+
+    protected abstract void drawPoint(
+            FilterPoint candidate, Canvas canvas, Matrix originalToScreen,
+            Matrix originalRotateToScreen, Paint paint);
+
+    public void setEditor(EditorRedEye editorRedEye) {
+        mEditorRedEye = editorRedEye;
+    }
+
+    public void setRepresentation(FilterRedEyeRepresentation redEyeRep) {
+        mRedEyeRep = redEyeRep;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java
new file mode 100644
index 0000000..40433a0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.filters.FilterPoint;
+import com.android.gallery3d.filtershow.filters.RedEyeCandidate;
+
+public class ImageRedEye extends ImagePoint {
+    private static final String LOGTAG = "ImageRedEyes";
+    private RectF mCurrentRect = null;
+
+    public ImageRedEye(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void resetParameter() {
+        super.resetParameter();
+        invalidate();
+    }
+
+    @Override
+
+    public boolean onTouchEvent(MotionEvent event) {
+        super.onTouchEvent(event);
+
+        if (event.getPointerCount() > 1) {
+            return true;
+        }
+
+        if (didFinishScalingOperation()) {
+            return true;
+        }
+
+        float ex = event.getX();
+        float ey = event.getY();
+
+        // let's transform (ex, ey) to displayed image coordinates
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            mCurrentRect = new RectF();
+            mCurrentRect.left = ex - mTouchPadding;
+            mCurrentRect.top = ey - mTouchPadding;
+        }
+        if (event.getAction() == MotionEvent.ACTION_MOVE) {
+            mCurrentRect.right = ex + mTouchPadding;
+            mCurrentRect.bottom = ey + mTouchPadding;
+        }
+        if (event.getAction() == MotionEvent.ACTION_UP) {
+            if (mCurrentRect != null) {
+                // transform to original coordinates
+                Matrix originalNoRotateToScreen = getImageToScreenMatrix(false);
+                Matrix originalToScreen = getImageToScreenMatrix(true);
+                Matrix invert = new Matrix();
+                originalToScreen.invert(invert);
+                RectF r = new RectF(mCurrentRect);
+                invert.mapRect(r);
+                RectF r2 = new RectF(mCurrentRect);
+                invert.reset();
+                originalNoRotateToScreen.invert(invert);
+                invert.mapRect(r2);
+                mRedEyeRep.addRect(r, r2);
+                this.resetImageCaches(this);
+            }
+            mCurrentRect = null;
+        }
+        mEditorRedEye.commitLocalRepresentation();
+        invalidate();
+        return true;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        Paint paint = new Paint();
+        paint.setStyle(Style.STROKE);
+        paint.setColor(Color.RED);
+        paint.setStrokeWidth(2);
+        if (mCurrentRect != null) {
+            paint.setColor(Color.RED);
+            RectF drawRect = new RectF(mCurrentRect);
+            canvas.drawRect(drawRect, paint);
+        }
+    }
+
+    @Override
+    protected void drawPoint(FilterPoint point, Canvas canvas, Matrix originalToScreen,
+            Matrix originalRotateToScreen, Paint paint) {
+        RedEyeCandidate candidate = (RedEyeCandidate) point;
+        RectF rect = candidate.getRect();
+        RectF drawRect = new RectF();
+        originalToScreen.mapRect(drawRect, rect);
+        RectF fullRect = new RectF();
+        originalRotateToScreen.mapRect(fullRect, rect);
+        paint.setColor(Color.BLUE);
+        canvas.drawRect(fullRect, paint);
+        canvas.drawLine(fullRect.centerX(), fullRect.top,
+                fullRect.centerX(), fullRect.bottom, paint);
+        canvas.drawLine(fullRect.left, fullRect.centerY(),
+                fullRect.right, fullRect.centerY(), paint);
+        paint.setColor(Color.GREEN);
+        float dw = drawRect.width();
+        float dh = drawRect.height();
+        float dx = fullRect.centerX() - dw / 2;
+        float dy = fullRect.centerY() - dh / 2;
+        drawRect.set(dx, dy, dx + dw, dy + dh);
+        canvas.drawRect(drawRect, paint);
+        canvas.drawLine(drawRect.centerX(), drawRect.top,
+                drawRect.centerX(), drawRect.bottom, paint);
+        canvas.drawLine(drawRect.left, drawRect.centerY(),
+                drawRect.right, drawRect.centerY(), paint);
+        canvas.drawCircle(drawRect.centerX(), drawRect.centerY(),
+                mTouchPadding, paint);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
new file mode 100644
index 0000000..5186c09
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+public class ImageRotate extends ImageShow {
+    private EditorRotate mEditorRotate;
+    private static final String TAG = ImageRotate.class.getSimpleName();
+    private FilterRotateRepresentation mLocalRep = new FilterRotateRepresentation();
+    private GeometryHolder mDrawHolder = new GeometryHolder();
+
+    public ImageRotate(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ImageRotate(Context context) {
+        super(context);
+    }
+
+    public void setFilterRotateRepresentation(FilterRotateRepresentation rep) {
+        mLocalRep = (rep == null) ? new FilterRotateRepresentation() : rep;
+    }
+
+    public void rotate() {
+        mLocalRep.rotateCW();
+        invalidate();
+    }
+
+    public FilterRotateRepresentation getFinalRepresentation() {
+        return mLocalRep;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Treat event as handled.
+        return true;
+    }
+
+    public int getLocalValue() {
+        return mLocalRep.getRotation().value();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        MasterImage master = MasterImage.getImage();
+        Bitmap image = master.getFiltersOnlyImage();
+        if (image == null) {
+            return;
+        }
+        GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep);
+        GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, canvas.getWidth(),
+                canvas.getHeight());
+    }
+
+    public void setEditor(EditorRotate editorRotate) {
+        mEditorRotate = editorRotate;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
new file mode 100644
index 0000000..6278b2a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ImageShow extends View implements OnGestureListener,
+        ScaleGestureDetector.OnScaleGestureListener,
+        OnDoubleTapListener {
+
+    private static final String LOGTAG = "ImageShow";
+    private static final boolean ENABLE_ZOOMED_COMPARISON = false;
+
+    protected Paint mPaint = new Paint();
+    protected int mTextSize;
+    protected int mTextPadding;
+
+    protected int mBackgroundColor;
+
+    private GestureDetector mGestureDetector = null;
+    private ScaleGestureDetector mScaleGestureDetector = null;
+
+    protected Rect mImageBounds = new Rect();
+    private boolean mOriginalDisabled = false;
+    private boolean mTouchShowOriginal = false;
+    private long mTouchShowOriginalDate = 0;
+    private final long mTouchShowOriginalDelayMin = 200; // 200ms
+    private int mShowOriginalDirection = 0;
+    private static int UNVEIL_HORIZONTAL = 1;
+    private static int UNVEIL_VERTICAL = 2;
+
+    private Point mTouchDown = new Point();
+    private Point mTouch = new Point();
+    private boolean mFinishedScalingOperation = false;
+
+    private int mOriginalTextMargin;
+    private int mOriginalTextSize;
+    private String mOriginalText;
+    private boolean mZoomIn = false;
+    Point mOriginalTranslation = new Point();
+    float mOriginalScale;
+    float mStartFocusX, mStartFocusY;
+    private enum InteractionMode {
+        NONE,
+        SCALE,
+        MOVE
+    }
+    InteractionMode mInteractionMode = InteractionMode.NONE;
+
+    private FilterShowActivity mActivity = null;
+
+    public FilterShowActivity getActivity() {
+        return mActivity;
+    }
+
+    public boolean hasModifications() {
+        return MasterImage.getImage().hasModifications();
+    }
+
+    public void resetParameter() {
+        // TODO: implement reset
+    }
+
+    public void onNewValue(int parameter) {
+        invalidate();
+    }
+
+    public ImageShow(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        setupImageShow(context);
+    }
+
+    public ImageShow(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setupImageShow(context);
+
+    }
+
+    public ImageShow(Context context) {
+        super(context);
+        setupImageShow(context);
+    }
+
+    private void setupImageShow(Context context) {
+        Resources res = context.getResources();
+        mTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_text_size);
+        mTextPadding = res.getDimensionPixelSize(R.dimen.photoeditor_text_padding);
+        mOriginalTextMargin = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_margin);
+        mOriginalTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_size);
+        mBackgroundColor = res.getColor(R.color.background_screen);
+        mOriginalText = res.getString(R.string.original_picture_text);
+        setupGestureDetector(context);
+        mActivity = (FilterShowActivity) context;
+        MasterImage.getImage().addObserver(this);
+    }
+
+    public void setupGestureDetector(Context context) {
+        mGestureDetector = new GestureDetector(context, this);
+        mScaleGestureDetector = new ScaleGestureDetector(context, this);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
+        setMeasuredDimension(parentWidth, parentHeight);
+    }
+
+    public ImageFilter getCurrentFilter() {
+        return MasterImage.getImage().getCurrentFilter();
+    }
+
+    /* consider moving the following 2 methods into a subclass */
+    /**
+     * This function calculates a Image to Screen Transformation matrix
+     *
+     * @param reflectRotation set true if you want the rotation encoded
+     * @return Image to Screen transformation matrix
+     */
+    protected Matrix getImageToScreenMatrix(boolean reflectRotation) {
+        MasterImage master = MasterImage.getImage();
+        if (master.getOriginalBounds() == null) {
+            return new Matrix();
+        }
+        Matrix m = GeometryMathUtils.getImageToScreenMatrix(master.getPreset().getGeometryFilters(),
+                reflectRotation, master.getOriginalBounds(), getWidth(), getHeight());
+        Point translate = master.getTranslation();
+        float scaleFactor = master.getScaleFactor();
+        m.postTranslate(translate.x, translate.y);
+        m.postScale(scaleFactor, scaleFactor, getWidth() / 2.0f, getHeight() / 2.0f);
+        return m;
+    }
+
+    /**
+     * This function calculates a to Screen Image Transformation matrix
+     *
+     * @param reflectRotation set true if you want the rotation encoded
+     * @return Screen to Image transformation matrix
+     */
+    protected Matrix getScreenToImageMatrix(boolean reflectRotation) {
+        Matrix m = getImageToScreenMatrix(reflectRotation);
+        Matrix invert = new Matrix();
+        m.invert(invert);
+        return invert;
+    }
+
+    public ImagePreset getImagePreset() {
+        return MasterImage.getImage().getPreset();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        MasterImage.getImage().setImageShowSize(getWidth(), getHeight());
+
+        float cx = canvas.getWidth()/2.0f;
+        float cy = canvas.getHeight()/2.0f;
+        float scaleFactor = MasterImage.getImage().getScaleFactor();
+        Point translation = MasterImage.getImage().getTranslation();
+
+        Matrix scalingMatrix = new Matrix();
+        scalingMatrix.postScale(scaleFactor, scaleFactor, cx, cy);
+        scalingMatrix.preTranslate(translation.x, translation.y);
+
+        RectF unscaledClipRect = new RectF(mImageBounds);
+        scalingMatrix.mapRect(unscaledClipRect, unscaledClipRect);
+
+        canvas.save();
+
+        boolean enablePartialRendering = false;
+
+        // For now, partial rendering is disabled for all filters,
+        // so no need to clip.
+        if (enablePartialRendering && !unscaledClipRect.isEmpty()) {
+            canvas.clipRect(unscaledClipRect);
+        }
+
+        canvas.save();
+        // TODO: center scale on gesture
+        canvas.scale(scaleFactor, scaleFactor, cx, cy);
+        canvas.translate(translation.x, translation.y);
+        drawImage(canvas, getFilteredImage(), true);
+        Bitmap highresPreview = MasterImage.getImage().getHighresImage();
+        if (highresPreview != null) {
+            drawImage(canvas, highresPreview, true);
+        }
+        canvas.restore();
+
+        Bitmap partialPreview = MasterImage.getImage().getPartialImage();
+        if (partialPreview != null) {
+            Rect src = new Rect(0, 0, partialPreview.getWidth(), partialPreview.getHeight());
+            Rect dest = new Rect(0, 0, getWidth(), getHeight());
+            canvas.drawBitmap(partialPreview, src, dest, mPaint);
+        }
+
+        canvas.save();
+        canvas.scale(scaleFactor, scaleFactor, cx, cy);
+        canvas.translate(translation.x, translation.y);
+        drawPartialImage(canvas, getGeometryOnlyImage());
+        canvas.restore();
+
+        canvas.restore();
+    }
+
+    public void resetImageCaches(ImageShow caller) {
+        MasterImage.getImage().updatePresets(true);
+    }
+
+    public Bitmap getFiltersOnlyImage() {
+        return MasterImage.getImage().getFiltersOnlyImage();
+    }
+
+    public Bitmap getGeometryOnlyImage() {
+        return MasterImage.getImage().getGeometryOnlyImage();
+    }
+
+    public Bitmap getFilteredImage() {
+        return MasterImage.getImage().getFilteredImage();
+    }
+
+    public void drawImage(Canvas canvas, Bitmap image, boolean updateBounds) {
+        if (image != null) {
+            Rect s = new Rect(0, 0, image.getWidth(),
+                    image.getHeight());
+
+            float scale = GeometryMathUtils.scale(image.getWidth(), image.getHeight(), getWidth(),
+                    getHeight());
+
+            float w = image.getWidth() * scale;
+            float h = image.getHeight() * scale;
+            float ty = (getHeight() - h) / 2.0f;
+            float tx = (getWidth() - w) / 2.0f;
+
+            Rect d = new Rect((int) tx, (int) ty, (int) (w + tx),
+                    (int) (h + ty));
+            if (updateBounds) {
+                mImageBounds = d;
+            }
+            canvas.drawBitmap(image, s, d, mPaint);
+        }
+    }
+
+    public void drawPartialImage(Canvas canvas, Bitmap image) {
+        boolean showsOriginal = MasterImage.getImage().showsOriginal();
+        if (!showsOriginal && !mTouchShowOriginal)
+            return;
+        canvas.save();
+        if (image != null) {
+            if (mShowOriginalDirection == 0) {
+                if (Math.abs(mTouch.y - mTouchDown.y) > Math.abs(mTouch.x - mTouchDown.x)) {
+                    mShowOriginalDirection = UNVEIL_VERTICAL;
+                } else {
+                    mShowOriginalDirection = UNVEIL_HORIZONTAL;
+                }
+            }
+
+            int px = 0;
+            int py = 0;
+            if (mShowOriginalDirection == UNVEIL_VERTICAL) {
+                px = mImageBounds.width();
+                py = mTouch.y - mImageBounds.top;
+            } else {
+                px = mTouch.x - mImageBounds.left;
+                py = mImageBounds.height();
+                if (showsOriginal) {
+                    px = mImageBounds.width();
+                }
+            }
+
+            Rect d = new Rect(mImageBounds.left, mImageBounds.top,
+                    mImageBounds.left + px, mImageBounds.top + py);
+            canvas.clipRect(d);
+            drawImage(canvas, image, false);
+            Paint paint = new Paint();
+            paint.setColor(Color.BLACK);
+            paint.setStrokeWidth(3);
+
+            if (mShowOriginalDirection == UNVEIL_VERTICAL) {
+                canvas.drawLine(mImageBounds.left, mTouch.y,
+                        mImageBounds.right, mTouch.y, paint);
+            } else {
+                canvas.drawLine(mTouch.x, mImageBounds.top,
+                        mTouch.x, mImageBounds.bottom, paint);
+            }
+
+            Rect bounds = new Rect();
+            paint.setAntiAlias(true);
+            paint.setTextSize(mOriginalTextSize);
+            paint.getTextBounds(mOriginalText, 0, mOriginalText.length(), bounds);
+            paint.setColor(Color.BLACK);
+            paint.setStyle(Paint.Style.STROKE);
+            paint.setStrokeWidth(3);
+            canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin,
+                    mImageBounds.top + bounds.height() + mOriginalTextMargin, paint);
+            paint.setStyle(Paint.Style.FILL);
+            paint.setStrokeWidth(1);
+            paint.setColor(Color.WHITE);
+            canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin,
+                    mImageBounds.top + bounds.height() + mOriginalTextMargin, paint);
+        }
+        canvas.restore();
+    }
+
+    public void bindAsImageLoadListener() {
+        MasterImage.getImage().addListener(this);
+    }
+
+    public void updateImage() {
+        invalidate();
+    }
+
+    public void imageLoaded() {
+        updateImage();
+    }
+
+    public void saveImage(FilterShowActivity filterShowActivity, File file) {
+        SaveImage.saveImage(getImagePreset(), filterShowActivity, file);
+    }
+
+
+    public boolean scaleInProgress() {
+        return mScaleGestureDetector.isInProgress();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        super.onTouchEvent(event);
+        int action = event.getAction();
+        action = action & MotionEvent.ACTION_MASK;
+
+        mGestureDetector.onTouchEvent(event);
+        boolean scaleInProgress = scaleInProgress();
+        mScaleGestureDetector.onTouchEvent(event);
+        if (mInteractionMode == InteractionMode.SCALE) {
+            return true;
+        }
+        if (!scaleInProgress() && scaleInProgress) {
+            // If we were scaling, the scale will stop but we will
+            // still issue an ACTION_UP. Let the subclasses know.
+            mFinishedScalingOperation = true;
+        }
+
+        int ex = (int) event.getX();
+        int ey = (int) event.getY();
+        if (action == MotionEvent.ACTION_DOWN) {
+            mInteractionMode = InteractionMode.MOVE;
+            mTouchDown.x = ex;
+            mTouchDown.y = ey;
+            mTouchShowOriginalDate = System.currentTimeMillis();
+            mShowOriginalDirection = 0;
+            MasterImage.getImage().setOriginalTranslation(MasterImage.getImage().getTranslation());
+        }
+
+        if (action == MotionEvent.ACTION_MOVE && mInteractionMode == InteractionMode.MOVE) {
+            mTouch.x = ex;
+            mTouch.y = ey;
+
+            float scaleFactor = MasterImage.getImage().getScaleFactor();
+            if (scaleFactor > 1 && (!ENABLE_ZOOMED_COMPARISON || event.getPointerCount() == 2)) {
+                float translateX = (mTouch.x - mTouchDown.x) / scaleFactor;
+                float translateY = (mTouch.y - mTouchDown.y) / scaleFactor;
+                Point originalTranslation = MasterImage.getImage().getOriginalTranslation();
+                Point translation = MasterImage.getImage().getTranslation();
+                translation.x = (int) (originalTranslation.x + translateX);
+                translation.y = (int) (originalTranslation.y + translateY);
+                constrainTranslation(translation, scaleFactor);
+                MasterImage.getImage().setTranslation(translation);
+                mTouchShowOriginal = false;
+            } else if (enableComparison() && !mOriginalDisabled
+                    && (System.currentTimeMillis() - mTouchShowOriginalDate
+                            > mTouchShowOriginalDelayMin)
+                    && event.getPointerCount() == 1) {
+                mTouchShowOriginal = true;
+            }
+        }
+
+        if (action == MotionEvent.ACTION_UP) {
+            mInteractionMode = InteractionMode.NONE;
+            mTouchShowOriginal = false;
+            mTouchDown.x = 0;
+            mTouchDown.y = 0;
+            mTouch.x = 0;
+            mTouch.y = 0;
+            if (MasterImage.getImage().getScaleFactor() <= 1) {
+                MasterImage.getImage().setScaleFactor(1);
+                MasterImage.getImage().resetTranslation();
+            }
+        }
+        invalidate();
+        return true;
+    }
+
+    protected boolean enableComparison() {
+        return true;
+    }
+
+    @Override
+    public boolean onDoubleTap(MotionEvent arg0) {
+        mZoomIn = !mZoomIn;
+        float scale = 1.0f;
+        if (mZoomIn) {
+            scale = MasterImage.getImage().getMaxScaleFactor();
+        }
+        if (scale != MasterImage.getImage().getScaleFactor()) {
+            MasterImage.getImage().setScaleFactor(scale);
+            float translateX = (getWidth() / 2 - arg0.getX());
+            float translateY = (getHeight() / 2 - arg0.getY());
+            Point translation = MasterImage.getImage().getTranslation();
+            translation.x = (int) (mOriginalTranslation.x + translateX);
+            translation.y = (int) (mOriginalTranslation.y + translateY);
+            constrainTranslation(translation, scale);
+            MasterImage.getImage().setTranslation(translation);
+            invalidate();
+        }
+        return true;
+    }
+
+    private void constrainTranslation(Point translation, float scale) {
+        float maxTranslationX = getWidth() / scale;
+        float maxTranslationY = getHeight() / scale;
+        if (Math.abs(translation.x) > maxTranslationX) {
+            translation.x = (int) (Math.signum(translation.x) *
+                    maxTranslationX);
+            if (Math.abs(translation.y) > maxTranslationY) {
+                translation.y = (int) (Math.signum(translation.y) *
+                        maxTranslationY);
+            }
+
+        }
+    }
+
+    @Override
+    public boolean onDoubleTapEvent(MotionEvent arg0) {
+        return false;
+    }
+
+    @Override
+    public boolean onSingleTapConfirmed(MotionEvent arg0) {
+        return false;
+    }
+
+    @Override
+    public boolean onDown(MotionEvent arg0) {
+        return false;
+    }
+
+    @Override
+    public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float arg2, float arg3) {
+        if (mActivity == null) {
+            return false;
+        }
+        if (endEvent.getPointerCount() == 2) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void onLongPress(MotionEvent arg0) {
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
+        return false;
+    }
+
+    @Override
+    public void onShowPress(MotionEvent arg0) {
+    }
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent arg0) {
+        return false;
+    }
+
+    public boolean useUtilityPanel() {
+        return false;
+    }
+
+    public void openUtilityPanel(final LinearLayout accessoryViewList) {
+    }
+
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+        MasterImage img = MasterImage.getImage();
+        float scaleFactor = img.getScaleFactor();
+
+        scaleFactor = scaleFactor * detector.getScaleFactor();
+        if (scaleFactor > MasterImage.getImage().getMaxScaleFactor()) {
+            scaleFactor = MasterImage.getImage().getMaxScaleFactor();
+        }
+        if (scaleFactor < 0.5) {
+            scaleFactor = 0.5f;
+        }
+        MasterImage.getImage().setScaleFactor(scaleFactor);
+        scaleFactor = img.getScaleFactor();
+        float focusx = detector.getFocusX();
+        float focusy = detector.getFocusY();
+        float translateX = (focusx - mStartFocusX) / scaleFactor;
+        float translateY = (focusy - mStartFocusY) / scaleFactor;
+        Point translation = MasterImage.getImage().getTranslation();
+        translation.x = (int) (mOriginalTranslation.x + translateX);
+        translation.y = (int) (mOriginalTranslation.y + translateY);
+        constrainTranslation(translation, scaleFactor);
+        MasterImage.getImage().setTranslation(translation);
+
+        invalidate();
+        return true;
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        Point pos = MasterImage.getImage().getTranslation();
+        mOriginalTranslation.x = pos.x;
+        mOriginalTranslation.y = pos.y;
+        mOriginalScale = MasterImage.getImage().getScaleFactor();
+        mStartFocusX = detector.getFocusX();
+        mStartFocusY = detector.getFocusY();
+        mInteractionMode = InteractionMode.SCALE;
+        return true;
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+        mInteractionMode = InteractionMode.NONE;
+        if (MasterImage.getImage().getScaleFactor() < 1) {
+            MasterImage.getImage().setScaleFactor(1);
+            invalidate();
+        }
+    }
+
+    public boolean didFinishScalingOperation() {
+        if (mFinishedScalingOperation) {
+            mFinishedScalingOperation = false;
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
new file mode 100644
index 0000000..ff75dcc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+
+public class ImageStraighten extends ImageShow {
+    private static final String TAG = ImageStraighten.class.getSimpleName();
+    private float mBaseAngle = 0;
+    private float mAngle = 0;
+    private float mInitialAngle = 0;
+    private boolean mFirstDrawSinceUp = false;
+    private EditorStraighten mEditorStraighten;
+    private FilterStraightenRepresentation mLocalRep = new FilterStraightenRepresentation();
+    private RectF mPriorCropAtUp = new RectF();
+    private RectF mDrawRect = new RectF();
+    private Path mDrawPath = new Path();
+    private GeometryHolder mDrawHolder = new GeometryHolder();
+    private enum MODES {
+        NONE, MOVE
+    }
+    private MODES mState = MODES.NONE;
+    private static final float MAX_STRAIGHTEN_ANGLE
+        = FilterStraightenRepresentation.MAX_STRAIGHTEN_ANGLE;
+    private static final float MIN_STRAIGHTEN_ANGLE
+        = FilterStraightenRepresentation.MIN_STRAIGHTEN_ANGLE;
+    private float mCurrentX;
+    private float mCurrentY;
+    private float mTouchCenterX;
+    private float mTouchCenterY;
+    private RectF mCrop = new RectF();
+    private final Paint mPaint = new Paint();
+
+    public ImageStraighten(Context context) {
+        super(context);
+    }
+
+    public ImageStraighten(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setFilterStraightenRepresentation(FilterStraightenRepresentation rep) {
+        mLocalRep = (rep == null) ? new FilterStraightenRepresentation() : rep;
+        mInitialAngle = mBaseAngle = mAngle = mLocalRep.getStraighten();
+    }
+
+    public Collection<FilterRepresentation> getFinalRepresentation() {
+        ArrayList<FilterRepresentation> reps = new ArrayList<FilterRepresentation>(2);
+        reps.add(mLocalRep);
+        if (mInitialAngle != mLocalRep.getStraighten()) {
+            reps.add(new FilterCropRepresentation(mCrop));
+        }
+        return reps;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+
+        switch (event.getActionMasked()) {
+            case (MotionEvent.ACTION_DOWN):
+                if (mState == MODES.NONE) {
+                    mTouchCenterX = x;
+                    mTouchCenterY = y;
+                    mCurrentX = x;
+                    mCurrentY = y;
+                    mState = MODES.MOVE;
+                    mBaseAngle = mAngle;
+                }
+                break;
+            case (MotionEvent.ACTION_UP):
+                if (mState == MODES.MOVE) {
+                    mState = MODES.NONE;
+                    mCurrentX = x;
+                    mCurrentY = y;
+                    computeValue();
+                    mFirstDrawSinceUp = true;
+                }
+                break;
+            case (MotionEvent.ACTION_MOVE):
+                if (mState == MODES.MOVE) {
+                    mCurrentX = x;
+                    mCurrentY = y;
+                    computeValue();
+                }
+                break;
+            default:
+                break;
+        }
+        invalidate();
+        return true;
+    }
+
+    private static float angleFor(float dx, float dy) {
+        return (float) (Math.atan2(dx, dy) * 180 / Math.PI);
+    }
+
+    private float getCurrentTouchAngle() {
+        float centerX = getWidth() / 2f;
+        float centerY = getHeight() / 2f;
+        if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) {
+            return 0;
+        }
+        float dX1 = mTouchCenterX - centerX;
+        float dY1 = mTouchCenterY - centerY;
+        float dX2 = mCurrentX - centerX;
+        float dY2 = mCurrentY - centerY;
+        float angleA = angleFor(dX1, dY1);
+        float angleB = angleFor(dX2, dY2);
+        return (angleB - angleA) % 360;
+    }
+
+    private void computeValue() {
+        float angle = getCurrentTouchAngle();
+        mAngle = (mBaseAngle - angle) % 360;
+        mAngle = Math.max(MIN_STRAIGHTEN_ANGLE, mAngle);
+        mAngle = Math.min(MAX_STRAIGHTEN_ANGLE, mAngle);
+    }
+
+    private static void getUntranslatedStraightenCropBounds(RectF outRect, float straightenAngle) {
+        float deg = straightenAngle;
+        if (deg < 0) {
+            deg = -deg;
+        }
+        double a = Math.toRadians(deg);
+        double sina = Math.sin(a);
+        double cosa = Math.cos(a);
+        double rw = outRect.width();
+        double rh = outRect.height();
+        double h1 = rh * rh / (rw * sina + rh * cosa);
+        double h2 = rh * rw / (rw * cosa + rh * sina);
+        double hh = Math.min(h1, h2);
+        double ww = hh * rw / rh;
+        float left = (float) ((rw - ww) * 0.5f);
+        float top = (float) ((rh - hh) * 0.5f);
+        float right = (float) (left + ww);
+        float bottom = (float) (top + hh);
+        outRect.set(left, top, right, bottom);
+    }
+
+    private void updateCurrentCrop(Matrix m, GeometryHolder h, RectF tmp, int imageWidth,
+            int imageHeight, int viewWidth, int viewHeight) {
+        if (GeometryMathUtils.needsDimensionSwap(h.rotation)) {
+            tmp.set(0, 0, imageHeight, imageWidth);
+        } else {
+            tmp.set(0, 0, imageWidth, imageHeight);
+        }
+        float scale = GeometryMathUtils.scale(imageWidth, imageHeight, viewWidth, viewHeight);
+        GeometryMathUtils.scaleRect(tmp, scale);
+        getUntranslatedStraightenCropBounds(tmp, mAngle);
+        tmp.offset(viewWidth / 2f - tmp.centerX(), viewHeight / 2f - tmp.centerY());
+        h.straighten = 0;
+        Matrix m1 = GeometryMathUtils.getFullGeometryToScreenMatrix(h, imageWidth,
+                imageHeight, viewWidth, viewHeight);
+        m.reset();
+        m1.invert(m);
+        mCrop.set(tmp);
+        m.mapRect(mCrop);
+        FilterCropRepresentation.findNormalizedCrop(mCrop, imageWidth, imageHeight);
+    }
+
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        MasterImage master = MasterImage.getImage();
+        Bitmap image = master.getFiltersOnlyImage();
+        if (image == null) {
+            return;
+        }
+        GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep);
+        mDrawHolder.straighten = mAngle;
+        int imageWidth = image.getWidth();
+        int imageHeight = image.getHeight();
+        int viewWidth = canvas.getWidth();
+        int viewHeight = canvas.getHeight();
+
+        // Get matrix for drawing bitmap
+        Matrix m = GeometryMathUtils.getFullGeometryToScreenMatrix(mDrawHolder, imageWidth,
+                imageHeight, viewWidth, viewHeight);
+        mPaint.reset();
+        mPaint.setAntiAlias(true);
+        mPaint.setFilterBitmap(true);
+        canvas.drawBitmap(image, m, mPaint);
+
+        mPaint.setFilterBitmap(false);
+        mPaint.setColor(Color.WHITE);
+        mPaint.setStrokeWidth(2);
+        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+        updateCurrentCrop(m, mDrawHolder, mDrawRect, imageWidth,
+                imageHeight, viewWidth, viewHeight);
+        if (mFirstDrawSinceUp) {
+            mPriorCropAtUp.set(mCrop);
+            mLocalRep.setStraighten(mAngle);
+            mFirstDrawSinceUp = false;
+        }
+
+        // Draw the grid
+        if (mState == MODES.MOVE) {
+            canvas.save();
+            canvas.clipRect(mDrawRect);
+            int n = 16;
+            float step = viewWidth / n;
+            float p = 0;
+            for (int i = 1; i < n; i++) {
+                p = i * step;
+                mPaint.setAlpha(60);
+                canvas.drawLine(p, 0, p, viewHeight, mPaint);
+                canvas.drawLine(0, p, viewHeight, p, mPaint);
+            }
+            canvas.restore();
+        }
+        mPaint.reset();
+        mPaint.setColor(Color.WHITE);
+        mPaint.setStyle(Style.STROKE);
+        mPaint.setStrokeWidth(3);
+        mDrawPath.reset();
+        mDrawPath.addRect(mDrawRect, Path.Direction.CW);
+        canvas.drawPath(mDrawPath, mPaint);
+    }
+
+    public void setEditor(EditorStraighten editorStraighten) {
+        mEditorStraighten = editorStraighten;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java
new file mode 100644
index 0000000..25a0a90
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
+
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
+import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
+
+public class ImageTinyPlanet extends ImageShow {
+    private static final String LOGTAG = "ImageTinyPlanet";
+
+    private float mTouchCenterX = 0;
+    private float mTouchCenterY = 0;
+    private float mCurrentX = 0;
+    private float mCurrentY = 0;
+    private float mCenterX = 0;
+    private float mCenterY = 0;
+    private float mStartAngle = 0;
+    private FilterTinyPlanetRepresentation mTinyPlanetRep;
+    private EditorTinyPlanet mEditorTinyPlanet;
+    private ScaleGestureDetector mScaleGestureDetector = null;
+    boolean mInScale = false;
+    RectF mDestRect = new RectF();
+
+    OnScaleGestureListener mScaleGestureListener = new OnScaleGestureListener() {
+        private float mScale = 100;
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            mInScale = false;
+        }
+
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            mInScale = true;
+            mScale = mTinyPlanetRep.getValue();
+            return true;
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            int value = mTinyPlanetRep.getValue();
+            mScale *= detector.getScaleFactor();
+            value = (int) (mScale);
+            value = Math.min(mTinyPlanetRep.getMaximum(), value);
+            value = Math.max(mTinyPlanetRep.getMinimum(), value);
+            mTinyPlanetRep.setValue(value);
+            invalidate();
+            mEditorTinyPlanet.commitLocalRepresentation();
+            mEditorTinyPlanet.updateUI();
+            return true;
+        }
+    };
+
+    public ImageTinyPlanet(Context context) {
+        super(context);
+        mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener);
+    }
+
+    public ImageTinyPlanet(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mScaleGestureDetector = new ScaleGestureDetector(context,mScaleGestureListener );
+    }
+
+    protected static float angleFor(float dx, float dy) {
+        return (float) (Math.atan2(dx, dy) * 180 / Math.PI);
+    }
+
+    protected float getCurrentTouchAngle() {
+        if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) {
+            return 0;
+        }
+        float dX1 = mTouchCenterX - mCenterX;
+        float dY1 = mTouchCenterY - mCenterY;
+        float dX2 = mCurrentX - mCenterX;
+        float dY2 = mCurrentY - mCenterY;
+
+        float angleA = angleFor(dX1, dY1);
+        float angleB = angleFor(dX2, dY2);
+        return (float) (((angleB - angleA) % 360) * Math.PI / 180);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        mCurrentX = x;
+        mCurrentY = y;
+        mCenterX = getWidth() / 2;
+        mCenterY = getHeight() / 2;
+        mScaleGestureDetector.onTouchEvent(event);
+        if (mInScale) {
+            return true;
+        }
+        switch (event.getActionMasked()) {
+            case (MotionEvent.ACTION_DOWN):
+                mTouchCenterX = x;
+                mTouchCenterY = y;
+                mStartAngle = mTinyPlanetRep.getAngle();
+                break;
+
+            case (MotionEvent.ACTION_MOVE):
+                mTinyPlanetRep.setAngle(mStartAngle + getCurrentTouchAngle());
+                break;
+        }
+        invalidate();
+        mEditorTinyPlanet.commitLocalRepresentation();
+        return true;
+    }
+
+    public void setRepresentation(FilterTinyPlanetRepresentation tinyPlanetRep) {
+        mTinyPlanetRep = tinyPlanetRep;
+    }
+
+    public void setEditor(BasicEditor editorTinyPlanet) {
+        mEditorTinyPlanet = (EditorTinyPlanet) editorTinyPlanet;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        Bitmap bitmap = MasterImage.getImage().getHighresImage();
+        if (bitmap == null) {
+            bitmap = MasterImage.getImage().getFilteredImage();
+        }
+
+        if (bitmap != null) {
+            display(canvas, bitmap);
+        }
+    }
+
+    private void display(Canvas canvas, Bitmap bitmap) {
+        float sw = canvas.getWidth();
+        float sh = canvas.getHeight();
+        float iw = bitmap.getWidth();
+        float ih = bitmap.getHeight();
+        float nsw = sw;
+        float nsh = sh;
+
+        if (sw * ih > sh * iw) {
+            nsw = sh * iw / ih;
+        } else {
+            nsh = sw * ih / iw;
+        }
+
+        mDestRect.left = (sw - nsw) / 2;
+        mDestRect.top = (sh - nsh) / 2;
+        mDestRect.right = sw - mDestRect.left;
+        mDestRect.bottom = sh - mDestRect.top;
+
+        canvas.drawBitmap(bitmap, null, mDestRect, mPaint);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
new file mode 100644
index 0000000..518969e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorVignette;
+import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation;
+
+public class ImageVignette extends ImageShow {
+    private static final String LOGTAG = "ImageVignette";
+
+    private FilterVignetteRepresentation mVignetteRep;
+    private EditorVignette mEditorVignette;
+
+    private int mActiveHandle = -1;
+
+    EclipseControl mElipse;
+
+    public ImageVignette(Context context) {
+        super(context);
+        mElipse = new EclipseControl(context);
+    }
+
+    public ImageVignette(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mElipse = new EclipseControl(context);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int mask = event.getActionMasked();
+        if (mActiveHandle == -1) {
+            if (MotionEvent.ACTION_DOWN != mask) {
+                return super.onTouchEvent(event);
+            }
+            if (event.getPointerCount() == 1) {
+                mActiveHandle = mElipse.getCloseHandle(event.getX(), event.getY());
+            }
+            if (mActiveHandle == -1) {
+                return super.onTouchEvent(event);
+            }
+        } else {
+            switch (mask) {
+                case MotionEvent.ACTION_UP:
+                    mActiveHandle = -1;
+                    break;
+                case MotionEvent.ACTION_DOWN:
+                    break;
+            }
+        }
+        float x = event.getX();
+        float y = event.getY();
+
+        mElipse.setScrToImageMatrix(getScreenToImageMatrix(true));
+
+        boolean didComputeEllipses = false;
+        switch (mask) {
+            case (MotionEvent.ACTION_DOWN):
+                mElipse.actionDown(x, y, mVignetteRep);
+                break;
+            case (MotionEvent.ACTION_UP):
+            case (MotionEvent.ACTION_MOVE):
+                mElipse.actionMove(mActiveHandle, x, y, mVignetteRep);
+                setRepresentation(mVignetteRep);
+                didComputeEllipses = true;
+                break;
+        }
+        if (!didComputeEllipses) {
+            computeEllipses();
+        }
+        invalidate();
+        return true;
+    }
+
+    public void setRepresentation(FilterVignetteRepresentation vignetteRep) {
+        mVignetteRep = vignetteRep;
+        computeEllipses();
+    }
+
+    public void computeEllipses() {
+        if (mVignetteRep == null) {
+            return;
+        }
+        Matrix toImg = getScreenToImageMatrix(false);
+        Matrix toScr = new Matrix();
+        toImg.invert(toScr);
+
+        float[] c = new float[] {
+                mVignetteRep.getCenterX(), mVignetteRep.getCenterY() };
+        if (Float.isNaN(c[0])) {
+            float cx = MasterImage.getImage().getOriginalBounds().width() / 2;
+            float cy = MasterImage.getImage().getOriginalBounds().height() / 2;
+            float rx = Math.min(cx, cy) * .8f;
+            float ry = rx;
+            mVignetteRep.setCenter(cx, cy);
+            mVignetteRep.setRadius(rx, ry);
+
+            c[0] = cx;
+            c[1] = cy;
+            toScr.mapPoints(c);
+            if (getWidth() != 0) {
+                mElipse.setCenter(c[0], c[1]);
+                mElipse.setRadius(c[0] * 0.8f, c[1] * 0.8f);
+            }
+        } else {
+
+            toScr.mapPoints(c);
+
+            mElipse.setCenter(c[0], c[1]);
+            mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()),
+                    toScr.mapRadius(mVignetteRep.getRadiusY()));
+        }
+        mEditorVignette.commitLocalRepresentation();
+    }
+
+    public void setEditor(EditorVignette editorVignette) {
+        mEditorVignette = editorVignette;
+    }
+
+    @Override
+    public void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w,  h, oldw, oldh);
+        computeEllipses();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mVignetteRep == null) {
+            return;
+        }
+        Matrix toImg = getScreenToImageMatrix(false);
+        Matrix toScr = new Matrix();
+        toImg.invert(toScr);
+        float[] c = new float[] {
+                mVignetteRep.getCenterX(), mVignetteRep.getCenterY() };
+        toScr.mapPoints(c);
+        mElipse.setCenter(c[0], c[1]);
+        mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()),
+                toScr.mapRadius(mVignetteRep.getRadiusY()));
+
+        mElipse.draw(canvas);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/Line.java b/src/com/android/gallery3d/filtershow/imageshow/Line.java
new file mode 100644
index 0000000..a767bd8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/Line.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+public interface Line {
+    void setPoint1(float x, float y);
+    void setPoint2(float x, float y);
+    float getPoint1X();
+    float getPoint1Y();
+    float getPoint2X();
+    float getPoint2Y();
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
new file mode 100644
index 0000000..92e57bf
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.imageshow;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.history.HistoryItem;
+import com.android.gallery3d.filtershow.history.HistoryManager;
+import com.android.gallery3d.filtershow.pipeline.Buffer;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+import com.android.gallery3d.filtershow.pipeline.SharedBuffer;
+import com.android.gallery3d.filtershow.pipeline.SharedPreset;
+import com.android.gallery3d.filtershow.state.StateAdapter;
+
+import java.util.Vector;
+
+public class MasterImage implements RenderingRequestCaller {
+
+    private static final String LOGTAG = "MasterImage";
+    private boolean DEBUG  = false;
+    private static final boolean DISABLEZOOM = false;
+    public static final int SMALL_BITMAP_DIM = 160;
+    public static final int MAX_BITMAP_DIM = 900;
+    private static MasterImage sMasterImage = null;
+
+    private boolean mSupportsHighRes = false;
+
+    private ImageFilter mCurrentFilter = null;
+    private ImagePreset mPreset = null;
+    private ImagePreset mLoadedPreset = null;
+    private ImagePreset mGeometryOnlyPreset = null;
+    private ImagePreset mFiltersOnlyPreset = null;
+
+    private SharedBuffer mPreviewBuffer = new SharedBuffer();
+    private SharedPreset mPreviewPreset = new SharedPreset();
+
+    private Bitmap mOriginalBitmapSmall = null;
+    private Bitmap mOriginalBitmapLarge = null;
+    private Bitmap mOriginalBitmapHighres = null;
+    private int mOrientation;
+    private Rect mOriginalBounds;
+    private final Vector<ImageShow> mLoadListeners = new Vector<ImageShow>();
+    private Uri mUri = null;
+    private int mZoomOrientation = ImageLoader.ORI_NORMAL;
+
+    private Bitmap mGeometryOnlyBitmap = null;
+    private Bitmap mFiltersOnlyBitmap = null;
+    private Bitmap mPartialBitmap = null;
+    private Bitmap mHighresBitmap = null;
+
+    private HistoryManager mHistory = null;
+    private StateAdapter mState = null;
+
+    private FilterShowActivity mActivity = null;
+
+    private Vector<ImageShow> mObservers = new Vector<ImageShow>();
+    private FilterRepresentation mCurrentFilterRepresentation;
+
+    private float mScaleFactor = 1.0f;
+    private float mMaxScaleFactor = 3.0f; // TODO: base this on the current view / image
+    private Point mTranslation = new Point();
+    private Point mOriginalTranslation = new Point();
+
+    private Point mImageShowSize = new Point();
+
+    private boolean mShowsOriginal;
+
+    private MasterImage() {
+    }
+
+    // TODO: remove singleton
+    public static void setMaster(MasterImage master) {
+        sMasterImage = master;
+    }
+
+    public static MasterImage getImage() {
+        if (sMasterImage == null) {
+            sMasterImage = new MasterImage();
+        }
+        return sMasterImage;
+    }
+
+    public Bitmap getOriginalBitmapSmall() {
+        return mOriginalBitmapSmall;
+    }
+
+    public Bitmap getOriginalBitmapLarge() {
+        return mOriginalBitmapLarge;
+    }
+
+    public Bitmap getOriginalBitmapHighres() {
+        return mOriginalBitmapHighres;
+    }
+
+    public void setOriginalBitmapHighres(Bitmap mOriginalBitmapHighres) {
+        this.mOriginalBitmapHighres = mOriginalBitmapHighres;
+    }
+
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    public Rect getOriginalBounds() {
+        return mOriginalBounds;
+    }
+
+    public void setOriginalBounds(Rect r) {
+        mOriginalBounds = r;
+    }
+
+    public Uri getUri() {
+        return mUri;
+    }
+
+    public void setUri(Uri uri) {
+        mUri = uri;
+    }
+
+    public int getZoomOrientation() {
+        return mZoomOrientation;
+    }
+
+    public void addListener(ImageShow imageShow) {
+        if (!mLoadListeners.contains(imageShow)) {
+            mLoadListeners.add(imageShow);
+        }
+    }
+
+    public void warnListeners() {
+        mActivity.runOnUiThread(mWarnListenersRunnable);
+    }
+
+    private Runnable mWarnListenersRunnable = new Runnable() {
+        @Override
+        public void run() {
+            for (int i = 0; i < mLoadListeners.size(); i++) {
+                ImageShow imageShow = mLoadListeners.elementAt(i);
+                imageShow.imageLoaded();
+            }
+            invalidatePreview();
+        }
+    };
+
+    public boolean loadBitmap(Uri uri, int size) {
+        setUri(uri);
+        mOrientation = ImageLoader.getMetadataOrientation(mActivity, uri);
+        Rect originalBounds = new Rect();
+        mOriginalBitmapLarge = ImageLoader.loadOrientedConstrainedBitmap(uri, mActivity,
+                Math.min(MAX_BITMAP_DIM, size),
+                mOrientation, originalBounds);
+        setOriginalBounds(originalBounds);
+        if (mOriginalBitmapLarge == null) {
+            return false;
+        }
+        int sw = SMALL_BITMAP_DIM;
+        int sh = (int) (sw * (float) mOriginalBitmapLarge.getHeight() / mOriginalBitmapLarge
+                .getWidth());
+        mOriginalBitmapSmall = Bitmap.createScaledBitmap(mOriginalBitmapLarge, sw, sh, true);
+        mZoomOrientation = mOrientation;
+        warnListeners();
+        return true;
+    }
+
+    public void setSupportsHighRes(boolean value) {
+        mSupportsHighRes = value;
+    }
+
+    public void addObserver(ImageShow observer) {
+        if (mObservers.contains(observer)) {
+            return;
+        }
+        mObservers.add(observer);
+    }
+
+    public void setActivity(FilterShowActivity activity) {
+        mActivity = activity;
+    }
+
+    public FilterShowActivity getActivity() {
+        return mActivity;
+    }
+
+    public synchronized ImagePreset getPreset() {
+        return mPreset;
+    }
+
+    public synchronized ImagePreset getGeometryPreset() {
+        return mGeometryOnlyPreset;
+    }
+
+    public synchronized ImagePreset getFiltersOnlyPreset() {
+        return mFiltersOnlyPreset;
+    }
+
+    public synchronized void setPreset(ImagePreset preset,
+                                       FilterRepresentation change,
+                                       boolean addToHistory) {
+        if (DEBUG) {
+            preset.showFilters();
+        }
+        mPreset = preset;
+        mPreset.fillImageStateAdapter(mState);
+        if (addToHistory) {
+            HistoryItem historyItem = new HistoryItem(mPreset, change);
+            mHistory.addHistoryItem(historyItem);
+        }
+        updatePresets(true);
+        mActivity.updateCategories();
+    }
+
+    public void onHistoryItemClick(int position) {
+        HistoryItem historyItem = mHistory.getItem(position);
+        // We need a copy from the history
+        ImagePreset newPreset = new ImagePreset(historyItem.getImagePreset());
+        // don't need to add it to the history
+        setPreset(newPreset, historyItem.getFilterRepresentation(), false);
+        mHistory.setCurrentPreset(position);
+    }
+
+    public HistoryManager getHistory() {
+        return mHistory;
+    }
+
+    public StateAdapter getState() {
+        return mState;
+    }
+
+    public void setHistoryManager(HistoryManager adapter) {
+        mHistory = adapter;
+    }
+
+    public void setStateAdapter(StateAdapter adapter) {
+        mState = adapter;
+    }
+
+    public void setCurrentFilter(ImageFilter filter) {
+        mCurrentFilter = filter;
+    }
+
+    public ImageFilter getCurrentFilter() {
+        return mCurrentFilter;
+    }
+
+    public synchronized boolean hasModifications() {
+        // TODO: We need to have a better same effects check to see if two
+        // presets are functionally the same. Right now, we are relying on a
+        // stricter check as equals().
+        ImagePreset loadedPreset = getLoadedPreset();
+        if (mPreset == null) {
+            if (loadedPreset == null) {
+                return false;
+            } else {
+                return loadedPreset.hasModifications();
+            }
+        } else {
+            if (loadedPreset == null) {
+                return mPreset.hasModifications();
+            } else {
+                return !mPreset.equals(loadedPreset);
+            }
+        }
+    }
+
+    public SharedBuffer getPreviewBuffer() {
+        return mPreviewBuffer;
+    }
+
+    public SharedPreset getPreviewPreset() {
+        return mPreviewPreset;
+    }
+
+    public Bitmap getFilteredImage() {
+        mPreviewBuffer.swapConsumerIfNeeded(); // get latest bitmap
+        Buffer consumer = mPreviewBuffer.getConsumer();
+        if (consumer != null) {
+            return consumer.getBitmap();
+        }
+        return null;
+    }
+
+    public Bitmap getFiltersOnlyImage() {
+        return mFiltersOnlyBitmap;
+    }
+
+    public Bitmap getGeometryOnlyImage() {
+        return mGeometryOnlyBitmap;
+    }
+
+    public Bitmap getPartialImage() {
+        return mPartialBitmap;
+    }
+
+    public Bitmap getHighresImage() {
+        return mHighresBitmap;
+    }
+
+    public void notifyObservers() {
+        for (ImageShow observer : mObservers) {
+            observer.invalidate();
+        }
+    }
+
+    public void updatePresets(boolean force) {
+        if (force || mGeometryOnlyPreset == null) {
+            ImagePreset newPreset = new ImagePreset(mPreset);
+            newPreset.setDoApplyFilters(false);
+            newPreset.setDoApplyGeometry(true);
+            if (force || mGeometryOnlyPreset == null
+                    || !newPreset.same(mGeometryOnlyPreset)) {
+                mGeometryOnlyPreset = newPreset;
+                RenderingRequest.post(mActivity, getOriginalBitmapLarge(),
+                        mGeometryOnlyPreset, RenderingRequest.GEOMETRY_RENDERING, this);
+            }
+        }
+        if (force || mFiltersOnlyPreset == null) {
+            ImagePreset newPreset = new ImagePreset(mPreset);
+            newPreset.setDoApplyFilters(true);
+            newPreset.setDoApplyGeometry(false);
+            if (force || mFiltersOnlyPreset == null
+                    || !newPreset.same(mFiltersOnlyPreset)) {
+                mFiltersOnlyPreset = newPreset;
+                RenderingRequest.post(mActivity, MasterImage.getImage().getOriginalBitmapLarge(),
+                        mFiltersOnlyPreset, RenderingRequest.FILTERS_RENDERING, this);
+            }
+        }
+        invalidatePreview();
+    }
+
+    public FilterRepresentation getCurrentFilterRepresentation() {
+        return mCurrentFilterRepresentation;
+    }
+
+    public void setCurrentFilterRepresentation(FilterRepresentation currentFilterRepresentation) {
+        mCurrentFilterRepresentation = currentFilterRepresentation;
+    }
+
+    public void invalidateFiltersOnly() {
+        mFiltersOnlyPreset = null;
+        updatePresets(false);
+    }
+
+    public void invalidatePartialPreview() {
+        if (mPartialBitmap != null) {
+            mPartialBitmap = null;
+            notifyObservers();
+        }
+    }
+
+    public void invalidateHighresPreview() {
+        if (mHighresBitmap != null) {
+            mHighresBitmap = null;
+            notifyObservers();
+        }
+    }
+
+    public void invalidatePreview() {
+        mPreviewPreset.enqueuePreset(mPreset);
+        mPreviewBuffer.invalidate();
+        invalidatePartialPreview();
+        invalidateHighresPreview();
+        needsUpdatePartialPreview();
+        needsUpdateHighResPreview();
+        mActivity.getProcessingService().updatePreviewBuffer();
+    }
+
+    public void setImageShowSize(int w, int h) {
+        if (mImageShowSize.x != w || mImageShowSize.y != h) {
+            mImageShowSize.set(w, h);
+            needsUpdatePartialPreview();
+            needsUpdateHighResPreview();
+        }
+    }
+
+    private Matrix getImageToScreenMatrix(boolean reflectRotation) {
+        if (getOriginalBounds() == null || mImageShowSize.x == 0 || mImageShowSize.y == 0) {
+            return new Matrix();
+        }
+        Matrix m = GeometryMathUtils.getImageToScreenMatrix(mPreset.getGeometryFilters(),
+                reflectRotation, getOriginalBounds(), mImageShowSize.x, mImageShowSize.y);
+        if (m == null) {
+            m = new Matrix();
+            m.reset();
+            return m;
+        }
+        Point translate = getTranslation();
+        float scaleFactor = getScaleFactor();
+        m.postTranslate(translate.x, translate.y);
+        m.postScale(scaleFactor, scaleFactor, mImageShowSize.x / 2.0f, mImageShowSize.y / 2.0f);
+        return m;
+    }
+
+    private Matrix getScreenToImageMatrix(boolean reflectRotation) {
+        Matrix m = getImageToScreenMatrix(reflectRotation);
+        Matrix invert = new Matrix();
+        m.invert(invert);
+        return invert;
+    }
+
+    public void needsUpdateHighResPreview() {
+        if (!mSupportsHighRes) {
+            return;
+        }
+        if (mActivity.getProcessingService() == null) {
+            return;
+        }
+        mActivity.getProcessingService().postHighresRenderingRequest(mPreset,
+                getScaleFactor(), this);
+        invalidateHighresPreview();
+    }
+
+    public void needsUpdatePartialPreview() {
+        if (mPreset == null) {
+            return;
+        }
+        if (!mPreset.canDoPartialRendering()) {
+            invalidatePartialPreview();
+            return;
+        }
+        Matrix m = getScreenToImageMatrix(true);
+        RectF r = new RectF(0, 0, mImageShowSize.x, mImageShowSize.y);
+        RectF dest = new RectF();
+        m.mapRect(dest, r);
+        Rect bounds = new Rect();
+        dest.roundOut(bounds);
+        RenderingRequest.post(mActivity, null, mPreset, RenderingRequest.PARTIAL_RENDERING,
+                this, bounds, new Rect(0, 0, mImageShowSize.x, mImageShowSize.y));
+        invalidatePartialPreview();
+    }
+
+    @Override
+    public void available(RenderingRequest request) {
+        if (request.getBitmap() == null) {
+            return;
+        }
+
+        boolean needsCheckModification = false;
+        if (request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
+            mGeometryOnlyBitmap = request.getBitmap();
+            needsCheckModification = true;
+        }
+        if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
+            mFiltersOnlyBitmap = request.getBitmap();
+            notifyObservers();
+            needsCheckModification = true;
+        }
+        if (request.getType() == RenderingRequest.PARTIAL_RENDERING
+                && request.getScaleFactor() == getScaleFactor()) {
+            mPartialBitmap = request.getBitmap();
+            notifyObservers();
+            needsCheckModification = true;
+        }
+        if (request.getType() == RenderingRequest.HIGHRES_RENDERING) {
+            mHighresBitmap = request.getBitmap();
+            notifyObservers();
+            needsCheckModification = true;
+        }
+        if (needsCheckModification) {
+            mActivity.enableSave(hasModifications());
+        }
+    }
+
+    public static void reset() {
+        sMasterImage = null;
+    }
+
+    public float getScaleFactor() {
+        return mScaleFactor;
+    }
+
+    public void setScaleFactor(float scaleFactor) {
+        if (DISABLEZOOM) {
+            return;
+        }
+        if (scaleFactor == mScaleFactor) {
+            return;
+        }
+        mScaleFactor = scaleFactor;
+        invalidatePartialPreview();
+    }
+
+    public Point getTranslation() {
+        return mTranslation;
+    }
+
+    public void setTranslation(Point translation) {
+        if (DISABLEZOOM) {
+            mTranslation.x = 0;
+            mTranslation.y = 0;
+            return;
+        }
+        mTranslation.x = translation.x;
+        mTranslation.y = translation.y;
+        needsUpdatePartialPreview();
+    }
+
+    public Point getOriginalTranslation() {
+        return mOriginalTranslation;
+    }
+
+    public void setOriginalTranslation(Point originalTranslation) {
+        if (DISABLEZOOM) {
+            return;
+        }
+        mOriginalTranslation.x = originalTranslation.x;
+        mOriginalTranslation.y = originalTranslation.y;
+    }
+
+    public void resetTranslation() {
+        mTranslation.x = 0;
+        mTranslation.y = 0;
+        needsUpdatePartialPreview();
+    }
+
+    public Bitmap getThumbnailBitmap() {
+        return getOriginalBitmapSmall();
+    }
+
+    public Bitmap getLargeThumbnailBitmap() {
+        return getOriginalBitmapLarge();
+    }
+
+    public float getMaxScaleFactor() {
+        if (DISABLEZOOM) {
+            return 1;
+        }
+        return mMaxScaleFactor;
+    }
+
+    public void setMaxScaleFactor(float maxScaleFactor) {
+        mMaxScaleFactor = maxScaleFactor;
+    }
+
+    public boolean supportsHighRes() {
+        return mSupportsHighRes;
+    }
+
+    public void setShowsOriginal(boolean value) {
+        mShowsOriginal = value;
+        notifyObservers();
+    }
+
+    public boolean showsOriginal() {
+        return mShowsOriginal;
+    }
+
+    public void setLoadedPreset(ImagePreset preset) {
+        mLoadedPreset = preset;
+    }
+
+    public ImagePreset getLoadedPreset() {
+        return mLoadedPreset;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/Oval.java b/src/com/android/gallery3d/filtershow/imageshow/Oval.java
new file mode 100644
index 0000000..28f278f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/Oval.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+public interface Oval {
+    void setCenter(float x, float y);
+    void setRadius(float w, float h);
+    float getCenterX();
+    float getCenterY();
+    float getRadiusX();
+    float getRadiusY();
+    void setRadiusY(float y);
+    void setRadiusX(float x);
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/Spline.java b/src/com/android/gallery3d/filtershow/imageshow/Spline.java
new file mode 100644
index 0000000..3c27a4d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/Spline.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.imageshow;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.Vector;
+
+public class Spline {
+    private final Vector<ControlPoint> mPoints;
+    private static Drawable mCurveHandle;
+    private static int mCurveHandleSize;
+    private static int mCurveWidth;
+
+    public static final int RGB = 0;
+    public static final int RED = 1;
+    public static final int GREEN = 2;
+    public static final int BLUE = 3;
+    private static final String LOGTAG = "Spline";
+
+    private final Paint gPaint = new Paint();
+    private ControlPoint mCurrentControlPoint = null;
+
+    public Spline() {
+        mPoints = new Vector<ControlPoint>();
+    }
+
+    public Spline(Spline spline) {
+        mPoints = new Vector<ControlPoint>();
+        for (int i = 0; i < spline.mPoints.size(); i++) {
+            ControlPoint p = spline.mPoints.elementAt(i);
+            ControlPoint newPoint = new ControlPoint(p);
+            mPoints.add(newPoint);
+            if (spline.mCurrentControlPoint == p) {
+                mCurrentControlPoint = newPoint;
+            }
+        }
+        Collections.sort(mPoints);
+    }
+
+    public static void setCurveHandle(Drawable drawable, int size) {
+        mCurveHandle = drawable;
+        mCurveHandleSize = size;
+    }
+
+    public static void setCurveWidth(int width) {
+        mCurveWidth = width;
+    }
+
+    public static int curveHandleSize() {
+        return mCurveHandleSize;
+    }
+
+    public static int colorForCurve(int curveIndex) {
+        switch (curveIndex) {
+            case Spline.RED:
+                return Color.RED;
+            case GREEN:
+                return Color.GREEN;
+            case BLUE:
+                return Color.BLUE;
+        }
+        return Color.WHITE;
+    }
+
+    public boolean sameValues(Spline other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null) {
+            return false;
+        }
+
+        if (getNbPoints() != other.getNbPoints()) {
+            return false;
+        }
+
+        for (int i = 0; i < getNbPoints(); i++) {
+            ControlPoint p = mPoints.elementAt(i);
+            ControlPoint otherPoint = other.mPoints.elementAt(i);
+            if (!p.sameValues(otherPoint)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void didMovePoint(ControlPoint point) {
+        mCurrentControlPoint = point;
+    }
+
+    public void movePoint(int pick, float x, float y) {
+        if (pick < 0 || pick > mPoints.size() - 1) {
+            return;
+        }
+        ControlPoint point = mPoints.elementAt(pick);
+        point.x = x;
+        point.y = y;
+        didMovePoint(point);
+    }
+
+    public boolean isOriginal() {
+        if (this.getNbPoints() != 2) {
+            return false;
+        }
+        if (mPoints.elementAt(0).x != 0 || mPoints.elementAt(0).y != 1) {
+            return false;
+        }
+        if (mPoints.elementAt(1).x != 1 || mPoints.elementAt(1).y != 0) {
+            return false;
+        }
+        return true;
+    }
+
+    public void reset() {
+        mPoints.clear();
+        addPoint(0.0f, 1.0f);
+        addPoint(1.0f, 0.0f);
+    }
+
+    private void drawHandles(Canvas canvas, Drawable indicator, float centerX, float centerY) {
+        int left = (int) centerX - mCurveHandleSize / 2;
+        int top = (int) centerY - mCurveHandleSize / 2;
+        indicator.setBounds(left, top, left + mCurveHandleSize, top + mCurveHandleSize);
+        indicator.draw(canvas);
+    }
+
+    public float[] getAppliedCurve() {
+        float[] curve = new float[256];
+        ControlPoint[] points = new ControlPoint[mPoints.size()];
+        for (int i = 0; i < mPoints.size(); i++) {
+            ControlPoint p = mPoints.get(i);
+            points[i] = new ControlPoint(p.x, p.y);
+        }
+        double[] derivatives = solveSystem(points);
+        int start = 0;
+        int end = 256;
+        if (points[0].x != 0) {
+            start = (int) (points[0].x * 256);
+        }
+        if (points[points.length - 1].x != 1) {
+            end = (int) (points[points.length - 1].x * 256);
+        }
+        for (int i = 0; i < start; i++) {
+            curve[i] = 1.0f - points[0].y;
+        }
+        for (int i = end; i < 256; i++) {
+            curve[i] = 1.0f - points[points.length - 1].y;
+        }
+        for (int i = start; i < end; i++) {
+            ControlPoint cur = null;
+            ControlPoint next = null;
+            double x = i / 256.0;
+            int pivot = 0;
+            for (int j = 0; j < points.length - 1; j++) {
+                if (x >= points[j].x && x <= points[j + 1].x) {
+                    pivot = j;
+                }
+            }
+            cur = points[pivot];
+            next = points[pivot + 1];
+            if (x <= next.x) {
+                double x1 = cur.x;
+                double x2 = next.x;
+                double y1 = cur.y;
+                double y2 = next.y;
+
+                // Use the second derivatives to apply the cubic spline
+                // equation:
+                double delta = (x2 - x1);
+                double delta2 = delta * delta;
+                double b = (x - x1) / delta;
+                double a = 1 - b;
+                double ta = a * y1;
+                double tb = b * y2;
+                double tc = (a * a * a - a) * derivatives[pivot];
+                double td = (b * b * b - b) * derivatives[pivot + 1];
+                double y = ta + tb + (delta2 / 6) * (tc + td);
+                if (y > 1.0f) {
+                    y = 1.0f;
+                }
+                if (y < 0) {
+                    y = 0;
+                }
+                curve[i] = (float) (1.0f - y);
+            } else {
+                curve[i] = 1.0f - next.y;
+            }
+        }
+        return curve;
+    }
+
+    private void drawGrid(Canvas canvas, float w, float h) {
+        // Grid
+        gPaint.setARGB(128, 150, 150, 150);
+        gPaint.setStrokeWidth(1);
+
+        float stepH = h / 9;
+        float stepW = w / 9;
+
+        // central diagonal
+        gPaint.setARGB(255, 100, 100, 100);
+        gPaint.setStrokeWidth(2);
+        canvas.drawLine(0, h, w, 0, gPaint);
+
+        gPaint.setARGB(128, 200, 200, 200);
+        gPaint.setStrokeWidth(4);
+        stepH = h / 3;
+        stepW = w / 3;
+        for (int j = 1; j < 3; j++) {
+            canvas.drawLine(0, j * stepH, w, j * stepH, gPaint);
+            canvas.drawLine(j * stepW, 0, j * stepW, h, gPaint);
+        }
+        canvas.drawLine(0, 0, 0, h, gPaint);
+        canvas.drawLine(w, 0, w, h, gPaint);
+        canvas.drawLine(0, 0, w, 0, gPaint);
+        canvas.drawLine(0, h, w, h, gPaint);
+    }
+
+    public void draw(Canvas canvas, int color, int canvasWidth, int canvasHeight,
+            boolean showHandles, boolean moving) {
+        float w = canvasWidth - mCurveHandleSize;
+        float h = canvasHeight - mCurveHandleSize;
+        float dx = mCurveHandleSize / 2;
+        float dy = mCurveHandleSize / 2;
+
+        // The cubic spline equation is (from numerical recipes in C):
+        // y = a(y_i) + b(y_i+1) + c(y"_i) + d(y"_i+1)
+        //
+        // with c(y"_i) and d(y"_i+1):
+        // c(y"_i) = 1/6 (a^3 - a) delta^2 (y"_i)
+        // d(y"_i_+1) = 1/6 (b^3 - b) delta^2 (y"_i+1)
+        //
+        // and delta:
+        // delta = x_i+1 - x_i
+        //
+        // To find the second derivatives y", we can rearrange the equation as:
+        // A(y"_i-1) + B(y"_i) + C(y"_i+1) = D
+        //
+        // With the coefficients A, B, C, D:
+        // A = 1/6 (x_i - x_i-1)
+        // B = 1/3 (x_i+1 - x_i-1)
+        // C = 1/6 (x_i+1 - x_i)
+        // D = (y_i+1 - y_i)/(x_i+1 - x_i) - (y_i - y_i-1)/(x_i - x_i-1)
+        //
+        // We can now easily solve the equation to find the second derivatives:
+        ControlPoint[] points = new ControlPoint[mPoints.size()];
+        for (int i = 0; i < mPoints.size(); i++) {
+            ControlPoint p = mPoints.get(i);
+            points[i] = new ControlPoint(p.x * w, p.y * h);
+        }
+        double[] derivatives = solveSystem(points);
+
+        Path path = new Path();
+        path.moveTo(0, points[0].y);
+        for (int i = 0; i < points.length - 1; i++) {
+            double x1 = points[i].x;
+            double x2 = points[i + 1].x;
+            double y1 = points[i].y;
+            double y2 = points[i + 1].y;
+
+            for (double x = x1; x < x2; x += 20) {
+                // Use the second derivatives to apply the cubic spline
+                // equation:
+                double delta = (x2 - x1);
+                double delta2 = delta * delta;
+                double b = (x - x1) / delta;
+                double a = 1 - b;
+                double ta = a * y1;
+                double tb = b * y2;
+                double tc = (a * a * a - a) * derivatives[i];
+                double td = (b * b * b - b) * derivatives[i + 1];
+                double y = ta + tb + (delta2 / 6) * (tc + td);
+                if (y > h) {
+                    y = h;
+                }
+                if (y < 0) {
+                    y = 0;
+                }
+                path.lineTo((float) x, (float) y);
+            }
+        }
+        canvas.save();
+        canvas.translate(dx, dy);
+        drawGrid(canvas, w, h);
+        ControlPoint lastPoint = points[points.length - 1];
+        path.lineTo(lastPoint.x, lastPoint.y);
+        path.lineTo(w, lastPoint.y);
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setFilterBitmap(true);
+        paint.setDither(true);
+        paint.setStyle(Paint.Style.STROKE);
+        int curveWidth = mCurveWidth;
+        if (showHandles) {
+            curveWidth *= 1.5;
+        }
+        paint.setStrokeWidth(curveWidth + 2);
+        paint.setColor(Color.BLACK);
+        canvas.drawPath(path, paint);
+
+        if (moving && mCurrentControlPoint != null) {
+            float px = mCurrentControlPoint.x * w;
+            float py = mCurrentControlPoint.y * h;
+            paint.setStrokeWidth(3);
+            paint.setColor(Color.BLACK);
+            canvas.drawLine(px, py, px, h, paint);
+            canvas.drawLine(0, py, px, py, paint);
+            paint.setStrokeWidth(1);
+            paint.setColor(color);
+            canvas.drawLine(px, py, px, h, paint);
+            canvas.drawLine(0, py, px, py, paint);
+        }
+
+        paint.setStrokeWidth(curveWidth);
+        paint.setColor(color);
+        canvas.drawPath(path, paint);
+        if (showHandles) {
+            for (int i = 0; i < points.length; i++) {
+                float x = points[i].x;
+                float y = points[i].y;
+                drawHandles(canvas, mCurveHandle, x, y);
+            }
+        }
+        canvas.restore();
+    }
+
+    double[] solveSystem(ControlPoint[] points) {
+        int n = points.length;
+        double[][] system = new double[n][3];
+        double[] result = new double[n]; // d
+        double[] solution = new double[n]; // returned coefficients
+        system[0][1] = 1;
+        system[n - 1][1] = 1;
+        double d6 = 1.0 / 6.0;
+        double d3 = 1.0 / 3.0;
+
+        // let's create a tridiagonal matrix representing the
+        // system, and apply the TDMA algorithm to solve it
+        // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
+        for (int i = 1; i < n - 1; i++) {
+            double deltaPrevX = points[i].x - points[i - 1].x;
+            double deltaX = points[i + 1].x - points[i - 1].x;
+            double deltaNextX = points[i + 1].x - points[i].x;
+            double deltaNextY = points[i + 1].y - points[i].y;
+            double deltaPrevY = points[i].y - points[i - 1].y;
+            system[i][0] = d6 * deltaPrevX; // a_i
+            system[i][1] = d3 * deltaX; // b_i
+            system[i][2] = d6 * deltaNextX; // c_i
+            result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i
+        }
+
+        // Forward sweep
+        for (int i = 1; i < n; i++) {
+            // m = a_i/b_i-1
+            double m = system[i][0] / system[i - 1][1];
+            // b_i = b_i - m(c_i-1)
+            system[i][1] = system[i][1] - m * system[i - 1][2];
+            // d_i = d_i - m(d_i-1)
+            result[i] = result[i] - m * result[i - 1];
+        }
+
+        // Back substitution
+        solution[n - 1] = result[n - 1] / system[n - 1][1];
+        for (int i = n - 2; i >= 0; --i) {
+            solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1];
+        }
+        return solution;
+    }
+
+    public int addPoint(float x, float y) {
+        return addPoint(new ControlPoint(x, y));
+    }
+
+    public int addPoint(ControlPoint v) {
+        mPoints.add(v);
+        Collections.sort(mPoints);
+        return mPoints.indexOf(v);
+    }
+
+    public void deletePoint(int n) {
+        mPoints.remove(n);
+        if (mPoints.size() < 2) {
+            reset();
+        }
+        Collections.sort(mPoints);
+    }
+
+    public int getNbPoints() {
+        return mPoints.size();
+    }
+
+    public ControlPoint getPoint(int n) {
+        return mPoints.elementAt(n);
+    }
+
+    public boolean isPointContained(float x, int n) {
+        for (int i = 0; i < n; i++) {
+            ControlPoint point = mPoints.elementAt(i);
+            if (point.x > x) {
+                return false;
+            }
+        }
+        for (int i = n + 1; i < mPoints.size(); i++) {
+            ControlPoint point = mPoints.elementAt(i);
+            if (point.x < x) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public Spline copy() {
+        Spline spline = new Spline();
+        for (int i = 0; i < mPoints.size(); i++) {
+            ControlPoint point = mPoints.elementAt(i);
+            spline.addPoint(point.copy());
+        }
+        return spline;
+    }
+
+    public void show() {
+        Log.v(LOGTAG, "show curve " + this);
+        for (int i = 0; i < mPoints.size(); i++) {
+            ControlPoint point = mPoints.elementAt(i);
+            Log.v(LOGTAG, "point " + i + " is (" + point.x + ", " + point.y + ")");
+        }
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/Buffer.java b/src/com/android/gallery3d/filtershow/pipeline/Buffer.java
new file mode 100644
index 0000000..7444512
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/Buffer.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.RenderScript;
+
+public class Buffer {
+    private static final String LOGTAG = "Buffer";
+    private Bitmap mBitmap;
+    private Allocation mAllocation;
+    private boolean mUseAllocation = false;
+    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
+    private ImagePreset mPreset;
+
+    public Buffer(Bitmap bitmap) {
+        RenderScript rs = CachingPipeline.getRenderScriptContext();
+        if (bitmap != null) {
+            mBitmap = bitmap.copy(BITMAP_CONFIG, true);
+        }
+        if (mUseAllocation) {
+            // TODO: recreate the allocation when the RS context changes
+            mAllocation = Allocation.createFromBitmap(rs, mBitmap,
+                    Allocation.MipmapControl.MIPMAP_NONE,
+                    Allocation.USAGE_SHARED | Allocation.USAGE_SCRIPT);
+        }
+    }
+
+    public void setBitmap(Bitmap bitmap) {
+        mBitmap = bitmap.copy(BITMAP_CONFIG, true);
+    }
+
+    public Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    public Allocation getAllocation() {
+        return mAllocation;
+    }
+
+    public void sync() {
+        if (mUseAllocation) {
+            mAllocation.copyTo(mBitmap);
+        }
+    }
+
+    public ImagePreset getPreset() {
+        return mPreset;
+    }
+
+    public void setPreset(ImagePreset preset) {
+        if ((mPreset == null) || (!mPreset.same(preset))) {
+            mPreset = new ImagePreset(preset);
+        } else {
+            mPreset.updateWith(preset);
+        }
+    }
+}
+
diff --git a/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java b/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java
new file mode 100644
index 0000000..e0269e9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+import java.util.Vector;
+
+public class CacheProcessing {
+    private static final String LOGTAG = "CacheProcessing";
+    private static final boolean DEBUG = false;
+    private Vector<CacheStep> mSteps = new Vector<CacheStep>();
+
+    static class CacheStep {
+        FilterRepresentation representation;
+        Bitmap cache;
+    }
+
+    public Bitmap process(Bitmap originalBitmap,
+                          Vector<FilterRepresentation> filters,
+                          FilterEnvironment environment) {
+
+        if (filters.size() == 0) {
+            return originalBitmap;
+        }
+
+        // New set of filters, let's clear the cache and rebuild it.
+        if (filters.size() != mSteps.size()) {
+            mSteps.clear();
+            for (int i = 0; i < filters.size(); i++) {
+                FilterRepresentation representation = filters.elementAt(i);
+                CacheStep step = new CacheStep();
+                step.representation = representation.copy();
+                mSteps.add(step);
+            }
+        }
+
+        if (DEBUG) {
+            displayFilters(filters);
+        }
+
+        // First, let's find how similar we are in our cache
+        // compared to the current list of filters
+        int similarUpToIndex = -1;
+        for (int i = 0; i < filters.size(); i++) {
+            FilterRepresentation representation = filters.elementAt(i);
+            CacheStep step = mSteps.elementAt(i);
+            boolean similar = step.representation.equals(representation);
+            if (similar) {
+                similarUpToIndex = i;
+            } else {
+                break;
+            }
+        }
+        if (DEBUG) {
+            Log.v(LOGTAG, "similar up to index " + similarUpToIndex);
+        }
+
+        // Now, let's get the earliest cached result in our pipeline
+        Bitmap cacheBitmap = null;
+        int findBaseImageIndex = similarUpToIndex;
+        if (findBaseImageIndex > -1) {
+            while (findBaseImageIndex > 0
+                    && mSteps.elementAt(findBaseImageIndex).cache == null) {
+                findBaseImageIndex--;
+            }
+            cacheBitmap = mSteps.elementAt(findBaseImageIndex).cache;
+        }
+        boolean emptyStack = false;
+        if (cacheBitmap == null) {
+            emptyStack = true;
+            // Damn, it's an empty stack, we have to start from scratch
+            // TODO: use a bitmap cache + RS allocation instead of Bitmap.copy()
+            cacheBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true);
+            if (findBaseImageIndex > -1) {
+                FilterRepresentation representation = filters.elementAt(findBaseImageIndex);
+                if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) {
+                    cacheBitmap = environment.applyRepresentation(representation, cacheBitmap);
+                }
+                mSteps.elementAt(findBaseImageIndex).representation = representation.copy();
+                mSteps.elementAt(findBaseImageIndex).cache = cacheBitmap;
+            }
+            if (DEBUG) {
+                Log.v(LOGTAG, "empty stack");
+            }
+        }
+
+        // Ok, so sadly the earliest cached result is before the index we want.
+        // We have to rebuild a new result for this position, and then cache it.
+        if (findBaseImageIndex != similarUpToIndex) {
+            if (DEBUG) {
+                Log.v(LOGTAG, "rebuild cacheBitmap from " + findBaseImageIndex
+                        + " to " + similarUpToIndex);
+            }
+            // rebuild the cache image for this step
+            if (!emptyStack) {
+                cacheBitmap = cacheBitmap.copy(Bitmap.Config.ARGB_8888, true);
+            } else {
+                // if it was an empty stack, we already applied it
+                findBaseImageIndex ++;
+            }
+            for (int i = findBaseImageIndex; i <= similarUpToIndex; i++) {
+                FilterRepresentation representation = filters.elementAt(i);
+                if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) {
+                    cacheBitmap = environment.applyRepresentation(representation, cacheBitmap);
+                }
+                if (DEBUG) {
+                    Log.v(LOGTAG, " - " + i  + " => apply " + representation.getName());
+                }
+            }
+            // Let's cache it!
+            mSteps.elementAt(similarUpToIndex).cache = cacheBitmap;
+        }
+
+        if (DEBUG) {
+            Log.v(LOGTAG, "process pipeline from " + similarUpToIndex
+                    + " to " + (filters.size() - 1));
+        }
+
+        // Now we are good to go, let's use the cacheBitmap as a starting point
+        for (int i = similarUpToIndex + 1; i < filters.size(); i++) {
+            FilterRepresentation representation = filters.elementAt(i);
+            CacheStep currentStep = mSteps.elementAt(i);
+            cacheBitmap = cacheBitmap.copy(Bitmap.Config.ARGB_8888, true);
+            if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) {
+                cacheBitmap = environment.applyRepresentation(representation, cacheBitmap);
+            }
+            currentStep.representation = representation.copy();
+            currentStep.cache = cacheBitmap;
+            if (DEBUG) {
+                Log.v(LOGTAG, " - " + i  + " => apply " + representation.getName());
+            }
+        }
+
+        if (DEBUG) {
+            Log.v(LOGTAG, "now let's cleanup the cache...");
+            displayNbBitmapsInCache();
+        }
+
+        // Let's see if we can cleanup the cache for unused bitmaps
+        for (int i = 0; i < similarUpToIndex; i++) {
+            CacheStep currentStep = mSteps.elementAt(i);
+            currentStep.cache = null;
+        }
+
+        if (DEBUG) {
+            Log.v(LOGTAG, "cleanup done...");
+            displayNbBitmapsInCache();
+        }
+        return cacheBitmap;
+    }
+
+    private void displayFilters(Vector<FilterRepresentation> filters) {
+        Log.v(LOGTAG, "------>>>");
+        for (int i = 0; i < filters.size(); i++) {
+            FilterRepresentation representation = filters.elementAt(i);
+            CacheStep step = mSteps.elementAt(i);
+            boolean similar = step.representation.equals(representation);
+            Log.v(LOGTAG, "[" + i + "] - " + representation.getName()
+                    + " similar rep ? " + (similar ? "YES" : "NO")
+                    + " -- bitmap: " + step.cache);
+        }
+        Log.v(LOGTAG, "<<<------");
+    }
+
+    private void displayNbBitmapsInCache() {
+        int nbBitmapsCached = 0;
+        for (int i = 0; i < mSteps.size(); i++) {
+            CacheStep step = mSteps.elementAt(i);
+            if (step.cache != null) {
+                nbBitmapsCached++;
+            }
+        }
+        Log.v(LOGTAG, "nb bitmaps in cache: " + nbBitmapsCached + " / " + mSteps.size());
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java
new file mode 100644
index 0000000..fc0d6ce
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.RenderScript;
+import android.util.Log;
+
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+import java.util.Vector;
+
+public class CachingPipeline implements PipelineInterface {
+    private static final String LOGTAG = "CachingPipeline";
+    private boolean DEBUG = false;
+
+    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
+
+    private static volatile RenderScript sRS = null;
+
+    private FiltersManager mFiltersManager = null;
+    private volatile Bitmap mOriginalBitmap = null;
+    private volatile Bitmap mResizedOriginalBitmap = null;
+
+    private FilterEnvironment mEnvironment = new FilterEnvironment();
+    private CacheProcessing mCachedProcessing = new CacheProcessing();
+
+
+    private volatile Allocation mOriginalAllocation = null;
+    private volatile Allocation mFiltersOnlyOriginalAllocation =  null;
+
+    protected volatile Allocation mInPixelsAllocation;
+    protected volatile Allocation mOutPixelsAllocation;
+    private volatile int mWidth = 0;
+    private volatile int mHeight = 0;
+
+    private volatile float mPreviewScaleFactor = 1.0f;
+    private volatile float mHighResPreviewScaleFactor = 1.0f;
+    private volatile String mName = "";
+
+    public CachingPipeline(FiltersManager filtersManager, String name) {
+        mFiltersManager = filtersManager;
+        mName = name;
+    }
+
+    public static synchronized RenderScript getRenderScriptContext() {
+        return sRS;
+    }
+
+    public static synchronized void createRenderscriptContext(Context context) {
+        if (sRS != null) {
+            Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext");
+            destroyRenderScriptContext();
+        }
+        sRS = RenderScript.create(context);
+    }
+
+    public static synchronized void destroyRenderScriptContext() {
+        if (sRS != null) {
+            sRS.destroy();
+        }
+        sRS = null;
+    }
+
+    public void stop() {
+        mEnvironment.setStop(true);
+    }
+
+    public synchronized void reset() {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return;
+            }
+            mOriginalBitmap = null; // just a reference to the bitmap in ImageLoader
+            if (mResizedOriginalBitmap != null) {
+                mResizedOriginalBitmap.recycle();
+                mResizedOriginalBitmap = null;
+            }
+            if (mOriginalAllocation != null) {
+                mOriginalAllocation.destroy();
+                mOriginalAllocation = null;
+            }
+            if (mFiltersOnlyOriginalAllocation != null) {
+                mFiltersOnlyOriginalAllocation.destroy();
+                mFiltersOnlyOriginalAllocation = null;
+            }
+            mPreviewScaleFactor = 1.0f;
+            mHighResPreviewScaleFactor = 1.0f;
+
+            destroyPixelAllocations();
+        }
+    }
+
+    public Resources getResources() {
+        return sRS.getApplicationContext().getResources();
+    }
+
+    private synchronized void destroyPixelAllocations() {
+        if (DEBUG) {
+            Log.v(LOGTAG, "destroyPixelAllocations in " + getName());
+        }
+        if (mInPixelsAllocation != null) {
+            mInPixelsAllocation.destroy();
+            mInPixelsAllocation = null;
+        }
+        if (mOutPixelsAllocation != null) {
+            mOutPixelsAllocation.destroy();
+            mOutPixelsAllocation = null;
+        }
+        mWidth = 0;
+        mHeight = 0;
+    }
+
+    private String getType(RenderingRequest request) {
+        if (request.getType() == RenderingRequest.ICON_RENDERING) {
+            return "ICON_RENDERING";
+        }
+        if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
+            return "FILTERS_RENDERING";
+        }
+        if (request.getType() == RenderingRequest.FULL_RENDERING) {
+            return "FULL_RENDERING";
+        }
+        if (request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
+            return "GEOMETRY_RENDERING";
+        }
+        if (request.getType() == RenderingRequest.PARTIAL_RENDERING) {
+            return "PARTIAL_RENDERING";
+        }
+        if (request.getType() == RenderingRequest.HIGHRES_RENDERING) {
+            return "HIGHRES_RENDERING";
+        }
+        return "UNKNOWN TYPE!";
+    }
+
+    private void setupEnvironment(ImagePreset preset, boolean highResPreview) {
+        mEnvironment.setPipeline(this);
+        mEnvironment.setFiltersManager(mFiltersManager);
+        if (highResPreview) {
+            mEnvironment.setScaleFactor(mHighResPreviewScaleFactor);
+        } else {
+            mEnvironment.setScaleFactor(mPreviewScaleFactor);
+        }
+        mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
+        mEnvironment.setImagePreset(preset);
+        mEnvironment.setStop(false);
+    }
+
+    public void setOriginal(Bitmap bitmap) {
+        mOriginalBitmap = bitmap;
+        Log.v(LOGTAG,"setOriginal, size " + bitmap.getWidth() + " x " + bitmap.getHeight());
+        ImagePreset preset = MasterImage.getImage().getPreset();
+        setupEnvironment(preset, false);
+        updateOriginalAllocation(preset);
+    }
+
+    private synchronized boolean updateOriginalAllocation(ImagePreset preset) {
+        Bitmap originalBitmap = mOriginalBitmap;
+
+        if (originalBitmap == null) {
+            return false;
+        }
+
+        RenderScript RS = getRenderScriptContext();
+
+        Allocation filtersOnlyOriginalAllocation = mFiltersOnlyOriginalAllocation;
+        mFiltersOnlyOriginalAllocation = Allocation.createFromBitmap(RS, originalBitmap,
+                Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
+        if (filtersOnlyOriginalAllocation != null) {
+            filtersOnlyOriginalAllocation.destroy();
+        }
+
+        Allocation originalAllocation = mOriginalAllocation;
+        mResizedOriginalBitmap = preset.applyGeometry(originalBitmap, mEnvironment);
+        mOriginalAllocation = Allocation.createFromBitmap(RS, mResizedOriginalBitmap,
+                Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
+        if (originalAllocation != null) {
+            originalAllocation.destroy();
+        }
+
+        return true;
+    }
+
+    public void renderHighres(RenderingRequest request) {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return;
+            }
+            ImagePreset preset = request.getImagePreset();
+            setupEnvironment(preset, false);
+            Bitmap bitmap = MasterImage.getImage().getOriginalBitmapHighres();
+            if (bitmap == null) {
+                return;
+            }
+            // TODO: use a cache of bitmaps
+            bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
+            bitmap = preset.applyGeometry(bitmap, mEnvironment);
+
+            mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
+            Bitmap bmp = preset.apply(bitmap, mEnvironment);
+            if (!mEnvironment.needsStop()) {
+                request.setBitmap(bmp);
+            }
+            mFiltersManager.freeFilterResources(preset);
+        }
+    }
+
+    public synchronized void render(RenderingRequest request) {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return;
+            }
+            if (((request.getType() != RenderingRequest.PARTIAL_RENDERING
+                    && request.getType() != RenderingRequest.HIGHRES_RENDERING)
+                    && request.getBitmap() == null)
+                    || request.getImagePreset() == null) {
+                return;
+            }
+
+            if (DEBUG) {
+                Log.v(LOGTAG, "render image of type " + getType(request));
+            }
+
+            Bitmap bitmap = request.getBitmap();
+            ImagePreset preset = request.getImagePreset();
+            setupEnvironment(preset,
+                    request.getType() != RenderingRequest.HIGHRES_RENDERING);
+            mFiltersManager.freeFilterResources(preset);
+
+            if (request.getType() == RenderingRequest.PARTIAL_RENDERING) {
+                MasterImage master = MasterImage.getImage();
+                bitmap = ImageLoader.getScaleOneImageForPreset(master.getActivity(),
+                        master.getUri(), request.getBounds(),
+                        request.getDestination());
+                if (bitmap == null) {
+                    Log.w(LOGTAG, "could not get bitmap for: " + getType(request));
+                    return;
+                }
+            }
+
+            if (request.getType() == RenderingRequest.HIGHRES_RENDERING) {
+                bitmap = MasterImage.getImage().getOriginalBitmapHighres();
+                if (bitmap != null) {
+                    bitmap = preset.applyGeometry(bitmap, mEnvironment);
+                }
+            }
+
+            if (request.getType() == RenderingRequest.FULL_RENDERING
+                    || request.getType() == RenderingRequest.GEOMETRY_RENDERING
+                    || request.getType() == RenderingRequest.FILTERS_RENDERING) {
+                updateOriginalAllocation(preset);
+            }
+
+            if (DEBUG) {
+                Log.v(LOGTAG, "after update, req bitmap (" + bitmap.getWidth() + "x" + bitmap.getHeight()
+                        + " ? resizeOriginal (" + mResizedOriginalBitmap.getWidth() + "x"
+                        + mResizedOriginalBitmap.getHeight());
+            }
+
+            if (request.getType() == RenderingRequest.FULL_RENDERING
+                    || request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
+                mOriginalAllocation.copyTo(bitmap);
+            } else if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
+                mFiltersOnlyOriginalAllocation.copyTo(bitmap);
+            }
+
+            if (request.getType() == RenderingRequest.FULL_RENDERING
+                    || request.getType() == RenderingRequest.FILTERS_RENDERING
+                    || request.getType() == RenderingRequest.ICON_RENDERING
+                    || request.getType() == RenderingRequest.PARTIAL_RENDERING
+                    || request.getType() == RenderingRequest.HIGHRES_RENDERING
+                    || request.getType() == RenderingRequest.STYLE_ICON_RENDERING) {
+
+                if (request.getType() == RenderingRequest.ICON_RENDERING) {
+                    mEnvironment.setQuality(FilterEnvironment.QUALITY_ICON);
+                } else {
+                    mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
+                }
+
+                Bitmap bmp = preset.apply(bitmap, mEnvironment);
+                if (!mEnvironment.needsStop()) {
+                    request.setBitmap(bmp);
+                }
+                mFiltersManager.freeFilterResources(preset);
+            }
+        }
+    }
+
+    public synchronized void renderImage(ImagePreset preset, Allocation in, Allocation out) {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return;
+            }
+            setupEnvironment(preset, false);
+            mFiltersManager.freeFilterResources(preset);
+            preset.applyFilters(-1, -1, in, out, mEnvironment);
+            boolean copyOut = false;
+            if (preset.nbFilters() > 0) {
+                copyOut = true;
+            }
+            preset.applyBorder(in, out, copyOut, mEnvironment);
+        }
+    }
+
+    public synchronized Bitmap renderFinalImage(Bitmap bitmap, ImagePreset preset) {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return bitmap;
+            }
+            setupEnvironment(preset, false);
+            mEnvironment.setQuality(FilterEnvironment.QUALITY_FINAL);
+            mEnvironment.setScaleFactor(1.0f);
+            mFiltersManager.freeFilterResources(preset);
+            bitmap = preset.applyGeometry(bitmap, mEnvironment);
+            bitmap = preset.apply(bitmap, mEnvironment);
+            return bitmap;
+        }
+    }
+
+    public Bitmap renderGeometryIcon(Bitmap bitmap, ImagePreset preset) {
+        return GeometryMathUtils.applyGeometryRepresentations(preset.getGeometryFilters(), bitmap);
+    }
+
+    public void compute(SharedBuffer buffer, ImagePreset preset, int type) {
+        if (getRenderScriptContext() == null) {
+            return;
+        }
+        setupEnvironment(preset, false);
+        Vector<FilterRepresentation> filters = preset.getFilters();
+        Bitmap result = mCachedProcessing.process(mOriginalBitmap, filters, mEnvironment);
+        buffer.setProducer(result);
+    }
+
+    public synchronized void computeOld(SharedBuffer buffer, ImagePreset preset, int type) {
+        synchronized (CachingPipeline.class) {
+            if (getRenderScriptContext() == null) {
+                return;
+            }
+            if (DEBUG) {
+                Log.v(LOGTAG, "compute preset " + preset);
+                preset.showFilters();
+            }
+
+            String thread = Thread.currentThread().getName();
+            long time = System.currentTimeMillis();
+            setupEnvironment(preset, false);
+            mFiltersManager.freeFilterResources(preset);
+
+            Bitmap resizedOriginalBitmap = mResizedOriginalBitmap;
+            if (updateOriginalAllocation(preset) || buffer.getProducer() == null) {
+                resizedOriginalBitmap = mResizedOriginalBitmap;
+                buffer.setProducer(resizedOriginalBitmap);
+                mEnvironment.cache(buffer.getProducer());
+            }
+
+            Bitmap bitmap = buffer.getProducer().getBitmap();
+            long time2 = System.currentTimeMillis();
+
+            if (bitmap == null || (bitmap.getWidth() != resizedOriginalBitmap.getWidth())
+                    || (bitmap.getHeight() != resizedOriginalBitmap.getHeight())) {
+                mEnvironment.cache(buffer.getProducer());
+                buffer.setProducer(resizedOriginalBitmap);
+                bitmap = buffer.getProducer().getBitmap();
+            }
+            mOriginalAllocation.copyTo(bitmap);
+
+            Bitmap tmpbitmap = preset.apply(bitmap, mEnvironment);
+            if (tmpbitmap != bitmap) {
+                mEnvironment.cache(buffer.getProducer());
+                buffer.setProducer(tmpbitmap);
+            }
+
+            mFiltersManager.freeFilterResources(preset);
+
+            time = System.currentTimeMillis() - time;
+            time2 = System.currentTimeMillis() - time2;
+            if (DEBUG) {
+                Log.v(LOGTAG, "Applying type " + type + " filters to bitmap "
+                        + bitmap + " (" + bitmap.getWidth() + " x " + bitmap.getHeight()
+                        + ") took " + time + " ms, " + time2 + " ms for the filter, on thread " + thread);
+            }
+        }
+    }
+
+    public boolean needsRepaint() {
+        SharedBuffer buffer = MasterImage.getImage().getPreviewBuffer();
+        return buffer.checkRepaintNeeded();
+    }
+
+    public void setPreviewScaleFactor(float previewScaleFactor) {
+        mPreviewScaleFactor = previewScaleFactor;
+    }
+
+    public void setHighResPreviewScaleFactor(float highResPreviewScaleFactor) {
+        mHighResPreviewScaleFactor = highResPreviewScaleFactor;
+    }
+
+    public synchronized boolean isInitialized() {
+        return getRenderScriptContext() != null && mOriginalBitmap != null;
+    }
+
+    public boolean prepareRenderscriptAllocations(Bitmap bitmap) {
+        RenderScript RS = getRenderScriptContext();
+        boolean needsUpdate = false;
+        if (mOutPixelsAllocation == null || mInPixelsAllocation == null ||
+                bitmap.getWidth() != mWidth || bitmap.getHeight() != mHeight) {
+            destroyPixelAllocations();
+            Bitmap bitmapBuffer = bitmap;
+            if (bitmap.getConfig() == null || bitmap.getConfig() != BITMAP_CONFIG) {
+                bitmapBuffer = bitmap.copy(BITMAP_CONFIG, true);
+            }
+            mOutPixelsAllocation = Allocation.createFromBitmap(RS, bitmapBuffer,
+                    Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
+            mInPixelsAllocation = Allocation.createTyped(RS,
+                    mOutPixelsAllocation.getType());
+            needsUpdate = true;
+        }
+        if (RS != null) {
+            mInPixelsAllocation.copyFrom(bitmap);
+        }
+        if (bitmap.getWidth() != mWidth
+                || bitmap.getHeight() != mHeight) {
+            mWidth = bitmap.getWidth();
+            mHeight = bitmap.getHeight();
+            needsUpdate = true;
+        }
+        if (DEBUG) {
+            Log.v(LOGTAG, "prepareRenderscriptAllocations: " + needsUpdate + " in " + getName());
+        }
+        return needsUpdate;
+    }
+
+    public synchronized Allocation getInPixelsAllocation() {
+        return mInPixelsAllocation;
+    }
+
+    public synchronized Allocation getOutPixelsAllocation() {
+        return mOutPixelsAllocation;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public RenderScript getRSContext() {
+        return CachingPipeline.getRenderScriptContext();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java b/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java
new file mode 100644
index 0000000..4fac956
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.support.v8.renderscript.Allocation;
+
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation.Rotation;
+import com.android.gallery3d.filtershow.filters.FiltersManagerInterface;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+
+public class FilterEnvironment {
+    private static final String LOGTAG = "FilterEnvironment";
+    private ImagePreset mImagePreset;
+    private float mScaleFactor;
+    private int mQuality;
+    private FiltersManagerInterface mFiltersManager;
+    private PipelineInterface mPipeline;
+    private volatile boolean mStop = false;
+
+    public static final int QUALITY_ICON = 0;
+    public static final int QUALITY_PREVIEW = 1;
+    public static final int QUALITY_FINAL = 2;
+
+    public synchronized boolean needsStop() {
+        return mStop;
+    }
+
+    public synchronized void setStop(boolean stop) {
+        this.mStop = stop;
+    }
+
+    private HashMap<Long, WeakReference<Bitmap>>
+            bitmapCach = new HashMap<Long, WeakReference<Bitmap>>();
+
+    private HashMap<Integer, Integer>
+                    generalParameters = new HashMap<Integer, Integer>();
+
+    public void cache(Buffer buffer) {
+        if (buffer == null) {
+            return;
+        }
+        Bitmap bitmap = buffer.getBitmap();
+        if (bitmap == null) {
+            return;
+        }
+        Long key = calcKey(bitmap.getWidth(), bitmap.getHeight());
+        bitmapCach.put(key, new WeakReference<Bitmap>(bitmap));
+    }
+
+    public Bitmap getBitmap(int w, int h) {
+        Long key = calcKey(w, h);
+        WeakReference<Bitmap> ref = bitmapCach.remove(key);
+        Bitmap bitmap = null;
+        if (ref != null) {
+            bitmap = ref.get();
+        }
+        if (bitmap == null) {
+            bitmap = Bitmap.createBitmap(
+                    w, h, Bitmap.Config.ARGB_8888);
+        }
+        return bitmap;
+    }
+
+    private Long calcKey(long w, long h) {
+        return (w << 32) | (h << 32);
+    }
+
+    public void setImagePreset(ImagePreset imagePreset) {
+        mImagePreset = imagePreset;
+    }
+
+    public ImagePreset getImagePreset() {
+        return mImagePreset;
+    }
+
+    public void setScaleFactor(float scaleFactor) {
+        mScaleFactor = scaleFactor;
+    }
+
+    public float getScaleFactor() {
+        return mScaleFactor;
+    }
+
+    public void setQuality(int quality) {
+        mQuality = quality;
+    }
+
+    public int getQuality() {
+        return mQuality;
+    }
+
+    public void setFiltersManager(FiltersManagerInterface filtersManager) {
+        mFiltersManager = filtersManager;
+    }
+
+    public FiltersManagerInterface getFiltersManager() {
+        return mFiltersManager;
+    }
+
+    public void applyRepresentation(FilterRepresentation representation,
+                                    Allocation in, Allocation out) {
+        ImageFilter filter = mFiltersManager.getFilterForRepresentation(representation);
+        filter.useRepresentation(representation);
+        filter.setEnvironment(this);
+        if (filter.supportsAllocationInput()) {
+            filter.apply(in, out);
+        }
+        filter.setGeneralParameters();
+        filter.setEnvironment(null);
+    }
+
+    public Bitmap applyRepresentation(FilterRepresentation representation, Bitmap bitmap) {
+        if (representation instanceof FilterUserPresetRepresentation) {
+            // we allow instances of FilterUserPresetRepresentation in a preset only to know if one
+            // has been applied (so we can show this in the UI). But as all the filters in them are
+            // applied directly they do not themselves need to do any kind of filtering.
+            return bitmap;
+        }
+        ImageFilter filter = mFiltersManager.getFilterForRepresentation(representation);
+        filter.useRepresentation(representation);
+        filter.setEnvironment(this);
+        Bitmap ret = filter.apply(bitmap, mScaleFactor, mQuality);
+        filter.setGeneralParameters();
+        filter.setEnvironment(null);
+        return ret;
+    }
+
+    public PipelineInterface getPipeline() {
+        return mPipeline;
+    }
+
+    public void setPipeline(PipelineInterface cachingPipeline) {
+        mPipeline = cachingPipeline;
+    }
+
+    public synchronized void clearGeneralParameters() {
+        generalParameters = null;
+    }
+
+    public synchronized Integer getGeneralParameter(int id) {
+        if (generalParameters == null || !generalParameters.containsKey(id)) {
+            return null;
+        }
+        return generalParameters.get(id);
+    }
+
+    public synchronized void setGeneralParameter(int id, int value) {
+        if (generalParameters == null) {
+            generalParameters = new HashMap<Integer, Integer>();
+        }
+
+        generalParameters.put(id, value);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java b/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java
new file mode 100644
index 0000000..5a0eb4d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+
+public class HighresRenderingRequestTask extends ProcessingTask {
+
+    private CachingPipeline mHighresPreviewPipeline = null;
+    private boolean mPipelineIsOn = false;
+
+    public void setHighresPreviewScaleFactor(float highResPreviewScale) {
+        mHighresPreviewPipeline.setHighResPreviewScaleFactor(highResPreviewScale);
+    }
+
+    public void setPreviewScaleFactor(float previewScale) {
+        mHighresPreviewPipeline.setPreviewScaleFactor(previewScale);
+    }
+
+    static class Render implements Request {
+        RenderingRequest request;
+    }
+
+    static class RenderResult implements Result {
+        RenderingRequest request;
+    }
+
+    public HighresRenderingRequestTask() {
+        mHighresPreviewPipeline = new CachingPipeline(
+                FiltersManager.getHighresManager(), "Highres");
+    }
+
+    public void setOriginal(Bitmap bitmap) {
+        mHighresPreviewPipeline.setOriginal(bitmap);
+    }
+
+    public void setOriginalBitmapHighres(Bitmap originalHires) {
+        mPipelineIsOn = true;
+    }
+
+    public void stop() {
+        mHighresPreviewPipeline.stop();
+    }
+
+    public void postRenderingRequest(RenderingRequest request) {
+        if (!mPipelineIsOn) {
+            return;
+        }
+        Render render = new Render();
+        render.request = request;
+        postRequest(render);
+    }
+
+    @Override
+    public Result doInBackground(Request message) {
+        RenderingRequest request = ((Render) message).request;
+        RenderResult result = null;
+        mHighresPreviewPipeline.renderHighres(request);
+        result = new RenderResult();
+        result.request = request;
+        return result;
+    }
+
+    @Override
+    public void onResult(Result message) {
+        if (message == null) {
+            return;
+        }
+        RenderingRequest request = ((RenderResult) message).request;
+        request.markAvailable();
+    }
+
+    @Override
+    public boolean isDelayedTask() { return true; }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java
new file mode 100644
index 0000000..d34216a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java
@@ -0,0 +1,694 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.support.v8.renderscript.Allocation;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.state.State;
+import com.android.gallery3d.filtershow.state.StateAdapter;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Vector;
+
+public class ImagePreset {
+
+    private static final String LOGTAG = "ImagePreset";
+
+    private Vector<FilterRepresentation> mFilters = new Vector<FilterRepresentation>();
+
+    private boolean mDoApplyGeometry = true;
+    private boolean mDoApplyFilters = true;
+
+    private boolean mPartialRendering = false;
+    private Rect mPartialRenderingBounds;
+    private static final boolean DEBUG = false;
+
+    public ImagePreset() {
+    }
+
+    public ImagePreset(ImagePreset source) {
+        for (int i = 0; i < source.mFilters.size(); i++) {
+            FilterRepresentation sourceRepresentation = source.mFilters.elementAt(i);
+            mFilters.add(sourceRepresentation.copy());
+        }
+    }
+
+    public Vector<FilterRepresentation> getFilters() {
+        return mFilters;
+    }
+
+    public FilterRepresentation getFilterRepresentation(int position) {
+        FilterRepresentation representation = null;
+
+        representation = mFilters.elementAt(position).copy();
+
+        return representation;
+    }
+
+    private static boolean sameSerializationName(String a, String b) {
+        if (a != null && b != null) {
+            return a.equals(b);
+        } else {
+            return a == null && b == null;
+        }
+    }
+
+    public static boolean sameSerializationName(FilterRepresentation a, FilterRepresentation b) {
+        if (a == null || b == null) {
+            return false;
+        }
+        return sameSerializationName(a.getSerializationName(), b.getSerializationName());
+    }
+
+    public int getPositionForRepresentation(FilterRepresentation representation) {
+        for (int i = 0; i < mFilters.size(); i++) {
+            if (sameSerializationName(mFilters.elementAt(i), representation)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private FilterRepresentation getFilterRepresentationForType(int type) {
+        for (int i = 0; i < mFilters.size(); i++) {
+            if (mFilters.elementAt(i).getFilterType() == type) {
+                return mFilters.elementAt(i);
+            }
+        }
+        return null;
+    }
+
+    public int getPositionForType(int type) {
+        for (int i = 0; i < mFilters.size(); i++) {
+            if (mFilters.elementAt(i).getFilterType() == type) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public FilterRepresentation getFilterRepresentationCopyFrom(
+            FilterRepresentation filterRepresentation) {
+        // TODO: add concept of position in the filters (to allow multiple instances)
+        if (filterRepresentation == null) {
+            return null;
+        }
+        int position = getPositionForRepresentation(filterRepresentation);
+        if (position == -1) {
+            return null;
+        }
+        FilterRepresentation representation = mFilters.elementAt(position);
+        if (representation != null) {
+            representation = representation.copy();
+        }
+        return representation;
+    }
+
+    public void updateFilterRepresentations(Collection<FilterRepresentation> reps) {
+        for (FilterRepresentation r : reps) {
+            updateOrAddFilterRepresentation(r);
+        }
+    }
+
+    public void updateOrAddFilterRepresentation(FilterRepresentation rep) {
+        int pos = getPositionForRepresentation(rep);
+        if (pos != -1) {
+            mFilters.elementAt(pos).useParametersFrom(rep);
+        } else {
+            addFilter(rep.copy());
+        }
+    }
+
+    public void setDoApplyGeometry(boolean value) {
+        mDoApplyGeometry = value;
+    }
+
+    public void setDoApplyFilters(boolean value) {
+        mDoApplyFilters = value;
+    }
+
+    public boolean getDoApplyFilters() {
+        return mDoApplyFilters;
+    }
+
+    public boolean hasModifications() {
+        for (int i = 0; i < mFilters.size(); i++) {
+            FilterRepresentation filter = mFilters.elementAt(i);
+            if (!filter.isNil()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean isPanoramaSafe() {
+        for (FilterRepresentation representation : mFilters) {
+            if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
+                    && !representation.isNil()) {
+                return false;
+            }
+            if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER
+                    && !representation.isNil()) {
+                return false;
+            }
+            if (representation.getFilterType() == FilterRepresentation.TYPE_VIGNETTE
+                    && !representation.isNil()) {
+                return false;
+            }
+            if (representation.getFilterType() == FilterRepresentation.TYPE_TINYPLANET
+                    && !representation.isNil()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean same(ImagePreset preset) {
+        if (preset == null) {
+            return false;
+        }
+
+        if (preset.mFilters.size() != mFilters.size()) {
+            return false;
+        }
+
+        if (mDoApplyGeometry != preset.mDoApplyGeometry) {
+            return false;
+        }
+
+        if (mDoApplyFilters != preset.mDoApplyFilters) {
+            if (mFilters.size() > 0 || preset.mFilters.size() > 0) {
+                return false;
+            }
+        }
+
+        if (mDoApplyFilters && preset.mDoApplyFilters) {
+            for (int i = 0; i < preset.mFilters.size(); i++) {
+                FilterRepresentation a = preset.mFilters.elementAt(i);
+                FilterRepresentation b = mFilters.elementAt(i);
+
+                if (!a.same(b)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public int similarUpTo(ImagePreset preset) {
+        for (int i = 0; i < preset.mFilters.size(); i++) {
+            FilterRepresentation a = preset.mFilters.elementAt(i);
+            if (i < mFilters.size()) {
+                FilterRepresentation b = mFilters.elementAt(i);
+                if (!a.same(b)) {
+                    return i;
+                }
+                if (!a.equals(b)) {
+                    return i;
+                }
+            } else {
+                return i;
+            }
+        }
+        return preset.mFilters.size();
+    }
+
+    public void showFilters() {
+        Log.v(LOGTAG, "\\\\\\ showFilters -- " + mFilters.size() + " filters");
+        int n = 0;
+        for (FilterRepresentation representation : mFilters) {
+            Log.v(LOGTAG, " filter " + n + " : " + representation.toString());
+            n++;
+        }
+        Log.v(LOGTAG, "/// showFilters -- " + mFilters.size() + " filters");
+    }
+
+    public FilterRepresentation getLastRepresentation() {
+        if (mFilters.size() > 0) {
+            return mFilters.lastElement();
+        }
+        return null;
+    }
+
+    public void removeFilter(FilterRepresentation filterRepresentation) {
+        if (filterRepresentation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+            for (int i = 0; i < mFilters.size(); i++) {
+                if (mFilters.elementAt(i).getFilterType()
+                == filterRepresentation.getFilterType()) {
+                    mFilters.remove(i);
+                    break;
+                }
+            }
+        } else {
+            for (int i = 0; i < mFilters.size(); i++) {
+                if (sameSerializationName(mFilters.elementAt(i), filterRepresentation)) {
+                    mFilters.remove(i);
+                    break;
+                }
+            }
+        }
+    }
+
+    // If the filter is an "None" effect or border, then just don't add this filter.
+    public void addFilter(FilterRepresentation representation) {
+        if (representation instanceof FilterUserPresetRepresentation) {
+            ImagePreset preset = ((FilterUserPresetRepresentation) representation).getImagePreset();
+            // user preset replace everything but geometry
+            mFilters.clear();
+            for (int i = 0; i < preset.nbFilters(); i++) {
+                addFilter(preset.getFilterRepresentation(i));
+            }
+            mFilters.add(representation);
+        } else if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+            // Add geometry filter, removing duplicates and do-nothing operations.
+            for (int i = 0; i < mFilters.size(); i++) {
+                if (sameSerializationName(representation, mFilters.elementAt(i))) {
+                    mFilters.remove(i);
+                }
+            }
+            if (!representation.isNil()) {
+                mFilters.add(representation);
+            }
+        } else if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+            removeFilter(representation);
+            if (!isNoneBorderFilter(representation)) {
+                mFilters.add(representation);
+            }
+        } else if (representation.getFilterType() == FilterRepresentation.TYPE_FX) {
+            boolean found = false;
+            for (int i = 0; i < mFilters.size(); i++) {
+                FilterRepresentation current = mFilters.elementAt(i);
+                int type = current.getFilterType();
+                if (found) {
+                    if (type != FilterRepresentation.TYPE_VIGNETTE) {
+                        mFilters.remove(i);
+                        continue;
+                    }
+                }
+                if (type == FilterRepresentation.TYPE_FX) {
+                    if (current instanceof FilterUserPresetRepresentation) {
+                        ImagePreset preset = ((FilterUserPresetRepresentation) current)
+                                .getImagePreset();
+                        // If we had an existing user preset, let's remove all the presets that
+                        // were added by it
+                        for (int j = 0; j < preset.nbFilters(); j++) {
+                            FilterRepresentation rep = preset.getFilterRepresentation(j);
+                            int pos = getPositionForRepresentation(rep);
+                            if (pos != -1) {
+                                mFilters.remove(pos);
+                            }
+                        }
+                        int pos = getPositionForRepresentation(current);
+                        if (pos != -1) {
+                            mFilters.remove(pos);
+                        } else {
+                            pos = 0;
+                        }
+                        if (!isNoneFxFilter(representation)) {
+                            mFilters.add(pos, representation);
+                        }
+
+                    } else {
+                        mFilters.remove(i);
+                        if (!isNoneFxFilter(representation)) {
+                            mFilters.add(i, representation);
+                        }
+                    }
+                    found = true;
+                }
+            }
+            if (!found) {
+                if (!isNoneFxFilter(representation)) {
+                    mFilters.add(representation);
+                }
+            }
+        } else {
+            mFilters.add(representation);
+        }
+    }
+
+    private boolean isNoneBorderFilter(FilterRepresentation representation) {
+        return representation instanceof FilterImageBorderRepresentation &&
+                ((FilterImageBorderRepresentation) representation).getDrawableResource() == 0;
+    }
+
+    private boolean isNoneFxFilter(FilterRepresentation representation) {
+        return representation instanceof FilterFxRepresentation &&
+                ((FilterFxRepresentation) representation).getNameResource() == R.string.none;
+    }
+
+    public FilterRepresentation getRepresentation(FilterRepresentation filterRepresentation) {
+        for (int i = 0; i < mFilters.size(); i++) {
+            FilterRepresentation representation = mFilters.elementAt(i);
+            if (sameSerializationName(representation, filterRepresentation)) {
+                return representation;
+            }
+        }
+        return null;
+    }
+
+    public Bitmap apply(Bitmap original, FilterEnvironment environment) {
+        Bitmap bitmap = original;
+        bitmap = applyFilters(bitmap, -1, -1, environment);
+        return applyBorder(bitmap, environment);
+    }
+
+    public Collection<FilterRepresentation> getGeometryFilters() {
+        ArrayList<FilterRepresentation> geometry = new ArrayList<FilterRepresentation>();
+        for (FilterRepresentation r : mFilters) {
+            if (r.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+                geometry.add(r);
+            }
+        }
+        return geometry;
+    }
+
+    public FilterRepresentation getFilterWithSerializationName(String serializationName) {
+        for (FilterRepresentation r : mFilters) {
+            if (r != null) {
+                if (sameSerializationName(r.getSerializationName(), serializationName)) {
+                    return r.copy();
+                }
+            }
+        }
+        return null;
+    }
+
+    public Bitmap applyGeometry(Bitmap bitmap, FilterEnvironment environment) {
+        // Apply any transform -- 90 rotate, flip, straighten, crop
+        // Returns a new bitmap.
+        if (mDoApplyGeometry) {
+            bitmap = GeometryMathUtils.applyGeometryRepresentations(getGeometryFilters(), bitmap);
+        }
+        return bitmap;
+    }
+
+    public Bitmap applyBorder(Bitmap bitmap, FilterEnvironment environment) {
+        // get the border from the list of filters.
+        FilterRepresentation border = getFilterRepresentationForType(
+                FilterRepresentation.TYPE_BORDER);
+        if (border != null && mDoApplyGeometry) {
+            bitmap = environment.applyRepresentation(border, bitmap);
+            if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
+                UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                        "SaveBorder", border.getSerializationName(), 1);
+            }
+        }
+        return bitmap;
+    }
+
+    public int nbFilters() {
+        return mFilters.size();
+    }
+
+    public Bitmap applyFilters(Bitmap bitmap, int from, int to, FilterEnvironment environment) {
+        if (mDoApplyFilters) {
+            if (from < 0) {
+                from = 0;
+            }
+            if (to == -1) {
+                to = mFilters.size();
+            }
+            if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
+                UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                        "SaveFilters", "Total", to - from + 1);
+            }
+            for (int i = from; i < to; i++) {
+                FilterRepresentation representation = mFilters.elementAt(i);
+                if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+                    // skip the geometry as it's already applied.
+                    continue;
+                }
+                if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+                    // for now, let's skip the border as it will be applied in
+                    // applyBorder()
+                    // TODO: might be worth getting rid of applyBorder.
+                    continue;
+                }
+                bitmap = environment.applyRepresentation(representation, bitmap);
+                if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
+                    UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                            "SaveFilter", representation.getSerializationName(), 1);
+                }
+                if (environment.needsStop()) {
+                    return bitmap;
+                }
+            }
+        }
+
+        return bitmap;
+    }
+
+    public void applyBorder(Allocation in, Allocation out,
+            boolean copyOut, FilterEnvironment environment) {
+        FilterRepresentation border = getFilterRepresentationForType(
+                FilterRepresentation.TYPE_BORDER);
+        if (border != null && mDoApplyGeometry) {
+            // TODO: should keep the bitmap around
+            Allocation bitmapIn = in;
+            if (copyOut) {
+                bitmapIn = Allocation.createTyped(
+                        CachingPipeline.getRenderScriptContext(), in.getType());
+                bitmapIn.copyFrom(out);
+            }
+            environment.applyRepresentation(border, bitmapIn, out);
+        }
+    }
+
+    public void applyFilters(int from, int to, Allocation in, Allocation out,
+            FilterEnvironment environment) {
+        if (mDoApplyFilters) {
+            if (from < 0) {
+                from = 0;
+            }
+            if (to == -1) {
+                to = mFilters.size();
+            }
+            for (int i = from; i < to; i++) {
+                FilterRepresentation representation = mFilters.elementAt(i);
+                if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
+                        || representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+                    continue;
+                }
+                if (i > from) {
+                    in.copyFrom(out);
+                }
+                environment.applyRepresentation(representation, in, out);
+            }
+        }
+    }
+
+    public boolean canDoPartialRendering() {
+        if (MasterImage.getImage().getZoomOrientation() != ImageLoader.ORI_NORMAL) {
+            return false;
+        }
+        for (int i = 0; i < mFilters.size(); i++) {
+            FilterRepresentation representation = mFilters.elementAt(i);
+            if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
+                    && !representation.isNil()) {
+                return false;
+            }
+            if (!representation.supportsPartialRendering()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public void fillImageStateAdapter(StateAdapter imageStateAdapter) {
+        if (imageStateAdapter == null) {
+            return;
+        }
+        Vector<State> states = new Vector<State>();
+        for (FilterRepresentation filter : mFilters) {
+            if (filter.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+                // TODO: supports Geometry representations in the state panel.
+                continue;
+            }
+            if (filter instanceof FilterUserPresetRepresentation) {
+                // do not show the user preset itself in the state panel
+                continue;
+            }
+            State state = new State(filter.getName());
+            state.setFilterRepresentation(filter);
+            states.add(state);
+        }
+        imageStateAdapter.fill(states);
+    }
+
+    public void setPartialRendering(boolean partialRendering, Rect bounds) {
+        mPartialRendering = partialRendering;
+        mPartialRenderingBounds = bounds;
+    }
+
+    public boolean isPartialRendering() {
+        return mPartialRendering;
+    }
+
+    public Rect getPartialRenderingBounds() {
+        return mPartialRenderingBounds;
+    }
+
+    public Vector<ImageFilter> getUsedFilters(BaseFiltersManager filtersManager) {
+        Vector<ImageFilter> usedFilters = new Vector<ImageFilter>();
+        for (int i = 0; i < mFilters.size(); i++) {
+            FilterRepresentation representation = mFilters.elementAt(i);
+            ImageFilter filter = filtersManager.getFilterForRepresentation(representation);
+            usedFilters.add(filter);
+        }
+        return usedFilters;
+    }
+
+    public String getJsonString(String name) {
+        StringWriter swriter = new StringWriter();
+        try {
+            JsonWriter writer = new JsonWriter(swriter);
+            writeJson(writer, name);
+            writer.close();
+        } catch (IOException e) {
+            return null;
+        }
+        return swriter.toString();
+    }
+
+    public void writeJson(JsonWriter writer, String name) {
+        int numFilters = mFilters.size();
+        try {
+            writer.beginObject();
+            for (int i = 0; i < numFilters; i++) {
+                FilterRepresentation filter = mFilters.get(i);
+                if (filter instanceof FilterUserPresetRepresentation) {
+                    continue;
+                }
+                String sname = filter.getSerializationName();
+                if (DEBUG) {
+                    Log.v(LOGTAG, "Serialization: " + sname);
+                    if (sname == null) {
+                        Log.v(LOGTAG, "Serialization name null for filter: " + filter);
+                    }
+                }
+                writer.name(sname);
+                filter.serializeRepresentation(writer);
+            }
+            writer.endObject();
+
+        } catch (IOException e) {
+           Log.e(LOGTAG,"Error encoding JASON",e);
+        }
+    }
+
+    /**
+     * populates preset from JSON string
+     *
+     * @param filterString a JSON string
+     * @return true on success if false ImagePreset is undefined
+     */
+    public boolean readJsonFromString(String filterString) {
+        if (DEBUG) {
+            Log.v(LOGTAG, "reading preset: \"" + filterString + "\"");
+        }
+        StringReader sreader = new StringReader(filterString);
+        try {
+            JsonReader reader = new JsonReader(sreader);
+            boolean ok = readJson(reader);
+            if (!ok) {
+                reader.close();
+                return false;
+            }
+            reader.close();
+        } catch (Exception e) {
+            Log.e(LOGTAG, "parsing the filter parameters:", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * populates preset from JSON stream
+     *
+     * @param sreader a JSON string
+     * @return true on success if false ImagePreset is undefined
+     */
+    public boolean readJson(JsonReader sreader) throws IOException {
+        sreader.beginObject();
+
+        while (sreader.hasNext()) {
+            String name = sreader.nextName();
+            FilterRepresentation filter = creatFilterFromName(name);
+            if (filter == null) {
+                Log.w(LOGTAG, "UNKNOWN FILTER! " + name);
+                return false;
+            }
+            filter.deSerializeRepresentation(sreader);
+            addFilter(filter);
+        }
+        sreader.endObject();
+        return true;
+    }
+
+    FilterRepresentation creatFilterFromName(String name) {
+        if (FilterRotateRepresentation.SERIALIZATION_NAME.equals(name)) {
+            return new FilterRotateRepresentation();
+        } else if (FilterMirrorRepresentation.SERIALIZATION_NAME.equals(name)) {
+            return new FilterMirrorRepresentation();
+        } else if (FilterStraightenRepresentation.SERIALIZATION_NAME.equals(name)) {
+            return new FilterStraightenRepresentation();
+        } else if (FilterCropRepresentation.SERIALIZATION_NAME.equals(name)) {
+            return new FilterCropRepresentation();
+        }
+        FiltersManager filtersManager = FiltersManager.getManager();
+        return filtersManager.createFilterFromName(name);
+    }
+
+    public void updateWith(ImagePreset preset) {
+        if (preset.mFilters.size() != mFilters.size()) {
+            Log.e(LOGTAG, "Updating a preset with an incompatible one");
+            return;
+        }
+        for (int i = 0; i < mFilters.size(); i++) {
+            FilterRepresentation destRepresentation = mFilters.elementAt(i);
+            FilterRepresentation sourceRepresentation = preset.mFilters.elementAt(i);
+            destRepresentation.useParametersFrom(sourceRepresentation);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java
new file mode 100644
index 0000000..b760edd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ImageSavingTask extends ProcessingTask {
+    private ProcessingService mProcessingService;
+
+    static class SaveRequest implements Request {
+        Uri sourceUri;
+        Uri selectedUri;
+        File destinationFile;
+        ImagePreset preset;
+        boolean flatten;
+        int quality;
+    }
+
+    static class UpdateBitmap implements Update {
+        Bitmap bitmap;
+    }
+
+    static class UpdateProgress implements Update {
+        int max;
+        int current;
+    }
+
+    static class URIResult implements Result {
+        Uri uri;
+    }
+
+    public ImageSavingTask(ProcessingService service) {
+        mProcessingService = service;
+    }
+
+    public void saveImage(Uri sourceUri, Uri selectedUri,
+                          File destinationFile, ImagePreset preset, boolean flatten, int quality) {
+        SaveRequest request = new SaveRequest();
+        request.sourceUri = sourceUri;
+        request.selectedUri = selectedUri;
+        request.destinationFile = destinationFile;
+        request.preset = preset;
+        request.flatten = flatten;
+        request.quality = quality;
+        postRequest(request);
+    }
+
+    public Result doInBackground(Request message) {
+        SaveRequest request = (SaveRequest) message;
+        Uri sourceUri = request.sourceUri;
+        Uri selectedUri = request.selectedUri;
+        File destinationFile = request.destinationFile;
+        ImagePreset preset = request.preset;
+        boolean flatten = request.flatten;
+        // We create a small bitmap showing the result that we can
+        // give to the notification
+        UpdateBitmap updateBitmap = new UpdateBitmap();
+        updateBitmap.bitmap = createNotificationBitmap(sourceUri, preset);
+        postUpdate(updateBitmap);
+        SaveImage saveImage = new SaveImage(mProcessingService, sourceUri,
+                selectedUri, destinationFile,
+                new SaveImage.Callback() {
+                    @Override
+                    public void onProgress(int max, int current) {
+                        UpdateProgress updateProgress = new UpdateProgress();
+                        updateProgress.max = max;
+                        updateProgress.current = current;
+                        postUpdate(updateProgress);
+                    }
+                });
+        Uri uri = saveImage.processAndSaveImage(preset, !flatten, request.quality);
+        URIResult result = new URIResult();
+        result.uri = uri;
+        return result;
+    }
+
+    @Override
+    public void onResult(Result message) {
+        URIResult result = (URIResult) message;
+        mProcessingService.completeSaveImage(result.uri);
+    }
+
+    @Override
+    public void onUpdate(Update message) {
+        if (message instanceof UpdateBitmap) {
+            Bitmap bitmap = ((UpdateBitmap) message).bitmap;
+            mProcessingService.updateNotificationWithBitmap(bitmap);
+        }
+        if (message instanceof UpdateProgress) {
+            UpdateProgress progress = (UpdateProgress) message;
+            mProcessingService.updateProgress(progress.max, progress.current);
+        }
+    }
+
+    private Bitmap createNotificationBitmap(Uri sourceUri, ImagePreset preset) {
+        int notificationBitmapSize = Resources.getSystem().getDimensionPixelSize(
+                android.R.dimen.notification_large_icon_width);
+        Bitmap bitmap = ImageLoader.loadConstrainedBitmap(sourceUri, getContext(),
+                notificationBitmapSize, null, true);
+        CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Thumb");
+        return pipeline.renderFinalImage(bitmap, preset);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java b/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java
new file mode 100644
index 0000000..d53768c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.RenderScript;
+
+public interface PipelineInterface {
+    public String getName();
+    public Resources getResources();
+    public Allocation getInPixelsAllocation();
+    public Allocation getOutPixelsAllocation();
+    public boolean prepareRenderscriptAllocations(Bitmap bitmap);
+    public RenderScript getRSContext();
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java
new file mode 100644
index 0000000..d0504d1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ProcessingService extends Service {
+    private static final String LOGTAG = "ProcessingService";
+    private static final boolean SHOW_IMAGE = false;
+    private int mNotificationId;
+    private NotificationManager mNotifyMgr = null;
+    private Notification.Builder mBuilder = null;
+
+    private static final String PRESET = "preset";
+    private static final String QUALITY = "quality";
+    private static final String SOURCE_URI = "sourceUri";
+    private static final String SELECTED_URI = "selectedUri";
+    private static final String DESTINATION_FILE = "destinationFile";
+    private static final String SAVING = "saving";
+    private static final String FLATTEN = "flatten";
+
+    private ProcessingTaskController mProcessingTaskController;
+    private ImageSavingTask mImageSavingTask;
+    private UpdatePreviewTask mUpdatePreviewTask;
+    private HighresRenderingRequestTask mHighresRenderingRequestTask;
+    private RenderingRequestTask mRenderingRequestTask;
+
+    private final IBinder mBinder = new LocalBinder();
+    private FilterShowActivity mFiltershowActivity;
+
+    private boolean mSaving = false;
+    private boolean mNeedsAlive = false;
+
+    public void setFiltershowActivity(FilterShowActivity filtershowActivity) {
+        mFiltershowActivity = filtershowActivity;
+    }
+
+    public void setOriginalBitmap(Bitmap originalBitmap) {
+        if (mUpdatePreviewTask == null) {
+            return;
+        }
+        mUpdatePreviewTask.setOriginal(originalBitmap);
+        mHighresRenderingRequestTask.setOriginal(originalBitmap);
+        mRenderingRequestTask.setOriginal(originalBitmap);
+    }
+
+    public void updatePreviewBuffer() {
+        mHighresRenderingRequestTask.stop();
+        mUpdatePreviewTask.updatePreview();
+    }
+
+    public void postRenderingRequest(RenderingRequest request) {
+        mRenderingRequestTask.postRenderingRequest(request);
+    }
+
+    public void postHighresRenderingRequest(ImagePreset preset, float scaleFactor,
+                                            RenderingRequestCaller caller) {
+        RenderingRequest request = new RenderingRequest();
+        // TODO: use the triple buffer preset as UpdatePreviewTask does instead of creating a copy
+        ImagePreset passedPreset = new ImagePreset(preset);
+        request.setOriginalImagePreset(preset);
+        request.setScaleFactor(scaleFactor);
+        request.setImagePreset(passedPreset);
+        request.setType(RenderingRequest.HIGHRES_RENDERING);
+        request.setCaller(caller);
+        mHighresRenderingRequestTask.postRenderingRequest(request);
+    }
+
+    public void setHighresPreviewScaleFactor(float highResPreviewScale) {
+        mHighresRenderingRequestTask.setHighresPreviewScaleFactor(highResPreviewScale);
+    }
+
+    public void setPreviewScaleFactor(float previewScale) {
+        mHighresRenderingRequestTask.setPreviewScaleFactor(previewScale);
+        mRenderingRequestTask.setPreviewScaleFactor(previewScale);
+    }
+
+    public void setOriginalBitmapHighres(Bitmap originalHires) {
+        mHighresRenderingRequestTask.setOriginalBitmapHighres(originalHires);
+    }
+
+    public class LocalBinder extends Binder {
+        public ProcessingService getService() {
+            return ProcessingService.this;
+        }
+    }
+
+    public static Intent getSaveIntent(Context context, ImagePreset preset, File destination,
+            Uri selectedImageUri, Uri sourceImageUri, boolean doFlatten, int quality) {
+        Intent processIntent = new Intent(context, ProcessingService.class);
+        processIntent.putExtra(ProcessingService.SOURCE_URI,
+                sourceImageUri.toString());
+        processIntent.putExtra(ProcessingService.SELECTED_URI,
+                selectedImageUri.toString());
+        processIntent.putExtra(ProcessingService.QUALITY, quality);
+        if (destination != null) {
+            processIntent.putExtra(ProcessingService.DESTINATION_FILE, destination.toString());
+        }
+        processIntent.putExtra(ProcessingService.PRESET,
+                preset.getJsonString(context.getString(R.string.saved)));
+        processIntent.putExtra(ProcessingService.SAVING, true);
+        if (doFlatten) {
+            processIntent.putExtra(ProcessingService.FLATTEN, true);
+        }
+        return processIntent;
+    }
+
+
+    @Override
+    public void onCreate() {
+        mProcessingTaskController = new ProcessingTaskController(this);
+        mImageSavingTask = new ImageSavingTask(this);
+        mUpdatePreviewTask = new UpdatePreviewTask();
+        mHighresRenderingRequestTask = new HighresRenderingRequestTask();
+        mRenderingRequestTask = new RenderingRequestTask();
+        mProcessingTaskController.add(mImageSavingTask);
+        mProcessingTaskController.add(mUpdatePreviewTask);
+        mProcessingTaskController.add(mHighresRenderingRequestTask);
+        mProcessingTaskController.add(mRenderingRequestTask);
+        setupPipeline();
+    }
+
+    @Override
+    public void onDestroy() {
+        tearDownPipeline();
+        mProcessingTaskController.quit();
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        mNeedsAlive = true;
+        if (intent != null && intent.getBooleanExtra(SAVING, false)) {
+            // we save using an intent to keep the service around after the
+            // activity has been destroyed.
+            String presetJson = intent.getStringExtra(PRESET);
+            String source = intent.getStringExtra(SOURCE_URI);
+            String selected = intent.getStringExtra(SELECTED_URI);
+            String destination = intent.getStringExtra(DESTINATION_FILE);
+            int quality = intent.getIntExtra(QUALITY, 100);
+            boolean flatten = intent.getBooleanExtra(FLATTEN, false);
+            Uri sourceUri = Uri.parse(source);
+            Uri selectedUri = null;
+            if (selected != null) {
+                selectedUri = Uri.parse(selected);
+            }
+            File destinationFile = null;
+            if (destination != null) {
+                destinationFile = new File(destination);
+            }
+            ImagePreset preset = new ImagePreset();
+            preset.readJsonFromString(presetJson);
+            mNeedsAlive = false;
+            mSaving = true;
+            handleSaveRequest(sourceUri, selectedUri, destinationFile, preset, flatten, quality);
+        }
+        return START_REDELIVER_INTENT;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    public void onStart() {
+        mNeedsAlive = true;
+        if (!mSaving && mFiltershowActivity != null) {
+            mFiltershowActivity.updateUIAfterServiceStarted();
+        }
+    }
+
+    public void handleSaveRequest(Uri sourceUri, Uri selectedUri,
+            File destinationFile, ImagePreset preset, boolean flatten, int quality) {
+        mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+        mNotificationId++;
+
+        mBuilder =
+                new Notification.Builder(this)
+                        .setSmallIcon(R.drawable.filtershow_button_fx)
+                        .setContentTitle(getString(R.string.filtershow_notification_label))
+                        .setContentText(getString(R.string.filtershow_notification_message));
+
+        startForeground(mNotificationId, mBuilder.build());
+
+        updateProgress(SaveImage.MAX_PROCESSING_STEPS, 0);
+
+        // Process the image
+
+        mImageSavingTask.saveImage(sourceUri, selectedUri, destinationFile,
+                preset, flatten, quality);
+    }
+
+    public void updateNotificationWithBitmap(Bitmap bitmap) {
+        mBuilder.setLargeIcon(bitmap);
+        mNotifyMgr.notify(mNotificationId, mBuilder.build());
+    }
+
+    public void updateProgress(int max, int current) {
+        mBuilder.setProgress(max, current, false);
+        mNotifyMgr.notify(mNotificationId, mBuilder.build());
+    }
+
+    public void completeSaveImage(Uri result) {
+        if (SHOW_IMAGE) {
+            // TODO: we should update the existing image in Gallery instead
+            Intent viewImage = new Intent(Intent.ACTION_VIEW, result);
+            viewImage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            startActivity(viewImage);
+        }
+        stopForeground(true);
+        stopSelf();
+        if (mNeedsAlive) {
+            // If the app has been restarted while we were saving...
+            mFiltershowActivity.updateUIAfterServiceStarted();
+        } else if (mFiltershowActivity.isSimpleEditAction()) {
+            // terminate now
+            mFiltershowActivity.completeSaveImage(result);
+        }
+    }
+
+    private void setupPipeline() {
+        Resources res = getResources();
+        FiltersManager.setResources(res);
+        CachingPipeline.createRenderscriptContext(this);
+
+        FiltersManager filtersManager = FiltersManager.getManager();
+        filtersManager.addLooks(this);
+        filtersManager.addBorders(this);
+        filtersManager.addTools(this);
+        filtersManager.addEffects();
+
+        FiltersManager highresFiltersManager = FiltersManager.getHighresManager();
+        highresFiltersManager.addLooks(this);
+        highresFiltersManager.addBorders(this);
+        highresFiltersManager.addTools(this);
+        highresFiltersManager.addEffects();
+    }
+
+    private void tearDownPipeline() {
+        ImageFilter.resetStatics();
+        FiltersManager.getPreviewManager().freeRSFilterScripts();
+        FiltersManager.getManager().freeRSFilterScripts();
+        FiltersManager.getHighresManager().freeRSFilterScripts();
+        FiltersManager.reset();
+        CachingPipeline.destroyRenderScriptContext();
+    }
+
+    static {
+        System.loadLibrary("jni_filtershow_filters");
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java
new file mode 100644
index 0000000..8d3e811
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+
+public abstract class ProcessingTask {
+    private ProcessingTaskController mTaskController;
+    private Handler mProcessingHandler;
+    private Handler mResultHandler;
+    private int mType;
+    private static final int DELAY = 300;
+
+    static interface Request {}
+    static interface Update {}
+    static interface Result {}
+
+    public boolean postRequest(Request message) {
+        Message msg = mProcessingHandler.obtainMessage(mType);
+        msg.obj = message;
+        if (isPriorityTask()) {
+            if (mProcessingHandler.hasMessages(getType())) {
+                return false;
+            }
+            mProcessingHandler.sendMessageAtFrontOfQueue(msg);
+        } else if (isDelayedTask()) {
+            if (mProcessingHandler.hasMessages(getType())) {
+                mProcessingHandler.removeMessages(getType());
+            }
+            mProcessingHandler.sendMessageDelayed(msg, DELAY);
+        } else {
+            mProcessingHandler.sendMessage(msg);
+        }
+        return true;
+    }
+
+    public void postUpdate(Update message) {
+        Message msg = mResultHandler.obtainMessage(mType);
+        msg.obj = message;
+        msg.arg1 = ProcessingTaskController.UPDATE;
+        mResultHandler.sendMessage(msg);
+    }
+
+    public void processRequest(Request message) {
+        Object result = doInBackground(message);
+        Message msg = mResultHandler.obtainMessage(mType);
+        msg.obj = result;
+        msg.arg1 = ProcessingTaskController.RESULT;
+        mResultHandler.sendMessage(msg);
+    }
+
+    public void added(ProcessingTaskController taskController) {
+        mTaskController = taskController;
+        mResultHandler = taskController.getResultHandler();
+        mProcessingHandler = taskController.getProcessingHandler();
+        mType = taskController.getReservedType();
+    }
+
+    public int getType() {
+        return mType;
+    }
+
+    public Context getContext() {
+        return mTaskController.getContext();
+    }
+
+    public abstract Result doInBackground(Request message);
+    public abstract void onResult(Result message);
+    public void onUpdate(Update message) {}
+    public boolean isPriorityTask() { return false; }
+    public boolean isDelayedTask() { return false; }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java
new file mode 100644
index 0000000..b54bbb0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.HashMap;
+
+public class ProcessingTaskController implements Handler.Callback {
+    private static final String LOGTAG = "ProcessingTaskController";
+
+    private Context mContext;
+    private HandlerThread mHandlerThread = null;
+    private Handler mProcessingHandler = null;
+    private int mCurrentType;
+    private HashMap<Integer, ProcessingTask> mTasks = new HashMap<Integer, ProcessingTask>();
+
+    public final static int RESULT = 1;
+    public final static int UPDATE = 2;
+
+    private final Handler mResultHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            ProcessingTask task = mTasks.get(msg.what);
+            if (task != null) {
+                if (msg.arg1 == RESULT) {
+                    task.onResult((ProcessingTask.Result) msg.obj);
+                } else if (msg.arg1 == UPDATE) {
+                    task.onUpdate((ProcessingTask.Update) msg.obj);
+                } else {
+                    Log.w(LOGTAG, "received unknown message! " + msg.arg1);
+                }
+            }
+        }
+    };
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        ProcessingTask task = mTasks.get(msg.what);
+        if (task != null) {
+            task.processRequest((ProcessingTask.Request) msg.obj);
+            return true;
+        }
+        return false;
+    }
+
+    public ProcessingTaskController(Context context) {
+        mContext = context;
+        mHandlerThread = new HandlerThread("ProcessingTaskController",
+                android.os.Process.THREAD_PRIORITY_FOREGROUND);
+        mHandlerThread.start();
+        mProcessingHandler = new Handler(mHandlerThread.getLooper(), this);
+    }
+
+    public Handler getProcessingHandler() {
+        return mProcessingHandler;
+    }
+
+    public Handler getResultHandler() {
+        return mResultHandler;
+    }
+
+    public int getReservedType() {
+        return mCurrentType++;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public void add(ProcessingTask task) {
+        task.added(this);
+        mTasks.put(task.getType(), task);
+    }
+
+    public void quit() {
+        mHandlerThread.quit();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java
new file mode 100644
index 0000000..ef4bb9b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import com.android.gallery3d.app.Log;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class RenderingRequest {
+    private static final String LOGTAG = "RenderingRequest";
+    private boolean mIsDirect = false;
+    private Bitmap mBitmap = null;
+    private ImagePreset mImagePreset = null;
+    private ImagePreset mOriginalImagePreset = null;
+    private RenderingRequestCaller mCaller = null;
+    private float mScaleFactor = 1.0f;
+    private Rect mBounds = null;
+    private Rect mDestination = null;
+    private int mType = FULL_RENDERING;
+    public static final int FULL_RENDERING = 0;
+    public static final int FILTERS_RENDERING = 1;
+    public static final int GEOMETRY_RENDERING = 2;
+    public static final int ICON_RENDERING = 3;
+    public static final int PARTIAL_RENDERING = 4;
+    public static final int HIGHRES_RENDERING = 5;
+    public static final int STYLE_ICON_RENDERING = 6;
+
+    private static final Bitmap.Config mConfig = Bitmap.Config.ARGB_8888;
+
+    public static void post(Context context, Bitmap source, ImagePreset preset,
+                            int type, RenderingRequestCaller caller) {
+        RenderingRequest.post(context, source, preset, type, caller, null, null);
+    }
+
+    public static void post(Context context, Bitmap source, ImagePreset preset, int type,
+                            RenderingRequestCaller caller, Rect bounds, Rect destination) {
+        if (((type != PARTIAL_RENDERING && type != HIGHRES_RENDERING) && source == null)
+                || preset == null || caller == null) {
+            Log.v(LOGTAG, "something null: source: " + source
+                    + " or preset: " + preset + " or caller: " + caller);
+            return;
+        }
+        RenderingRequest request = new RenderingRequest();
+        Bitmap bitmap = null;
+        if (type == FULL_RENDERING
+                || type == GEOMETRY_RENDERING
+                || type == ICON_RENDERING
+                || type == STYLE_ICON_RENDERING) {
+            CachingPipeline pipeline = new CachingPipeline(
+                    FiltersManager.getManager(), "Icon");
+            bitmap = pipeline.renderGeometryIcon(source, preset);
+        } else if (type != PARTIAL_RENDERING && type != HIGHRES_RENDERING) {
+            bitmap = Bitmap.createBitmap(source.getWidth(), source.getHeight(), mConfig);
+        }
+
+        request.setBitmap(bitmap);
+        ImagePreset passedPreset = new ImagePreset(preset);
+        request.setOriginalImagePreset(preset);
+        request.setScaleFactor(MasterImage.getImage().getScaleFactor());
+
+        if (type == PARTIAL_RENDERING) {
+            request.setBounds(bounds);
+            request.setDestination(destination);
+            passedPreset.setPartialRendering(true, bounds);
+        }
+
+        request.setImagePreset(passedPreset);
+        request.setType(type);
+        request.setCaller(caller);
+        request.post(context);
+    }
+
+    public void post(Context context) {
+        if (context instanceof FilterShowActivity) {
+            FilterShowActivity activity = (FilterShowActivity) context;
+            ProcessingService service = activity.getProcessingService();
+            service.postRenderingRequest(this);
+        }
+    }
+
+    public void markAvailable() {
+        if (mBitmap == null || mImagePreset == null
+                || mCaller == null) {
+            return;
+        }
+        mCaller.available(this);
+    }
+
+    public boolean isDirect() {
+        return mIsDirect;
+    }
+
+    public void setDirect(boolean isDirect) {
+        mIsDirect = isDirect;
+    }
+
+    public Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    public void setBitmap(Bitmap bitmap) {
+        mBitmap = bitmap;
+    }
+
+    public ImagePreset getImagePreset() {
+        return mImagePreset;
+    }
+
+    public void setImagePreset(ImagePreset imagePreset) {
+        mImagePreset = imagePreset;
+    }
+
+    public int getType() {
+        return mType;
+    }
+
+    public void setType(int type) {
+        mType = type;
+    }
+
+    public void setCaller(RenderingRequestCaller caller) {
+        mCaller = caller;
+    }
+
+    public Rect getBounds() {
+        return mBounds;
+    }
+
+    public void setBounds(Rect bounds) {
+        mBounds = bounds;
+    }
+
+    public void setScaleFactor(float scaleFactor) {
+        mScaleFactor = scaleFactor;
+    }
+
+    public float getScaleFactor() {
+        return mScaleFactor;
+    }
+
+    public Rect getDestination() {
+        return mDestination;
+    }
+
+    public void setDestination(Rect destination) {
+        mDestination = destination;
+    }
+
+    public ImagePreset getOriginalImagePreset() {
+        return mOriginalImagePreset;
+    }
+
+    public void setOriginalImagePreset(ImagePreset originalImagePreset) {
+        mOriginalImagePreset = originalImagePreset;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java
new file mode 100644
index 0000000..b978e70
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+public interface RenderingRequestCaller {
+    public void available(RenderingRequest request);
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java
new file mode 100644
index 0000000..7a83f70
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+
+public class RenderingRequestTask extends ProcessingTask {
+
+    private CachingPipeline mPreviewPipeline = null;
+    private boolean mPipelineIsOn = false;
+
+    public void setPreviewScaleFactor(float previewScale) {
+        mPreviewPipeline.setPreviewScaleFactor(previewScale);
+    }
+
+    static class Render implements Request {
+        RenderingRequest request;
+    }
+
+    static class RenderResult implements Result {
+        RenderingRequest request;
+    }
+
+    public RenderingRequestTask() {
+        mPreviewPipeline = new CachingPipeline(
+                FiltersManager.getManager(), "Normal");
+    }
+
+    public void setOriginal(Bitmap bitmap) {
+        mPreviewPipeline.setOriginal(bitmap);
+        mPipelineIsOn = true;
+    }
+
+    public void stop() {
+        mPreviewPipeline.stop();
+    }
+
+    public void postRenderingRequest(RenderingRequest request) {
+        if (!mPipelineIsOn) {
+            return;
+        }
+        Render render = new Render();
+        render.request = request;
+        postRequest(render);
+    }
+
+    @Override
+    public Result doInBackground(Request message) {
+        RenderingRequest request = ((Render) message).request;
+        RenderResult result = null;
+        mPreviewPipeline.render(request);
+        result = new RenderResult();
+        result.request = request;
+        return result;
+    }
+
+    @Override
+    public void onResult(Result message) {
+        if (message == null) {
+            return;
+        }
+        RenderingRequest request = ((RenderResult) message).request;
+        request.markAvailable();
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java b/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java
new file mode 100644
index 0000000..98e69f6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+
+public class SharedBuffer {
+
+    private static final String LOGTAG = "SharedBuffer";
+
+    private volatile Buffer mProducer = null;
+    private volatile Buffer mConsumer = null;
+    private volatile Buffer mIntermediate = null;
+
+    private volatile boolean mNeedsSwap = false;
+    private volatile boolean mNeedsRepaint = true;
+
+    public void setProducer(Bitmap producer) {
+        Buffer buffer = new Buffer(producer);
+        synchronized (this) {
+            mProducer = buffer;
+        }
+    }
+
+    public synchronized Buffer getProducer() {
+        return mProducer;
+    }
+
+    public synchronized Buffer getConsumer() {
+        return mConsumer;
+    }
+
+    public synchronized void swapProducer() {
+        Buffer intermediate = mIntermediate;
+        mIntermediate = mProducer;
+        mProducer = intermediate;
+        mNeedsSwap = true;
+    }
+
+    public synchronized void swapConsumerIfNeeded() {
+        if (!mNeedsSwap) {
+            return;
+        }
+        Buffer intermediate = mIntermediate;
+        mIntermediate = mConsumer;
+        mConsumer = intermediate;
+        mNeedsSwap = false;
+    }
+
+    public synchronized void invalidate() {
+        mNeedsRepaint = true;
+    }
+
+    public synchronized boolean checkRepaintNeeded() {
+        if (mNeedsRepaint) {
+            mNeedsRepaint = false;
+            return true;
+        }
+        return false;
+    }
+
+}
+
diff --git a/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java b/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java
new file mode 100644
index 0000000..3f850fe
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+public class SharedPreset {
+
+    private volatile ImagePreset mProducerPreset = null;
+    private volatile ImagePreset mConsumerPreset = null;
+    private volatile ImagePreset mIntermediatePreset = null;
+
+    public synchronized void enqueuePreset(ImagePreset preset) {
+        if (mProducerPreset == null || (!mProducerPreset.same(preset))) {
+            mProducerPreset = new ImagePreset(preset);
+        } else {
+            mProducerPreset.updateWith(preset);
+        }
+        ImagePreset temp = mIntermediatePreset;
+        mIntermediatePreset = mProducerPreset;
+        mProducerPreset = temp;
+    }
+
+    public synchronized ImagePreset dequeuePreset() {
+        ImagePreset temp = mConsumerPreset;
+        mConsumerPreset = mIntermediatePreset;
+        mIntermediatePreset = temp;
+        return mConsumerPreset;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java b/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java
new file mode 100644
index 0000000..406cc9b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class UpdatePreviewTask extends ProcessingTask {
+    private CachingPipeline mPreviewPipeline = null;
+    private boolean mHasUnhandledPreviewRequest = false;
+    private boolean mPipelineIsOn = false;
+
+    public UpdatePreviewTask() {
+        mPreviewPipeline = new CachingPipeline(
+                FiltersManager.getPreviewManager(), "Preview");
+    }
+
+    public void setOriginal(Bitmap bitmap) {
+        mPreviewPipeline.setOriginal(bitmap);
+        mPipelineIsOn = true;
+    }
+
+    public void updatePreview() {
+        if (!mPipelineIsOn) {
+            return;
+        }
+        mHasUnhandledPreviewRequest = true;
+        if (postRequest(null)) {
+            mHasUnhandledPreviewRequest = false;
+        }
+    }
+
+    @Override
+    public boolean isPriorityTask() {
+        return true;
+    }
+
+    @Override
+    public Result doInBackground(Request message) {
+        SharedBuffer buffer = MasterImage.getImage().getPreviewBuffer();
+        SharedPreset preset = MasterImage.getImage().getPreviewPreset();
+        ImagePreset renderingPreset = preset.dequeuePreset();
+        if (renderingPreset != null) {
+            mPreviewPipeline.compute(buffer, renderingPreset, 0);
+            // set the preset we used in the buffer for later inspection UI-side
+            buffer.getProducer().setPreset(renderingPreset);
+            buffer.getProducer().sync();
+            buffer.swapProducer(); // push back the result
+        }
+        return null;
+    }
+
+    @Override
+    public void onResult(Result message) {
+        MasterImage.getImage().notifyObservers();
+        if (mHasUnhandledPreviewRequest) {
+            updatePreview();
+        }
+    }
+
+    public void setPipelineIsOn(boolean pipelineIsOn) {
+        mPipelineIsOn = pipelineIsOn;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java b/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java
new file mode 100644
index 0000000..7ab61fc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.presets;
+
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+
+public class PresetManagementDialog extends DialogFragment implements View.OnClickListener {
+    private UserPresetsAdapter mAdapter;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.filtershow_presets_management_dialog, container);
+
+        FilterShowActivity activity = (FilterShowActivity) getActivity();
+        mAdapter = activity.getUserPresetsAdapter();
+        ListView panel = (ListView) view.findViewById(R.id.listItems);
+        panel.setAdapter(mAdapter);
+
+        view.findViewById(R.id.cancel).setOnClickListener(this);
+        view.findViewById(R.id.addpreset).setOnClickListener(this);
+        view.findViewById(R.id.ok).setOnClickListener(this);
+        getDialog().setTitle(getString(R.string.filtershow_manage_preset));
+        return view;
+    }
+
+    @Override
+    public void onClick(View v) {
+        FilterShowActivity activity = (FilterShowActivity) getActivity();
+        switch (v.getId()) {
+            case R.id.cancel:
+                mAdapter.clearChangedRepresentations();
+                mAdapter.clearDeletedRepresentations();
+                activity.updateUserPresetsFromAdapter(mAdapter);
+                dismiss();
+                break;
+            case R.id.addpreset:
+                activity.saveCurrentImagePreset();
+                dismiss();
+                break;
+            case R.id.ok:
+                mAdapter.updateCurrent();
+                activity.updateUserPresetsFromAdapter(mAdapter);
+                dismiss();
+                break;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java b/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java
new file mode 100644
index 0000000..dab9ea4
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.presets;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.category.Action;
+import com.android.gallery3d.filtershow.category.CategoryView;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+
+import java.util.ArrayList;
+
+public class UserPresetsAdapter extends ArrayAdapter<Action>
+        implements View.OnClickListener, View.OnFocusChangeListener {
+    private static final String LOGTAG = "UserPresetsAdapter";
+    private LayoutInflater mInflater;
+    private int mIconSize = 160;
+    private ArrayList<FilterUserPresetRepresentation> mDeletedRepresentations =
+            new ArrayList<FilterUserPresetRepresentation>();
+    private ArrayList<FilterUserPresetRepresentation> mChangedRepresentations =
+            new ArrayList<FilterUserPresetRepresentation>();
+    private EditText mCurrentEditText;
+
+    public UserPresetsAdapter(Context context, int textViewResourceId) {
+        super(context, textViewResourceId);
+        mInflater = LayoutInflater.from(context);
+        mIconSize = context.getResources().getDimensionPixelSize(R.dimen.category_panel_icon_size);
+    }
+
+    public UserPresetsAdapter(Context context) {
+        this(context, 0);
+    }
+
+    @Override
+    public void add(Action action) {
+        super.add(action);
+        action.setAdapter(this);
+    }
+
+    private void deletePreset(Action action) {
+        FilterRepresentation rep = action.getRepresentation();
+        if (rep instanceof FilterUserPresetRepresentation) {
+            mDeletedRepresentations.add((FilterUserPresetRepresentation) rep);
+        }
+        remove(action);
+        notifyDataSetChanged();
+    }
+
+    private void changePreset(Action action) {
+        FilterRepresentation rep = action.getRepresentation();
+        rep.setName(action.getName());
+        if (rep instanceof FilterUserPresetRepresentation) {
+            mChangedRepresentations.add((FilterUserPresetRepresentation) rep);
+        }
+    }
+
+    public void updateCurrent() {
+        if (mCurrentEditText != null) {
+            updateActionFromEditText(mCurrentEditText);
+        }
+    }
+
+    static class UserPresetViewHolder {
+        ImageView imageView;
+        EditText editText;
+        ImageButton deleteButton;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        UserPresetViewHolder viewHolder;
+        if (convertView == null) {
+            convertView = mInflater.inflate(R.layout.filtershow_presets_management_row, null);
+            viewHolder = new UserPresetViewHolder();
+            viewHolder.imageView = (ImageView) convertView.findViewById(R.id.imageView);
+            viewHolder.editText = (EditText) convertView.findViewById(R.id.editView);
+            viewHolder.deleteButton = (ImageButton) convertView.findViewById(R.id.deleteUserPreset);
+            viewHolder.editText.setOnClickListener(this);
+            viewHolder.editText.setOnFocusChangeListener(this);
+            viewHolder.deleteButton.setOnClickListener(this);
+            convertView.setTag(viewHolder);
+        } else {
+            viewHolder = (UserPresetViewHolder) convertView.getTag();
+        }
+        Action action = getItem(position);
+        viewHolder.imageView.setImageBitmap(action.getImage());
+        if (action.getImage() == null) {
+            // queue image rendering for this action
+            action.setImageFrame(new Rect(0, 0, mIconSize, mIconSize), CategoryView.VERTICAL);
+        }
+        viewHolder.deleteButton.setTag(action);
+        viewHolder.editText.setTag(action);
+        viewHolder.editText.setHint(action.getName());
+
+        return convertView;
+    }
+
+    public ArrayList<FilterUserPresetRepresentation> getDeletedRepresentations() {
+        return mDeletedRepresentations;
+    }
+
+    public void clearDeletedRepresentations() {
+        mDeletedRepresentations.clear();
+    }
+
+    public ArrayList<FilterUserPresetRepresentation> getChangedRepresentations() {
+        return mChangedRepresentations;
+    }
+
+    public void clearChangedRepresentations() {
+        mChangedRepresentations.clear();
+    }
+
+    @Override
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.editView:
+                v.requestFocus();
+                break;
+            case R.id.deleteUserPreset:
+                Action action = (Action) v.getTag();
+                deletePreset(action);
+                break;
+        }
+    }
+
+    @Override
+    public void onFocusChange(View v, boolean hasFocus) {
+        if (v.getId() != R.id.editView) {
+            return;
+        }
+        EditText editText = (EditText) v;
+        if (!hasFocus) {
+            updateActionFromEditText(editText);
+        } else {
+            mCurrentEditText = editText;
+        }
+    }
+
+    private void updateActionFromEditText(EditText editText) {
+        Action action = (Action) editText.getTag();
+        String newName = editText.getText().toString();
+        if (newName.length() > 0) {
+            action.setName(editText.getText().toString());
+            changePreset(action);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java b/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java
new file mode 100644
index 0000000..bc17a6e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+public class SharedImageProvider extends ContentProvider {
+
+    private static final String LOGTAG = "SharedImageProvider";
+
+    public static final String MIME_TYPE = "image/jpeg";
+    public static final String AUTHORITY = "com.android.gallery3d.filtershow.provider.SharedImageProvider";
+    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/image");
+    public static final String PREPARE = "prepare";
+
+    private final String[] mMimeStreamType = {
+            MIME_TYPE
+    };
+
+    private static ConditionVariable mImageReadyCond = new ConditionVariable(false);
+
+    @Override
+    public int delete(Uri arg0, String arg1, String[] arg2) {
+        return 0;
+    }
+
+    @Override
+    public String getType(Uri arg0) {
+        return MIME_TYPE;
+    }
+
+    @Override
+    public String[] getStreamTypes(Uri arg0, String mimeTypeFilter) {
+        return mMimeStreamType;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (values.containsKey(PREPARE)) {
+            if (values.getAsBoolean(PREPARE)) {
+                mImageReadyCond.close();
+            } else {
+                mImageReadyCond.open();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
+        return 0;
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+        String uriPath = uri.getLastPathSegment();
+        if (uriPath == null) {
+            return null;
+        }
+        if (projection == null) {
+            projection = new String[] {
+                    BaseColumns._ID,
+                    MediaStore.MediaColumns.DATA,
+                    OpenableColumns.DISPLAY_NAME,
+                    OpenableColumns.SIZE
+            };
+        }
+        // If we receive a query on display name or size,
+        // we should block until the image is ready
+        mImageReadyCond.block();
+
+        File path = new File(uriPath);
+
+        MatrixCursor cursor = new MatrixCursor(projection);
+        Object[] columns = new Object[projection.length];
+        for (int i = 0; i < projection.length; i++) {
+            if (projection[i].equalsIgnoreCase(BaseColumns._ID)) {
+                columns[i] = 0;
+            } else if (projection[i].equalsIgnoreCase(MediaStore.MediaColumns.DATA)) {
+                columns[i] = uri;
+            } else if (projection[i].equalsIgnoreCase(OpenableColumns.DISPLAY_NAME)) {
+                columns[i] = path.getName();
+            } else if (projection[i].equalsIgnoreCase(OpenableColumns.SIZE)) {
+                columns[i] = path.length();
+            }
+        }
+        cursor.addRow(columns);
+
+        return cursor;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode)
+            throws FileNotFoundException {
+        String uriPath = uri.getLastPathSegment();
+        if (uriPath == null) {
+            return null;
+        }
+        // Here we need to block until the image is ready
+        mImageReadyCond.block();
+        File path = new File(uriPath);
+        int imode = 0;
+        imode |= ParcelFileDescriptor.MODE_READ_ONLY;
+        return ParcelFileDescriptor.open(path, imode);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/DragListener.java b/src/com/android/gallery3d/filtershow/state/DragListener.java
new file mode 100644
index 0000000..1aa81ed
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/DragListener.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.state;
+
+import android.view.DragEvent;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+
+class DragListener implements View.OnDragListener {
+
+    private static final String LOGTAG = "DragListener";
+    private PanelTrack mStatePanelTrack;
+    private static float sSlope = 0.2f;
+
+    public DragListener(PanelTrack statePanelTrack) {
+        mStatePanelTrack = statePanelTrack;
+    }
+
+    private void setState(DragEvent event) {
+        float translation = event.getY() - mStatePanelTrack.getTouchPoint().y;
+        float alpha = 1.0f - (Math.abs(translation)
+                / mStatePanelTrack.getCurrentView().getHeight());
+        if (mStatePanelTrack.getOrientation() == LinearLayout.VERTICAL) {
+            translation = event.getX() - mStatePanelTrack.getTouchPoint().x;
+            alpha = 1.0f - (Math.abs(translation)
+                    / mStatePanelTrack.getCurrentView().getWidth());
+            mStatePanelTrack.getCurrentView().setTranslationX(translation);
+        } else {
+            mStatePanelTrack.getCurrentView().setTranslationY(translation);
+        }
+        mStatePanelTrack.getCurrentView().setBackgroundAlpha(alpha);
+    }
+
+    @Override
+    public boolean onDrag(View v, DragEvent event) {
+        switch (event.getAction()) {
+            case DragEvent.ACTION_DRAG_STARTED: {
+                break;
+            }
+            case DragEvent.ACTION_DRAG_LOCATION: {
+                if (mStatePanelTrack.getCurrentView() != null) {
+                    setState(event);
+                    View over = mStatePanelTrack.findChildAt((int) event.getX(),
+                                                             (int) event.getY());
+                    if (over != null && over != mStatePanelTrack.getCurrentView()) {
+                        StateView stateView = (StateView) over;
+                        if (stateView != mStatePanelTrack.getCurrentView()) {
+                            int pos = mStatePanelTrack.findChild(over);
+                            int origin = mStatePanelTrack.findChild(
+                                    mStatePanelTrack.getCurrentView());
+                            ArrayAdapter array = (ArrayAdapter) mStatePanelTrack.getAdapter();
+                            if (origin != -1 && pos != -1) {
+                                State current = (State) array.getItem(origin);
+                                array.remove(current);
+                                array.insert(current, pos);
+                                mStatePanelTrack.fillContent(false);
+                                mStatePanelTrack.setCurrentView(mStatePanelTrack.getChildAt(pos));
+                            }
+                        }
+                    }
+                }
+                break;
+            }
+            case DragEvent.ACTION_DRAG_ENTERED: {
+                mStatePanelTrack.setExited(false);
+                if (mStatePanelTrack.getCurrentView() != null) {
+                    mStatePanelTrack.getCurrentView().setVisibility(View.VISIBLE);
+                }
+                return true;
+            }
+            case DragEvent.ACTION_DRAG_EXITED: {
+                if (mStatePanelTrack.getCurrentView() != null) {
+                    setState(event);
+                    mStatePanelTrack.getCurrentView().setVisibility(View.INVISIBLE);
+                }
+                mStatePanelTrack.setExited(true);
+                break;
+            }
+            case DragEvent.ACTION_DROP: {
+                break;
+            }
+            case DragEvent.ACTION_DRAG_ENDED: {
+                if (mStatePanelTrack.getCurrentView() != null
+                        && mStatePanelTrack.getCurrentView().getAlpha() > sSlope) {
+                    setState(event);
+                }
+                mStatePanelTrack.checkEndState();
+                break;
+            }
+            default:
+                break;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/PanelTrack.java b/src/com/android/gallery3d/filtershow/state/PanelTrack.java
new file mode 100644
index 0000000..d02207d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/PanelTrack.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.state;
+
+import android.graphics.Point;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Adapter;
+
+public interface PanelTrack {
+    public int getOrientation();
+    public void onTouch(MotionEvent event, StateView view);
+    public StateView getCurrentView();
+    public void setCurrentView(View view);
+    public Point getTouchPoint();
+    public View findChildAt(int x, int y);
+    public int findChild(View view);
+    public Adapter getAdapter();
+    public void fillContent(boolean value);
+    public View getChildAt(int pos);
+    public void setExited(boolean value);
+    public void checkEndState();
+}
diff --git a/src/com/android/gallery3d/filtershow/state/State.java b/src/com/android/gallery3d/filtershow/state/State.java
new file mode 100644
index 0000000..e7dedd6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/State.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.state;
+
+import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+public class State {
+    private String mText;
+    private int mType;
+    private FilterRepresentation mFilterRepresentation;
+
+    public State(State state) {
+        this(state.getText(), state.getType());
+    }
+
+    public State(String text) {
+       this(text, StateView.DEFAULT);
+    }
+
+    public State(String text, int type) {
+        mText = text;
+        mType = type;
+    }
+
+    public boolean equals(State state) {
+        if (mFilterRepresentation.getFilterClass()
+                != state.mFilterRepresentation.getFilterClass()) {
+            return false;
+        }
+        if (mFilterRepresentation instanceof FilterFxRepresentation) {
+            return mFilterRepresentation.equals(state.getFilterRepresentation());
+        }
+        return true;
+    }
+
+    public boolean isDraggable() {
+        return mFilterRepresentation != null;
+    }
+
+    String getText() {
+        return mText;
+    }
+
+    void setText(String text) {
+        mText = text;
+    }
+
+    int getType() {
+        return mType;
+    }
+
+    void setType(int type) {
+        mType = type;
+    }
+
+    public FilterRepresentation getFilterRepresentation() {
+        return mFilterRepresentation;
+    }
+
+    public void setFilterRepresentation(FilterRepresentation filterRepresentation) {
+        mFilterRepresentation = filterRepresentation;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StateAdapter.java b/src/com/android/gallery3d/filtershow/state/StateAdapter.java
new file mode 100644
index 0000000..5225852
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StateAdapter.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.state;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+import java.util.Vector;
+
+public class StateAdapter extends ArrayAdapter<State> {
+
+    private static final String LOGTAG = "StateAdapter";
+    private int mOrientation;
+    private String mOriginalText;
+    private String mResultText;
+
+    public StateAdapter(Context context, int textViewResourceId) {
+        super(context, textViewResourceId);
+        mOriginalText = context.getString(R.string.state_panel_original);
+        mResultText = context.getString(R.string.state_panel_result);
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        StateView view = null;
+        if (convertView == null) {
+            convertView = new StateView(getContext());
+        }
+        view = (StateView) convertView;
+        State state = getItem(position);
+        view.setState(state);
+        view.setOrientation(mOrientation);
+        FilterRepresentation currentRep = MasterImage.getImage().getCurrentFilterRepresentation();
+        FilterRepresentation stateRep = state.getFilterRepresentation();
+        if (currentRep != null && stateRep != null
+            && currentRep.getFilterClass() == stateRep.getFilterClass()
+            && currentRep.getEditorId() != ImageOnlyEditor.ID) {
+            view.setSelected(true);
+        } else {
+            view.setSelected(false);
+        }
+        return view;
+    }
+
+    public boolean contains(State state) {
+        for (int i = 0; i < getCount(); i++) {
+            if (state == getItem(i)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setOrientation(int orientation) {
+        mOrientation = orientation;
+    }
+
+    public void addOriginal() {
+        add(new State(mOriginalText));
+    }
+
+    public boolean same(Vector<State> states) {
+        // we have the original state in addition
+        if (states.size() + 1 != getCount()) {
+            return false;
+        }
+        for (int i = 1; i < getCount(); i++) {
+            State state = getItem(i);
+            if (!state.equals(states.elementAt(i-1))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public void fill(Vector<State> states) {
+        if (same(states)) {
+            return;
+        }
+        clear();
+        addOriginal();
+        addAll(states);
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public void remove(State state) {
+        super.remove(state);
+        FilterRepresentation filterRepresentation = state.getFilterRepresentation();
+        FilterShowActivity activity = (FilterShowActivity) getContext();
+        activity.removeFilterRepresentation(filterRepresentation);
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StatePanel.java b/src/com/android/gallery3d/filtershow/state/StatePanel.java
new file mode 100644
index 0000000..df470f2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StatePanel.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.state;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class StatePanel extends Fragment {
+    private static final String LOGTAG = "StatePanel";
+    private StatePanelTrack track;
+    private LinearLayout mMainView;
+    public static final String FRAGMENT_TAG = "StatePanel";
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        mMainView = (LinearLayout) inflater.inflate(R.layout.filtershow_state_panel_new, null);
+        View panel = mMainView.findViewById(R.id.listStates);
+        track = (StatePanelTrack) panel;
+        track.setAdapter(MasterImage.getImage().getState());
+        return mMainView;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java b/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java
new file mode 100644
index 0000000..fff7e7f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.state;
+
+import android.animation.LayoutTransition;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class StatePanelTrack extends LinearLayout implements PanelTrack {
+
+    private static final String LOGTAG = "StatePanelTrack";
+    private Point mTouchPoint;
+    private StateView mCurrentView;
+    private StateView mCurrentSelectedView;
+    private boolean mExited = false;
+    private boolean mStartedDrag = false;
+    private StateAdapter mAdapter;
+    private DragListener mDragListener = new DragListener(this);
+    private float mDeleteSlope = 0.2f;
+    private GestureDetector mGestureDetector;
+    private int mElemWidth;
+    private int mElemHeight;
+    private int mElemSize;
+    private int mElemEndSize;
+    private int mEndElemWidth;
+    private int mEndElemHeight;
+    private long mTouchTime;
+    private int mMaxTouchDelay = 300; // 300ms delay for touch
+    private static final boolean ALLOWS_DRAG = false;
+    private DataSetObserver mObserver = new DataSetObserver() {
+        @Override
+        public void onChanged() {
+            super.onChanged();
+            fillContent(false);
+        }
+
+        @Override
+        public void onInvalidated() {
+            super.onInvalidated();
+            fillContent(false);
+        }
+    };
+
+    public StatePanelTrack(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.StatePanelTrack);
+        mElemSize = a.getDimensionPixelSize(R.styleable.StatePanelTrack_elemSize, 0);
+        mElemEndSize = a.getDimensionPixelSize(R.styleable.StatePanelTrack_elemEndSize, 0);
+        if (getOrientation() == LinearLayout.HORIZONTAL) {
+            mElemWidth = mElemSize;
+            mElemHeight = LayoutParams.MATCH_PARENT;
+            mEndElemWidth = mElemEndSize;
+            mEndElemHeight = LayoutParams.MATCH_PARENT;
+        } else {
+            mElemWidth = LayoutParams.MATCH_PARENT;
+            mElemHeight = mElemSize;
+            mEndElemWidth = LayoutParams.MATCH_PARENT;
+            mEndElemHeight = mElemEndSize;
+        }
+        GestureDetector.SimpleOnGestureListener simpleOnGestureListener
+                = new GestureDetector.SimpleOnGestureListener(){
+            @Override
+            public void onLongPress(MotionEvent e) {
+                longPress(e);
+            }
+            @Override
+            public boolean onDoubleTap(MotionEvent e) {
+                addDuplicate(e);
+                return true;
+            }
+        };
+        mGestureDetector = new GestureDetector(context, simpleOnGestureListener);
+    }
+
+    private void addDuplicate(MotionEvent e) {
+        if (mCurrentSelectedView == null) {
+            return;
+        }
+        int pos = findChild(mCurrentSelectedView);
+        if (pos != -1) {
+            mAdapter.insert(new State(mCurrentSelectedView.getState()), pos);
+            fillContent(true);
+        }
+    }
+
+    private void longPress(MotionEvent e) {
+        View view = findChildAt((int) e.getX(), (int) e.getY());
+        if (view == null) {
+            return;
+        }
+        if (view instanceof StateView) {
+            StateView stateView = (StateView) view;
+            stateView.setDuplicateButton(true);
+        }
+    }
+
+    public void setAdapter(StateAdapter adapter) {
+        mAdapter = adapter;
+        mAdapter.registerDataSetObserver(mObserver);
+        mAdapter.setOrientation(getOrientation());
+        fillContent(false);
+        requestLayout();
+    }
+
+    public StateView findChildWithState(State state) {
+        for (int i = 0; i < getChildCount(); i++) {
+            StateView view = (StateView) getChildAt(i);
+            if (view.getState() == state) {
+                return view;
+            }
+        }
+        return null;
+    }
+
+    public void fillContent(boolean animate) {
+        if (!animate) {
+            this.setLayoutTransition(null);
+        }
+        int n = mAdapter.getCount();
+        for (int i = 0; i < getChildCount(); i++) {
+            StateView child = (StateView) getChildAt(i);
+            child.resetPosition();
+            if (!mAdapter.contains(child.getState())) {
+                removeView(child);
+            }
+        }
+        LayoutParams params = new LayoutParams(mElemWidth, mElemHeight);
+        for (int i = 0; i < n; i++) {
+            State s = mAdapter.getItem(i);
+            if (findChildWithState(s) == null) {
+                View view = mAdapter.getView(i, null, this);
+                addView(view, i, params);
+            }
+        }
+
+        for (int i = 0; i < n; i++) {
+            State state = mAdapter.getItem(i);
+            StateView view = (StateView) getChildAt(i);
+            view.setState(state);
+            if (i == 0) {
+                view.setType(StateView.BEGIN);
+            } else if (i == n - 1) {
+                view.setType(StateView.END);
+            } else {
+                view.setType(StateView.DEFAULT);
+            }
+            view.resetPosition();
+        }
+
+        if (!animate) {
+            this.setLayoutTransition(new LayoutTransition());
+        }
+    }
+
+    public void onTouch(MotionEvent event, StateView view) {
+        if (!view.isDraggable()) {
+            return;
+        }
+        mCurrentView = view;
+        if (mCurrentSelectedView == mCurrentView) {
+            return;
+        }
+        if (mCurrentSelectedView != null) {
+            mCurrentSelectedView.setSelected(false);
+        }
+        // We changed the current view -- let's reset the
+        // gesture detector.
+        MotionEvent cancelEvent = MotionEvent.obtain(event);
+        cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
+        mGestureDetector.onTouchEvent(cancelEvent);
+        mCurrentSelectedView = mCurrentView;
+        // We have to send the event to the gesture detector
+        mGestureDetector.onTouchEvent(event);
+        mTouchTime = System.currentTimeMillis();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mCurrentView != null) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mCurrentView == null) {
+            return false;
+        }
+        if (mTouchTime == 0) {
+            mTouchTime = System.currentTimeMillis();
+        }
+        mGestureDetector.onTouchEvent(event);
+        if (mTouchPoint == null) {
+            mTouchPoint = new Point();
+            mTouchPoint.x = (int) event.getX();
+            mTouchPoint.y = (int) event.getY();
+        }
+
+        if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
+            float translation = event.getY() - mTouchPoint.y;
+            float alpha = 1.0f - (Math.abs(translation) / mCurrentView.getHeight());
+            if (getOrientation() == LinearLayout.VERTICAL) {
+                translation = event.getX() - mTouchPoint.x;
+                alpha = 1.0f - (Math.abs(translation) / mCurrentView.getWidth());
+                mCurrentView.setTranslationX(translation);
+            } else {
+                mCurrentView.setTranslationY(translation);
+            }
+            mCurrentView.setBackgroundAlpha(alpha);
+            if (ALLOWS_DRAG && alpha < 0.7) {
+                setOnDragListener(mDragListener);
+                DragShadowBuilder shadowBuilder = new DragShadowBuilder(mCurrentView);
+                mCurrentView.startDrag(null, shadowBuilder, mCurrentView, 0);
+                mStartedDrag = true;
+            }
+        }
+        if (!mExited && mCurrentView != null
+                && mCurrentView.getBackgroundAlpha() > mDeleteSlope
+                && event.getActionMasked() == MotionEvent.ACTION_UP
+                && System.currentTimeMillis() - mTouchTime < mMaxTouchDelay) {
+            FilterRepresentation representation = mCurrentView.getState().getFilterRepresentation();
+            mCurrentView.setSelected(true);
+            if (representation != MasterImage.getImage().getCurrentFilterRepresentation()) {
+                FilterShowActivity activity = (FilterShowActivity) getContext();
+                activity.showRepresentation(representation);
+                mCurrentView.setSelected(false);
+            }
+        }
+        if (event.getActionMasked() == MotionEvent.ACTION_UP
+                || (!mStartedDrag && event.getActionMasked() == MotionEvent.ACTION_CANCEL)) {
+            checkEndState();
+            if (mCurrentView != null) {
+                FilterRepresentation representation = mCurrentView.getState().getFilterRepresentation();
+                if (representation.getEditorId() == ImageOnlyEditor.ID) {
+                    mCurrentView.setSelected(false);
+                }
+            }
+        }
+        return true;
+    }
+
+    public void checkEndState() {
+        mTouchPoint = null;
+        mTouchTime = 0;
+        if (mExited || mCurrentView.getBackgroundAlpha() < mDeleteSlope) {
+            int origin = findChild(mCurrentView);
+            if (origin != -1) {
+                State current = mAdapter.getItem(origin);
+                FilterRepresentation currentRep = MasterImage.getImage().getCurrentFilterRepresentation();
+                FilterRepresentation removedRep = current.getFilterRepresentation();
+                mAdapter.remove(current);
+                fillContent(true);
+                if (currentRep != null && removedRep != null
+                        && currentRep.getFilterClass() == removedRep.getFilterClass()) {
+                    FilterShowActivity activity = (FilterShowActivity) getContext();
+                    activity.backToMain();
+                    return;
+                }
+            }
+        } else {
+            mCurrentView.setBackgroundAlpha(1.0f);
+            mCurrentView.setTranslationX(0);
+            mCurrentView.setTranslationY(0);
+        }
+        if (mCurrentSelectedView != null) {
+            mCurrentSelectedView.invalidate();
+        }
+        if (mCurrentView != null) {
+            mCurrentView.invalidate();
+        }
+        mCurrentView = null;
+        mExited = false;
+        mStartedDrag = false;
+    }
+
+    public View findChildAt(int x, int y) {
+        Rect frame = new Rect();
+        int scrolledXInt = getScrollX() + x;
+        int scrolledYInt = getScrollY() + y;
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            child.getHitRect(frame);
+            if (frame.contains(scrolledXInt, scrolledYInt)) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    public int findChild(View view) {
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (child == view) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public StateView getCurrentView() {
+        return mCurrentView;
+    }
+
+    public void setCurrentView(View currentView) {
+        mCurrentView = (StateView) currentView;
+    }
+
+    public void setExited(boolean value) {
+        mExited = value;
+    }
+
+    public Point getTouchPoint() {
+        return mTouchPoint;
+    }
+
+    public Adapter getAdapter() {
+        return mAdapter;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StateView.java b/src/com/android/gallery3d/filtershow/state/StateView.java
new file mode 100644
index 0000000..73d5784
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StateView.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.state;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.*;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class StateView extends View {
+
+    private static final String LOGTAG = "StateView";
+    private Path mPath = new Path();
+    private Paint mPaint = new Paint();
+
+    public static int DEFAULT = 0;
+    public static int BEGIN = 1;
+    public static int END = 2;
+
+    public static int UP = 1;
+    public static int DOWN = 2;
+    public static int LEFT = 3;
+    public static int RIGHT = 4;
+
+    private int mType = DEFAULT;
+    private float mAlpha = 1.0f;
+    private String mText = "Default";
+    private float mTextSize = 32;
+    private static int sMargin = 16;
+    private static int sArrowHeight = 16;
+    private static int sArrowWidth = 8;
+    private int mOrientation = LinearLayout.VERTICAL;
+    private int mDirection = DOWN;
+    private boolean mDuplicateButton;
+    private State mState;
+
+    private int mEndsBackgroundColor;
+    private int mEndsTextColor;
+    private int mBackgroundColor;
+    private int mTextColor;
+    private int mSelectedBackgroundColor;
+    private int mSelectedTextColor;
+    private Rect mTextBounds = new Rect();
+
+    public StateView(Context context) {
+        this(context, DEFAULT);
+    }
+
+    public StateView(Context context, int type) {
+        super(context);
+        mType = type;
+        Resources res = getResources();
+        mEndsBackgroundColor = res.getColor(R.color.filtershow_stateview_end_background);
+        mEndsTextColor = res.getColor(R.color.filtershow_stateview_end_text);
+        mBackgroundColor = res.getColor(R.color.filtershow_stateview_background);
+        mTextColor = res.getColor(R.color.filtershow_stateview_text);
+        mSelectedBackgroundColor = res.getColor(R.color.filtershow_stateview_selected_background);
+        mSelectedTextColor = res.getColor(R.color.filtershow_stateview_selected_text);
+        mTextSize = res.getDimensionPixelSize(R.dimen.state_panel_text_size);
+    }
+
+    public String getText() {
+        return mText;
+    }
+
+    public void setText(String text) {
+        mText = text;
+        invalidate();
+    }
+
+    public void setType(int type) {
+        mType = type;
+        invalidate();
+    }
+
+    @Override
+    public void setSelected(boolean value) {
+        super.setSelected(value);
+        if (!value) {
+            mDuplicateButton = false;
+        }
+        invalidate();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            ViewParent parent = getParent();
+            if (parent instanceof PanelTrack) {
+                ((PanelTrack) getParent()).onTouch(event, this);
+            }
+            if (mType == BEGIN) {
+                MasterImage.getImage().setShowsOriginal(true);
+            }
+        }
+        if (event.getActionMasked() == MotionEvent.ACTION_UP
+                || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+            MasterImage.getImage().setShowsOriginal(false);
+        }
+        return true;
+    }
+
+    public void drawText(Canvas canvas) {
+        if (mText == null) {
+            return;
+        }
+        mPaint.reset();
+        if (isSelected()) {
+            mPaint.setColor(mSelectedTextColor);
+        } else {
+            mPaint.setColor(mTextColor);
+        }
+        if (mType == BEGIN) {
+            mPaint.setColor(mEndsTextColor);
+        }
+        mPaint.setTypeface(Typeface.DEFAULT_BOLD);
+        mPaint.setAntiAlias(true);
+        mPaint.setTextSize(mTextSize);
+        mPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
+        int x = (canvas.getWidth() - mTextBounds.width()) / 2;
+        int y = mTextBounds.height() + (canvas.getHeight() - mTextBounds.height()) / 2;
+        canvas.drawText(mText, x, y, mPaint);
+    }
+
+    public void onDraw(Canvas canvas) {
+        canvas.drawARGB(0, 0, 0, 0);
+        mPaint.reset();
+        mPath.reset();
+
+        float w = canvas.getWidth();
+        float h = canvas.getHeight();
+        float r = sArrowHeight;
+        float d = sArrowWidth;
+
+        if (mOrientation == LinearLayout.HORIZONTAL) {
+            drawHorizontalPath(w, h, r, d);
+        } else {
+            if (mDirection == DOWN) {
+                drawVerticalDownPath(w, h, r, d);
+            } else {
+                drawVerticalPath(w, h, r, d);
+            }
+        }
+
+        if (mType == DEFAULT || mType == END) {
+            if (mDuplicateButton) {
+                mPaint.setARGB(255, 200, 0, 0);
+            } else if (isSelected()) {
+                mPaint.setColor(mSelectedBackgroundColor);
+            } else {
+                mPaint.setColor(mBackgroundColor);
+            }
+        } else {
+            mPaint.setColor(mEndsBackgroundColor);
+        }
+        canvas.drawPath(mPath, mPaint);
+        drawText(canvas);
+    }
+
+    private void drawHorizontalPath(float w, float h, float r, float d) {
+        mPath.moveTo(0, 0);
+        if (mType == END) {
+            mPath.lineTo(w, 0);
+            mPath.lineTo(w, h);
+        } else {
+            mPath.lineTo(w - d, 0);
+            mPath.lineTo(w - d, r);
+            mPath.lineTo(w, r + d);
+            mPath.lineTo(w - d, r + d + r);
+            mPath.lineTo(w - d, h);
+        }
+        mPath.lineTo(0, h);
+        if (mType != BEGIN) {
+            mPath.lineTo(0, r + d + r);
+            mPath.lineTo(d, r + d);
+            mPath.lineTo(0, r);
+        }
+        mPath.close();
+    }
+
+    private void drawVerticalPath(float w, float h, float r, float d) {
+        if (mType == BEGIN) {
+            mPath.moveTo(0, 0);
+            mPath.lineTo(w, 0);
+        } else {
+            mPath.moveTo(0, d);
+            mPath.lineTo(r, d);
+            mPath.lineTo(r + d, 0);
+            mPath.lineTo(r + d + r, d);
+            mPath.lineTo(w, d);
+        }
+        mPath.lineTo(w, h);
+        if (mType != END) {
+            mPath.lineTo(r + d + r, h);
+            mPath.lineTo(r + d, h - d);
+            mPath.lineTo(r, h);
+        }
+        mPath.lineTo(0, h);
+        mPath.close();
+    }
+
+    private void drawVerticalDownPath(float w, float h, float r, float d) {
+        mPath.moveTo(0, 0);
+        if (mType != BEGIN) {
+            mPath.lineTo(r, 0);
+            mPath.lineTo(r + d, d);
+            mPath.lineTo(r + d + r, 0);
+        }
+        mPath.lineTo(w, 0);
+
+        if (mType != END) {
+            mPath.lineTo(w, h - d);
+
+            mPath.lineTo(r + d + r, h - d);
+            mPath.lineTo(r + d, h);
+            mPath.lineTo(r, h - d);
+
+            mPath.lineTo(0, h - d);
+        } else {
+            mPath.lineTo(w, h);
+            mPath.lineTo(0, h);
+        }
+
+        mPath.close();
+    }
+
+    public void setBackgroundAlpha(float alpha) {
+        if (mType == BEGIN) {
+            return;
+        }
+        mAlpha = alpha;
+        setAlpha(alpha);
+        invalidate();
+    }
+
+    public float getBackgroundAlpha() {
+        return mAlpha;
+    }
+
+    public void setOrientation(int orientation) {
+        mOrientation = orientation;
+    }
+
+    public void setDuplicateButton(boolean b) {
+        mDuplicateButton = b;
+        invalidate();
+    }
+
+    public State getState() {
+        return mState;
+    }
+
+    public void setState(State state) {
+        mState = state;
+        mText = mState.getText().toUpperCase();
+        mType = mState.getType();
+        invalidate();
+    }
+
+    public void resetPosition() {
+        setTranslationX(0);
+        setTranslationY(0);
+        setBackgroundAlpha(1.0f);
+    }
+
+    public boolean isDraggable() {
+        return mState.isDraggable();
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/IconFactory.java b/src/com/android/gallery3d/filtershow/tools/IconFactory.java
new file mode 100644
index 0000000..9e39f27
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/IconFactory.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.tools;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+/**
+ * A factory class for producing bitmaps to use as UI icons.
+ */
+public class IconFactory {
+
+    /**
+     * Builds an icon with the dimensions iconWidth:iconHeight. If scale is set
+     * the source image is stretched to fit within the given dimensions;
+     * otherwise, the source image is cropped to the proper aspect ratio.
+     *
+     * @param sourceImage image to create an icon from.
+     * @param iconWidth width of the icon bitmap.
+     * @param iconHeight height of the icon bitmap.
+     * @param scale if true, stretch sourceImage to fit the icon dimensions.
+     * @return an icon bitmap with the dimensions iconWidth:iconHeight.
+     */
+    public static Bitmap createIcon(Bitmap sourceImage, int iconWidth, int iconHeight,
+            boolean scale) {
+        if (sourceImage == null) {
+            throw new IllegalArgumentException("Null argument to buildIcon");
+        }
+
+        int sourceWidth = sourceImage.getWidth();
+        int sourceHeight = sourceImage.getHeight();
+
+        if (sourceWidth == 0 || sourceHeight == 0 || iconWidth == 0 || iconHeight == 0) {
+            throw new IllegalArgumentException("Bitmap with dimension 0 used as input");
+        }
+
+        Bitmap icon = Bitmap.createBitmap(iconWidth, iconHeight,
+                Bitmap.Config.ARGB_8888);
+        drawIcon(icon, sourceImage, scale);
+        return icon;
+    }
+
+    /**
+     * Draws an icon in the destination bitmap. If scale is set the source image
+     * is stretched to fit within the destination dimensions; otherwise, the
+     * source image is cropped to the proper aspect ratio.
+     *
+     * @param dest bitmap into which to draw the icon.
+     * @param sourceImage image to create an icon from.
+     * @param scale if true, stretch sourceImage to fit the destination.
+     */
+    public static void drawIcon(Bitmap dest, Bitmap sourceImage, boolean scale) {
+        if (dest == null || sourceImage == null) {
+            throw new IllegalArgumentException("Null argument to buildIcon");
+        }
+
+        int sourceWidth = sourceImage.getWidth();
+        int sourceHeight = sourceImage.getHeight();
+        int iconWidth = dest.getWidth();
+        int iconHeight = dest.getHeight();
+
+        if (sourceWidth == 0 || sourceHeight == 0 || iconWidth == 0 || iconHeight == 0) {
+            throw new IllegalArgumentException("Bitmap with dimension 0 used as input");
+        }
+
+        Rect destRect = new Rect(0, 0, iconWidth, iconHeight);
+        Canvas canvas = new Canvas(dest);
+
+        Rect srcRect = null;
+        if (scale) {
+            // scale image to fit in icon (stretches if aspect isn't the same)
+            srcRect = new Rect(0, 0, sourceWidth, sourceHeight);
+        } else {
+            // crop image to aspect ratio iconWidth:iconHeight
+            float wScale = sourceWidth / (float) iconWidth;
+            float hScale = sourceHeight / (float) iconHeight;
+            float s = Math.min(hScale, wScale);
+
+            float iw = iconWidth * s;
+            float ih = iconHeight * s;
+
+            float borderW = (sourceWidth - iw) / 2.0f;
+            float borderH = (sourceHeight - ih) / 2.0f;
+            RectF rec = new RectF(borderW, borderH, borderW + iw, borderH + ih);
+            srcRect = new Rect();
+            rec.roundOut(srcRect);
+        }
+
+        canvas.drawBitmap(sourceImage, srcRect, destRect, new Paint(Paint.FILTER_BITMAP_FLAG));
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/MatrixFit.java b/src/com/android/gallery3d/filtershow/tools/MatrixFit.java
new file mode 100644
index 0000000..3b81567
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/MatrixFit.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.tools;
+
+import android.util.Log;
+
+public class MatrixFit {
+    // Simple implementation of a matrix fit in N dimensions.
+
+    private static final String LOGTAG = "MatrixFit";
+
+    private double[][] mMatrix;
+    private int mDimension;
+    private boolean mValid = false;
+    private static double sEPS = 1.0f/10000000000.0f;
+
+    public MatrixFit(double[][] from, double[][] to) {
+        mValid = fit(from, to);
+    }
+
+    public int getDimension() {
+        return mDimension;
+    }
+
+    public boolean isValid() {
+        return mValid;
+    }
+
+    public double[][] getMatrix() {
+        return mMatrix;
+    }
+
+    public boolean fit(double[][] from, double[][] to) {
+        if ((from.length != to.length) || (from.length < 1)) {
+            Log.e(LOGTAG, "from and to must be of same size");
+            return false;
+        }
+
+        mDimension = from[0].length;
+        mMatrix = new double[mDimension +1][mDimension + mDimension +1];
+
+        if (from.length < mDimension) {
+            Log.e(LOGTAG, "Too few points => under-determined system");
+            return false;
+        }
+
+        double[][] q = new double[from.length][mDimension];
+        for (int i = 0; i < from.length; i++) {
+            for (int j = 0; j < mDimension; j++) {
+                q[i][j] = from[i][j];
+            }
+        }
+
+        double[][] p = new double[to.length][mDimension];
+        for (int i = 0; i < to.length; i++) {
+            for (int j = 0; j < mDimension; j++) {
+                p[i][j] = to[i][j];
+            }
+        }
+
+        // Make an empty (dim) x (dim + 1) matrix and fill it
+        double[][] c = new double[mDimension+1][mDimension];
+        for (int j = 0; j < mDimension; j++) {
+            for (int k = 0; k < mDimension + 1; k++) {
+                for (int i = 0; i < q.length; i++) {
+                    double qt = 1;
+                    if (k < mDimension) {
+                        qt = q[i][k];
+                    }
+                    c[k][j] += qt * p[i][j];
+                }
+            }
+        }
+
+        // Make an empty (dim+1) x (dim+1) matrix and fill it
+        double[][] Q = new double[mDimension+1][mDimension+1];
+        for (int qi = 0; qi < q.length; qi++) {
+            double[] qt = new double[mDimension + 1];
+            for (int i = 0; i < mDimension; i++) {
+                qt[i] = q[qi][i];
+            }
+            qt[mDimension] = 1;
+            for (int i = 0; i < mDimension + 1; i++) {
+                for (int j = 0; j < mDimension + 1; j++) {
+                    Q[i][j] += qt[i] * qt[j];
+                }
+            }
+        }
+
+        // Use a gaussian elimination to solve the linear system
+        for (int i = 0; i < mDimension + 1; i++) {
+            for (int j = 0; j < mDimension + 1; j++) {
+                mMatrix[i][j] = Q[i][j];
+            }
+            for (int j = 0; j < mDimension; j++) {
+                mMatrix[i][mDimension + 1 + j] = c[i][j];
+            }
+        }
+        if (!gaussianElimination(mMatrix)) {
+            return false;
+        }
+        return true;
+    }
+
+    public double[] apply(double[] point) {
+        if (mDimension != point.length) {
+            return null;
+        }
+        double[] res = new double[mDimension];
+        for (int j = 0; j < mDimension; j++) {
+            for (int i = 0; i < mDimension; i++) {
+                res[j] += point[i] * mMatrix[i][j+ mDimension +1];
+            }
+            res[j] += mMatrix[mDimension][j+ mDimension +1];
+        }
+        return res;
+    }
+
+    public void printEquation() {
+        for (int j = 0; j < mDimension; j++) {
+            String str = "x" + j + "' = ";
+            for (int i = 0; i < mDimension; i++) {
+                str += "x" + i + " * " + mMatrix[i][j+mDimension+1] + " + ";
+            }
+            str += mMatrix[mDimension][j+mDimension+1];
+            Log.v(LOGTAG, str);
+        }
+    }
+
+    private void printMatrix(String name, double[][] matrix) {
+        Log.v(LOGTAG, "name: " + name);
+        for (int i = 0; i < matrix.length; i++) {
+            String str = "";
+            for (int j = 0; j < matrix[0].length; j++) {
+                str += "" + matrix[i][j] + " ";
+            }
+            Log.v(LOGTAG, str);
+        }
+    }
+
+    /*
+     * Transforms the given matrix into a row echelon matrix
+     */
+    private boolean gaussianElimination(double[][] m) {
+        int h = m.length;
+        int w = m[0].length;
+
+        for (int y = 0; y < h; y++) {
+            int maxrow = y;
+            for (int y2 = y + 1; y2 < h; y2++) { // Find max pivot
+                if (Math.abs(m[y2][y]) > Math.abs(m[maxrow][y])) {
+                    maxrow = y2;
+                }
+            }
+            // swap
+            for (int i = 0; i < mDimension; i++) {
+                double t = m[y][i];
+                m[y][i] = m[maxrow][i];
+                m[maxrow][i] = t;
+            }
+
+            if (Math.abs(m[y][y]) <= sEPS) { // Singular Matrix
+                return false;
+            }
+            for (int y2 = y + 1; y2 < h; y2++) { // Eliminate column y
+                double c = m[y2][y] / m[y][y];
+                for (int x = y; x < w; x++) {
+                    m[y2][x] -= m[y][x] * c;
+                }
+            }
+        }
+        for (int y = h -1; y > -1; y--) { // Back substitution
+            double c = m[y][y];
+            for (int y2 = 0; y2 < y; y2++) {
+                for (int x = w - 1; x > y - 1; x--) {
+                    m[y2][x] -= m[y][x] * m[y2][y] / c;
+                }
+            }
+            m[y][y] /= c;
+            for (int x = h; x < w; x++) { // Normalize row y
+                m[y][x] /= c;
+            }
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveImage.java b/src/com/android/gallery3d/filtershow/tools/SaveImage.java
new file mode 100644
index 0000000..83cbd01
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/SaveImage.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.filtershow.tools;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.CachingPipeline;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
+import com.android.gallery3d.util.UsageStatistics;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+
+/**
+ * Handles saving edited photo
+ */
+public class SaveImage {
+    private static final String LOGTAG = "SaveImage";
+
+    /**
+     * Callback for updates
+     */
+    public interface Callback {
+        void onProgress(int max, int current);
+    }
+
+    public interface ContentResolverQueryCallback {
+        void onCursorResult(Cursor cursor);
+    }
+
+    private static final String TIME_STAMP_NAME = "_yyyyMMdd_HHmmss";
+    private static final String PREFIX_PANO = "PANO";
+    private static final String PREFIX_IMG = "IMG";
+    private static final String POSTFIX_JPG = ".jpg";
+    private static final String AUX_DIR_NAME = ".aux";
+
+    private final Context mContext;
+    private final Uri mSourceUri;
+    private final Callback mCallback;
+    private final File mDestinationFile;
+    private final Uri mSelectedImageUri;
+
+    private int mCurrentProcessingStep = 1;
+
+    public static final int MAX_PROCESSING_STEPS = 6;
+    public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
+
+    // In order to support the new edit-save behavior such that user won't see
+    // the edited image together with the original image, we are adding a new
+    // auxiliary directory for the edited image. Basically, the original image
+    // will be hidden in that directory after edit and user will see the edited
+    // image only.
+    // Note that deletion on the edited image will also cause the deletion of
+    // the original image under auxiliary directory.
+    //
+    // There are several situations we need to consider:
+    // 1. User edit local image local01.jpg. A local02.jpg will be created in the
+    // same directory, and original image will be moved to auxiliary directory as
+    // ./.aux/local02.jpg.
+    // If user edit the local02.jpg, local03.jpg will be created in the local
+    // directory and ./.aux/local02.jpg will be renamed to ./.aux/local03.jpg
+    //
+    // 2. User edit remote image remote01.jpg from picassa or other server.
+    // remoteSavedLocal01.jpg will be saved under proper local directory.
+    // In remoteSavedLocal01.jpg, there will be a reference pointing to the
+    // remote01.jpg. There will be no local copy of remote01.jpg.
+    // If user edit remoteSavedLocal01.jpg, then a new remoteSavedLocal02.jpg
+    // will be generated and still pointing to the remote01.jpg
+    //
+    // 3. User delete any local image local.jpg.
+    // Since the filenames are kept consistent in auxiliary directory, every
+    // time a local.jpg get deleted, the files in auxiliary directory whose
+    // names starting with "local." will be deleted.
+    // This pattern will facilitate the multiple images deletion in the auxiliary
+    // directory.
+
+    /**
+     * @param context
+     * @param sourceUri The Uri for the original image, which can be the hidden
+     *  image under the auxiliary directory or the same as selectedImageUri.
+     * @param selectedImageUri The Uri for the image selected by the user.
+     *  In most cases, it is a content Uri for local image or remote image.
+     * @param destination Destinaton File, if this is null, a new file will be
+     *  created under the same directory as selectedImageUri.
+     * @param callback Let the caller know the saving has completed.
+     * @return the newSourceUri
+     */
+    public SaveImage(Context context, Uri sourceUri, Uri selectedImageUri,
+                     File destination, Callback callback)  {
+        mContext = context;
+        mSourceUri = sourceUri;
+        mCallback = callback;
+        if (destination == null) {
+            mDestinationFile = getNewFile(context, selectedImageUri);
+        } else {
+            mDestinationFile = destination;
+        }
+
+        mSelectedImageUri = selectedImageUri;
+    }
+
+    public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
+        File saveDirectory = SaveImage.getSaveDirectory(context, sourceUri);
+        if ((saveDirectory == null) || !saveDirectory.canWrite()) {
+            saveDirectory = new File(Environment.getExternalStorageDirectory(),
+                    SaveImage.DEFAULT_SAVE_DIRECTORY);
+        }
+        // Create the directory if it doesn't exist
+        if (!saveDirectory.exists())
+            saveDirectory.mkdirs();
+        return saveDirectory;
+    }
+
+    public static File getNewFile(Context context, Uri sourceUri) {
+        File saveDirectory = getFinalSaveDirectory(context, sourceUri);
+        String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
+                System.currentTimeMillis()));
+        if (hasPanoPrefix(context, sourceUri)) {
+            return new File(saveDirectory, PREFIX_PANO + filename + POSTFIX_JPG);
+        }
+        return new File(saveDirectory, PREFIX_IMG + filename + POSTFIX_JPG);
+    }
+
+    /**
+     * Remove the files in the auxiliary directory whose names are the same as
+     * the source image.
+     * @param contentResolver The application's contentResolver
+     * @param srcContentUri The content Uri for the source image.
+     */
+    public static void deleteAuxFiles(ContentResolver contentResolver,
+            Uri srcContentUri) {
+        final String[] fullPath = new String[1];
+        String[] queryProjection = new String[] { ImageColumns.DATA };
+        querySourceFromContentResolver(contentResolver,
+                srcContentUri, queryProjection,
+                new ContentResolverQueryCallback() {
+                    @Override
+                    public void onCursorResult(Cursor cursor) {
+                        fullPath[0] = cursor.getString(0);
+                    }
+                }
+        );
+        if (fullPath[0] != null) {
+            // Construct the auxiliary directory given the source file's path.
+            // Then select and delete all the files starting with the same name
+            // under the auxiliary directory.
+            File currentFile = new File(fullPath[0]);
+
+            String filename = currentFile.getName();
+            int firstDotPos = filename.indexOf(".");
+            final String filenameNoExt = (firstDotPos == -1) ? filename :
+                filename.substring(0, firstDotPos);
+            File auxDir = getLocalAuxDirectory(currentFile);
+            if (auxDir.exists()) {
+                FilenameFilter filter = new FilenameFilter() {
+                    @Override
+                    public boolean accept(File dir, String name) {
+                        if (name.startsWith(filenameNoExt + ".")) {
+                            return true;
+                        } else {
+                            return false;
+                        }
+                    }
+                };
+
+                // Delete all auxiliary files whose name is matching the
+                // current local image.
+                File[] auxFiles = auxDir.listFiles(filter);
+                for (File file : auxFiles) {
+                    file.delete();
+                }
+            }
+        }
+    }
+
+    public Object getPanoramaXMPData(Uri source, ImagePreset preset) {
+        Object xmp = null;
+        if (preset.isPanoramaSafe()) {
+            InputStream is = null;
+            try {
+                is = mContext.getContentResolver().openInputStream(source);
+                xmp = XmpUtilHelper.extractXMPMeta(is);
+            } catch (FileNotFoundException e) {
+                Log.w(LOGTAG, "Failed to get XMP data from image: ", e);
+            } finally {
+                Utils.closeSilently(is);
+            }
+        }
+        return xmp;
+    }
+
+    public boolean putPanoramaXMPData(File file, Object xmp) {
+        if (xmp != null) {
+            return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp);
+        }
+        return false;
+    }
+
+    public ExifInterface getExifData(Uri source) {
+        ExifInterface exif = new ExifInterface();
+        String mimeType = mContext.getContentResolver().getType(mSelectedImageUri);
+        if (mimeType == null) {
+            mimeType = ImageLoader.getMimeType(mSelectedImageUri);
+        }
+        if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) {
+            InputStream inStream = null;
+            try {
+                inStream = mContext.getContentResolver().openInputStream(source);
+                exif.readExif(inStream);
+            } catch (FileNotFoundException e) {
+                Log.w(LOGTAG, "Cannot find file: " + source, e);
+            } catch (IOException e) {
+                Log.w(LOGTAG, "Cannot read exif for: " + source, e);
+            } finally {
+                Utils.closeSilently(inStream);
+            }
+        }
+        return exif;
+    }
+
+    public boolean putExifData(File file, ExifInterface exif, Bitmap image,
+            int jpegCompressQuality) {
+        boolean ret = false;
+        OutputStream s = null;
+        try {
+            s = exif.getExifWriterStream(file.getAbsolutePath());
+            image.compress(Bitmap.CompressFormat.JPEG,
+                    (jpegCompressQuality > 0) ? jpegCompressQuality : 1, s);
+            s.flush();
+            s.close();
+            s = null;
+            ret = true;
+        } catch (FileNotFoundException e) {
+            Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e);
+        } catch (IOException e) {
+            Log.w(LOGTAG, "Could not write exif: ", e);
+        } finally {
+            Utils.closeSilently(s);
+        }
+        return ret;
+    }
+
+    private Uri resetToOriginalImageIfNeeded(ImagePreset preset, boolean doAuxBackup) {
+        Uri uri = null;
+        if (!preset.hasModifications()) {
+            // This can happen only when preset has no modification but save
+            // button is enabled, it means the file is loaded with filters in
+            // the XMP, then all the filters are removed or restore to default.
+            // In this case, when mSourceUri exists, rename it to the
+            // destination file.
+            File srcFile = getLocalFileFromUri(mContext, mSourceUri);
+            // If the source is not a local file, then skip this renaming and
+            // create a local copy as usual.
+            if (srcFile != null) {
+                srcFile.renameTo(mDestinationFile);
+                uri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri,
+                        mDestinationFile, System.currentTimeMillis(), doAuxBackup);
+            }
+        }
+        return uri;
+    }
+
+    private void resetProgress() {
+        mCurrentProcessingStep = 0;
+    }
+
+    private void updateProgress() {
+        if (mCallback != null) {
+            mCallback.onProgress(MAX_PROCESSING_STEPS, ++mCurrentProcessingStep);
+        }
+    }
+
+    public Uri processAndSaveImage(ImagePreset preset, boolean doAuxBackup, int quality) {
+
+        Uri uri = resetToOriginalImageIfNeeded(preset, doAuxBackup);
+        if (uri != null) {
+            return null;
+        }
+
+        resetProgress();
+
+        boolean noBitmap = true;
+        int num_tries = 0;
+        int sampleSize = 1;
+
+        // If necessary, move the source file into the auxiliary directory,
+        // newSourceUri is then pointing to the new location.
+        // If no file is moved, newSourceUri will be the same as mSourceUri.
+        Uri newSourceUri = mSourceUri;
+        if (doAuxBackup) {
+            newSourceUri = moveSrcToAuxIfNeeded(mSourceUri, mDestinationFile);
+        }
+
+        // Stopgap fix for low-memory devices.
+        while (noBitmap) {
+            try {
+                updateProgress();
+                // Try to do bitmap operations, downsample if low-memory
+                Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri,
+                        sampleSize);
+                if (bitmap == null) {
+                    return null;
+                }
+                updateProgress();
+                CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(),
+                        "Saving");
+
+                bitmap = pipeline.renderFinalImage(bitmap, preset);
+                updateProgress();
+
+                Object xmp = getPanoramaXMPData(newSourceUri, preset);
+                ExifInterface exif = getExifData(newSourceUri);
+
+                updateProgress();
+                // Set tags
+                long time = System.currentTimeMillis();
+                exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time,
+                        TimeZone.getDefault());
+                exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
+                        ExifInterface.Orientation.TOP_LEFT));
+                // Remove old thumbnail
+                exif.removeCompressedThumbnail();
+
+                updateProgress();
+
+                // If we succeed in writing the bitmap as a jpeg, return a uri.
+                if (putExifData(mDestinationFile, exif, bitmap, quality)) {
+                    putPanoramaXMPData(mDestinationFile, xmp);
+                    // mDestinationFile will save the newSourceUri info in the XMP.
+                    XmpPresets.writeFilterXMP(mContext, newSourceUri,
+                            mDestinationFile, preset);
+
+                    // After this call, mSelectedImageUri will be actually
+                    // pointing at the new file mDestinationFile.
+                    uri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri,
+                            mDestinationFile, time, doAuxBackup);
+                }
+                updateProgress();
+
+                noBitmap = false;
+                UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+                        "SaveComplete", null);
+            } catch (OutOfMemoryError e) {
+                // Try 5 times before failing for good.
+                if (++num_tries >= 5) {
+                    throw e;
+                }
+                System.gc();
+                sampleSize *= 2;
+                resetProgress();
+            }
+        }
+        return uri;
+    }
+
+    /**
+     *  Move the source file to auxiliary directory if needed and return the Uri
+     *  pointing to this new source file.
+     * @param srcUri Uri to the source image.
+     * @param dstFile Providing the destination file info to help to build the
+     *  auxiliary directory and new source file's name.
+     * @return the newSourceUri pointing to the new source image.
+     */
+    private Uri moveSrcToAuxIfNeeded(Uri srcUri, File dstFile) {
+        File srcFile = getLocalFileFromUri(mContext, srcUri);
+        if (srcFile == null) {
+            Log.d(LOGTAG, "Source file is not a local file, no update.");
+            return srcUri;
+        }
+
+        // Get the destination directory and create the auxilliary directory
+        // if necessary.
+        File auxDiretory = getLocalAuxDirectory(dstFile);
+        if (!auxDiretory.exists()) {
+            auxDiretory.mkdirs();
+        }
+
+        // Make sure there is a .nomedia file in the auxiliary directory, such
+        // that MediaScanner will not report those files under this directory.
+        File noMedia = new File(auxDiretory, ".nomedia");
+        if (!noMedia.exists()) {
+            try {
+                noMedia.createNewFile();
+            } catch (IOException e) {
+                Log.e(LOGTAG, "Can't create the nomedia");
+                return srcUri;
+            }
+        }
+        // We are using the destination file name such that photos sitting in
+        // the auxiliary directory are matching the parent directory.
+        File newSrcFile = new File(auxDiretory, dstFile.getName());
+
+        if (!newSrcFile.exists()) {
+            srcFile.renameTo(newSrcFile);
+        }
+
+        return Uri.fromFile(newSrcFile);
+
+    }
+
+    private static File getLocalAuxDirectory(File dstFile) {
+        File dstDirectory = dstFile.getParentFile();
+        File auxDiretory = new File(dstDirectory + "/" + AUX_DIR_NAME);
+        return auxDiretory;
+    }
+
+    public static Uri makeAndInsertUri(Context context, Uri sourceUri) {
+        long time = System.currentTimeMillis();
+        String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time));
+        File saveDirectory = getFinalSaveDirectory(context, sourceUri);
+        File file = new File(saveDirectory, filename  + ".JPG");
+        return linkNewFileToUri(context, sourceUri, file, time, false);
+    }
+
+    public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
+            File destination) {
+        Uri selectedImageUri = filterShowActivity.getSelectedImageUri();
+        Uri sourceImageUri = MasterImage.getImage().getUri();
+
+        Intent processIntent = ProcessingService.getSaveIntent(filterShowActivity, preset,
+                destination, selectedImageUri, sourceImageUri, false, 90);
+
+        filterShowActivity.startService(processIntent);
+
+        if (!filterShowActivity.isSimpleEditAction()) {
+            // terminate for now
+            filterShowActivity.completeSaveImage(selectedImageUri);
+        }
+    }
+
+    public static void querySource(Context context, Uri sourceUri, String[] projection,
+            ContentResolverQueryCallback callback) {
+        ContentResolver contentResolver = context.getContentResolver();
+        querySourceFromContentResolver(contentResolver, sourceUri, projection, callback);
+    }
+
+    private static void querySourceFromContentResolver(
+            ContentResolver contentResolver, Uri sourceUri, String[] projection,
+            ContentResolverQueryCallback callback) {
+        Cursor cursor = null;
+        try {
+            cursor = contentResolver.query(sourceUri, projection, null, null,
+                    null);
+            if ((cursor != null) && cursor.moveToNext()) {
+                callback.onCursorResult(cursor);
+            }
+        } catch (Exception e) {
+            // Ignore error for lacking the data column from the source.
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    private static File getSaveDirectory(Context context, Uri sourceUri) {
+        File file = getLocalFileFromUri(context, sourceUri);
+        if (file != null) {
+            return file.getParentFile();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Construct a File object based on the srcUri.
+     * @return The file object. Return null if srcUri is invalid or not a local
+     * file.
+     */
+    private static File getLocalFileFromUri(Context context, Uri srcUri) {
+        if (srcUri == null) {
+            Log.e(LOGTAG, "srcUri is null.");
+            return null;
+        }
+
+        String scheme = srcUri.getScheme();
+        if (scheme == null) {
+            Log.e(LOGTAG, "scheme is null.");
+            return null;
+        }
+
+        final File[] file = new File[1];
+        // sourceUri can be a file path or a content Uri, it need to be handled
+        // differently.
+        if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
+            if (srcUri.getAuthority().equals(MediaStore.AUTHORITY)) {
+                querySource(context, srcUri, new String[] {
+                        ImageColumns.DATA
+                },
+                        new ContentResolverQueryCallback() {
+
+                            @Override
+                            public void onCursorResult(Cursor cursor) {
+                                file[0] = new File(cursor.getString(0));
+                            }
+                        });
+            }
+        } else if (scheme.equals(ContentResolver.SCHEME_FILE)) {
+            file[0] = new File(srcUri.getPath());
+        }
+        return file[0];
+    }
+
+    /**
+     * Gets the actual filename for a Uri from Gallery's ContentProvider.
+     */
+    private static String getTrueFilename(Context context, Uri src) {
+        if (context == null || src == null) {
+            return null;
+        }
+        final String[] trueName = new String[1];
+        querySource(context, src, new String[] {
+                ImageColumns.DATA
+        }, new ContentResolverQueryCallback() {
+            @Override
+            public void onCursorResult(Cursor cursor) {
+                trueName[0] = new File(cursor.getString(0)).getName();
+            }
+        });
+        return trueName[0];
+    }
+
+    /**
+     * Checks whether the true filename has the panorama image prefix.
+     */
+    private static boolean hasPanoPrefix(Context context, Uri src) {
+        String name = getTrueFilename(context, src);
+        return name != null && name.startsWith(PREFIX_PANO);
+    }
+
+    /**
+     * If the <code>sourceUri</code> is a local content Uri, update the
+     * <code>sourceUri</code> to point to the <code>file</code>.
+     * At the same time, the old file <code>sourceUri</code> used to point to
+     * will be removed if it is local.
+     * If the <code>sourceUri</code> is not a local content Uri, then the
+     * <code>file</code> will be inserted as a new content Uri.
+     * @return the final Uri referring to the <code>file</code>.
+     */
+    public static Uri linkNewFileToUri(Context context, Uri sourceUri,
+            File file, long time, boolean deleteOriginal) {
+        File oldSelectedFile = getLocalFileFromUri(context, sourceUri);
+        final ContentValues values = new ContentValues();
+
+        time /= 1000;
+        values.put(Images.Media.TITLE, file.getName());
+        values.put(Images.Media.DISPLAY_NAME, file.getName());
+        values.put(Images.Media.MIME_TYPE, "image/jpeg");
+        values.put(Images.Media.DATE_TAKEN, time);
+        values.put(Images.Media.DATE_MODIFIED, time);
+        values.put(Images.Media.DATE_ADDED, time);
+        values.put(Images.Media.ORIENTATION, 0);
+        values.put(Images.Media.DATA, file.getAbsolutePath());
+        values.put(Images.Media.SIZE, file.length());
+
+        final String[] projection = new String[] {
+                ImageColumns.DATE_TAKEN,
+                ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
+        };
+        SaveImage.querySource(context, sourceUri, projection,
+                new SaveImage.ContentResolverQueryCallback() {
+
+                    @Override
+                    public void onCursorResult(Cursor cursor) {
+                        values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
+
+                        double latitude = cursor.getDouble(1);
+                        double longitude = cursor.getDouble(2);
+                        // TODO: Change || to && after the default location
+                        // issue is fixed.
+                        if ((latitude != 0f) || (longitude != 0f)) {
+                            values.put(Images.Media.LATITUDE, latitude);
+                            values.put(Images.Media.LONGITUDE, longitude);
+                        }
+                    }
+                });
+
+        Uri result = sourceUri;
+        if (oldSelectedFile == null || !deleteOriginal) {
+            result = context.getContentResolver().insert(
+                    Images.Media.EXTERNAL_CONTENT_URI, values);
+        } else {
+            context.getContentResolver().update(sourceUri, values, null, null);
+            if (oldSelectedFile.exists()) {
+                oldSelectedFile.delete();
+            }
+        }
+
+        return result;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/XmpPresets.java b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java
new file mode 100644
index 0000000..3995eeb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.tools;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.XMPMetaFactory;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+public class XmpPresets {
+    public static final String
+            XMP_GOOGLE_FILTER_NAMESPACE = "http://ns.google.com/photos/1.0/filter/";
+    public static final String XMP_GOOGLE_FILTER_PREFIX = "AFltr";
+    public static final String XMP_SRC_FILE_URI = "SourceFileUri";
+    public static final String XMP_FILTERSTACK = "filterstack";
+    private static final String LOGTAG = "XmpPresets";
+
+    public static class XMresults {
+        public String presetString;
+        public ImagePreset preset;
+        public Uri originalimage;
+    }
+
+    static {
+        try {
+            XMPMetaFactory.getSchemaRegistry().registerNamespace(
+                    XMP_GOOGLE_FILTER_NAMESPACE, XMP_GOOGLE_FILTER_PREFIX);
+        } catch (XMPException e) {
+            Log.e(LOGTAG, "Register XMP name space failed", e);
+        }
+    }
+
+    public static void writeFilterXMP(
+            Context context, Uri srcUri, File dstFile, ImagePreset preset) {
+        InputStream is = null;
+        XMPMeta xmpMeta = null;
+        try {
+            is = context.getContentResolver().openInputStream(srcUri);
+            xmpMeta = XmpUtilHelper.extractXMPMeta(is);
+        } catch (FileNotFoundException e) {
+
+        } finally {
+            Utils.closeSilently(is);
+        }
+
+        if (xmpMeta == null) {
+            xmpMeta = XMPMetaFactory.create();
+        }
+        try {
+            xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE,
+                    XMP_SRC_FILE_URI, srcUri.toString());
+            xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE,
+                    XMP_FILTERSTACK, preset.getJsonString(context.getString(R.string.saved)));
+        } catch (XMPException e) {
+            Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath());
+            return;
+        }
+
+        if (!XmpUtilHelper.writeXMPMeta(dstFile.getAbsolutePath(), xmpMeta)) {
+            Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath());
+        }
+    }
+
+    public static XMresults extractXMPData(
+            Context context, MasterImage mMasterImage, Uri uriToEdit) {
+        XMresults ret = new XMresults();
+
+        InputStream is = null;
+        XMPMeta xmpMeta = null;
+        try {
+            is = context.getContentResolver().openInputStream(uriToEdit);
+            xmpMeta = XmpUtilHelper.extractXMPMeta(is);
+        } catch (FileNotFoundException e) {
+        } finally {
+            Utils.closeSilently(is);
+        }
+
+        if (xmpMeta == null) {
+            return null;
+        }
+
+        try {
+            String strSrcUri = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE,
+                    XMP_SRC_FILE_URI);
+
+            if (strSrcUri != null) {
+                String filterString = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE,
+                        XMP_FILTERSTACK);
+
+                Uri srcUri = Uri.parse(strSrcUri);
+                ret.originalimage = srcUri;
+
+                ret.preset = new ImagePreset(mMasterImage.getPreset());
+                ret.presetString = filterString;
+                boolean ok = ret.preset.readJsonFromString(filterString);
+                if (!ok) {
+                    return null;
+                }
+                return ret;
+            }
+        } catch (XMPException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/ui/ExportDialog.java b/src/com/android/gallery3d/filtershow/ui/ExportDialog.java
new file mode 100644
index 0000000..4b30e7b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/ui/ExportDialog.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.ui;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ExportDialog extends DialogFragment implements View.OnClickListener, SeekBar.OnSeekBarChangeListener{
+    SeekBar mSeekBar;
+    TextView mSeekVal;
+    String mSliderLabel;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.filtershow_export_dialog, container);
+        mSeekBar = (SeekBar) view.findViewById(R.id.qualitySeekBar);
+        mSeekVal = (TextView) view.findViewById(R.id.qualityTextView);
+        mSliderLabel = getString(R.string.quality) + ": ";
+        mSeekVal.setText(mSliderLabel + mSeekBar.getProgress());
+        mSeekBar.setOnSeekBarChangeListener(this);
+        view.findViewById(R.id.cancel).setOnClickListener(this);
+        view.findViewById(R.id.done).setOnClickListener(this);
+        getDialog().setTitle(R.string.export_flattened);
+        return view;
+    }
+
+    @Override
+    public void onStopTrackingTouch(SeekBar arg0) {
+        // Do nothing
+    }
+
+    @Override
+    public void onStartTrackingTouch(SeekBar arg0) {
+     // Do nothing
+    }
+
+    @Override
+    public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) {
+        mSeekVal.setText(mSliderLabel + arg1);
+    }
+
+    @Override
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.cancel:
+                dismiss();
+                break;
+            case R.id.done:
+                FilterShowActivity activity = (FilterShowActivity) getActivity();
+                Uri sourceUri = MasterImage.getImage().getUri();
+                File dest = SaveImage.getNewFile(activity, sourceUri);
+                Intent processIntent = ProcessingService.getSaveIntent(activity, MasterImage
+                        .getImage().getPreset(), dest, activity.getSelectedImageUri(), sourceUri,
+                        true, mSeekBar.getProgress());
+                activity.startService(processIntent);
+                dismiss();
+                break;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java b/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java
new file mode 100644
index 0000000..c1e4109
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.ui;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.ImageButton;
+
+import com.android.gallery3d.R;
+
+public class FramedTextButton extends ImageButton {
+    private static final String LOGTAG = "FramedTextButton";
+    private String mText = null;
+    private static int mTextSize = 24;
+    private static int mTextPadding = 20;
+    private static Paint gPaint = new Paint();
+    private static Path gPath = new Path();
+    private static int mTrianglePadding = 2;
+    private static int mTriangleSize = 30;
+
+    public static void setTextSize(int value) {
+        mTextSize = value;
+    }
+
+    public static void setTextPadding(int value) {
+        mTextPadding = value;
+    }
+
+    public static void setTrianglePadding(int value) {
+        mTrianglePadding = value;
+    }
+
+    public static void setTriangleSize(int value) {
+        mTriangleSize = value;
+    }
+
+    public void setText(String text) {
+        mText = text;
+        invalidate();
+    }
+
+    public void setTextFrom(int itemId) {
+        switch (itemId) {
+            case R.id.curve_menu_rgb: {
+                setText(getContext().getString(R.string.curves_channel_rgb));
+                break;
+            }
+            case R.id.curve_menu_red: {
+                setText(getContext().getString(R.string.curves_channel_red));
+                break;
+            }
+            case R.id.curve_menu_green: {
+                setText(getContext().getString(R.string.curves_channel_green));
+                break;
+            }
+            case R.id.curve_menu_blue: {
+                setText(getContext().getString(R.string.curves_channel_blue));
+                break;
+            }
+        }
+        invalidate();
+    }
+
+    public FramedTextButton(Context context) {
+        this(context, null);
+    }
+
+    public FramedTextButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        if (attrs == null) {
+            return;
+        }
+        TypedArray a = getContext().obtainStyledAttributes(
+                attrs, R.styleable.ImageButtonTitle);
+
+        mText = a.getString(R.styleable.ImageButtonTitle_android_text);
+    }
+
+    public String getText(){
+        return mText;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        gPaint.setARGB(96, 255, 255, 255);
+        gPaint.setStrokeWidth(2);
+        gPaint.setStyle(Paint.Style.STROKE);
+        int w = getWidth();
+        int h = getHeight();
+        canvas.drawRect(mTextPadding, mTextPadding, w - mTextPadding,
+                h - mTextPadding, gPaint);
+        gPath.reset();
+        gPath.moveTo(w - mTextPadding - mTrianglePadding - mTriangleSize,
+                     h - mTextPadding - mTrianglePadding);
+        gPath.lineTo(w - mTextPadding - mTrianglePadding,
+                     h - mTextPadding - mTrianglePadding - mTriangleSize);
+        gPath.lineTo(w - mTextPadding - mTrianglePadding,
+                     h - mTextPadding - mTrianglePadding);
+        gPath.close();
+        gPaint.setARGB(128, 255, 255, 255);
+        gPaint.setStrokeWidth(1);
+        gPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+        canvas.drawPath(gPath, gPaint);
+        if (mText != null) {
+            gPaint.reset();
+            gPaint.setARGB(255, 255, 255, 255);
+            gPaint.setTextSize(mTextSize);
+            float textWidth = gPaint.measureText(mText);
+            Rect bounds = new Rect();
+            gPaint.getTextBounds(mText, 0, mText.length(), bounds);
+            int x = (int) ((w - textWidth) / 2);
+            int y = (h + bounds.height()) / 2;
+
+            canvas.drawText(mText, x, y, gPaint);
+        }
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java
new file mode 100644
index 0000000..ef40c5e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.ui;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+public class SelectionRenderer {
+
+    public static void drawSelection(Canvas canvas, int left, int top, int right, int bottom,
+            int stroke, Paint paint) {
+        canvas.drawRect(left, top, right, top + stroke, paint);
+        canvas.drawRect(left, bottom - stroke, right, bottom, paint);
+        canvas.drawRect(left, top, left + stroke, bottom, paint);
+        canvas.drawRect(right - stroke, top, right, bottom, paint);
+    }
+
+    public static void drawSelection(Canvas canvas, int left, int top, int right, int bottom,
+            int stroke, Paint selectPaint, int border, Paint borderPaint) {
+        canvas.drawRect(left, top, right, top + stroke, selectPaint);
+        canvas.drawRect(left, bottom - stroke, right, bottom, selectPaint);
+        canvas.drawRect(left, top, left + stroke, bottom, selectPaint);
+        canvas.drawRect(right - stroke, top, right, bottom, selectPaint);
+        canvas.drawRect(left + stroke, top + stroke, right - stroke,
+                top + stroke + border, borderPaint);
+        canvas.drawRect(left + stroke, bottom - stroke - border, right - stroke,
+                bottom - stroke, borderPaint);
+        canvas.drawRect(left + stroke, top + stroke, left + stroke + border,
+                bottom - stroke, borderPaint);
+        canvas.drawRect(right - stroke - border, top + stroke, right - stroke,
+                bottom - stroke, borderPaint);
+    }
+
+}
diff --git a/src/com/android/gallery3d/gadget/LocalPhotoSource.java b/src/com/android/gallery3d/gadget/LocalPhotoSource.java
new file mode 100644
index 0000000..4e94e8d
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/LocalPhotoSource.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.gadget;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.provider.MediaStore.Images.Media;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Random;
+
+public class LocalPhotoSource implements WidgetSource {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "LocalPhotoSource";
+
+    private static final int MAX_PHOTO_COUNT = 128;
+
+    /* Static fields used to query for the correct set of images */
+    private static final Uri CONTENT_URI = Media.EXTERNAL_CONTENT_URI;
+    private static final String DATE_TAKEN = Media.DATE_TAKEN;
+    private static final String[] PROJECTION = {Media._ID};
+    private static final String[] COUNT_PROJECTION = {"count(*)"};
+    /* We don't want to include the download directory */
+    private static final String SELECTION =
+            String.format("%s != %s", Media.BUCKET_ID, getDownloadBucketId());
+    private static final String ORDER = String.format("%s DESC", DATE_TAKEN);
+
+    private Context mContext;
+    private ArrayList<Long> mPhotos = new ArrayList<Long>();
+    private ContentListener mContentListener;
+    private ContentObserver mContentObserver;
+    private boolean mContentDirty = true;
+    private DataManager mDataManager;
+    private static final Path LOCAL_IMAGE_ROOT = Path.fromString("/local/image/item");
+
+    public LocalPhotoSource(Context context) {
+        mContext = context;
+        mDataManager = ((GalleryApp) context.getApplicationContext()).getDataManager();
+        mContentObserver = new ContentObserver(new Handler()) {
+            @Override
+            public void onChange(boolean selfChange) {
+                mContentDirty = true;
+                if (mContentListener != null) mContentListener.onContentDirty();
+            }
+        };
+        mContext.getContentResolver()
+                .registerContentObserver(CONTENT_URI, true, mContentObserver);
+    }
+
+    @Override
+    public void close() {
+        mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+    }
+
+    @Override
+    public Uri getContentUri(int index) {
+        if (index < mPhotos.size()) {
+            return CONTENT_URI.buildUpon()
+                    .appendPath(String.valueOf(mPhotos.get(index)))
+                    .build();
+        }
+        return null;
+    }
+
+    @Override
+    public Bitmap getImage(int index) {
+        if (index >= mPhotos.size()) return null;
+        long id = mPhotos.get(index);
+        MediaItem image = (MediaItem)
+                mDataManager.getMediaObject(LOCAL_IMAGE_ROOT.getChild(id));
+        if (image == null) return null;
+
+        return WidgetUtils.createWidgetBitmap(image);
+    }
+
+    private int[] getExponentialIndice(int total, int count) {
+        Random random = new Random();
+        if (count > total) count = total;
+        HashSet<Integer> selected = new HashSet<Integer>(count);
+        while (selected.size() < count) {
+            int row = (int)(-Math.log(random.nextDouble()) * total / 2);
+            if (row < total) selected.add(row);
+        }
+        int values[] = new int[count];
+        int index = 0;
+        for (int value : selected) {
+            values[index++] = value;
+        }
+        return values;
+    }
+
+    private int getPhotoCount(ContentResolver resolver) {
+        Cursor cursor = resolver.query(
+                CONTENT_URI, COUNT_PROJECTION, SELECTION, null, null);
+        if (cursor == null) return 0;
+        try {
+            Utils.assertTrue(cursor.moveToNext());
+            return cursor.getInt(0);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private boolean isContentSound(int totalCount) {
+        if (mPhotos.size() < Math.min(totalCount, MAX_PHOTO_COUNT)) return false;
+        if (mPhotos.size() == 0) return true; // totalCount is also 0
+
+        StringBuilder builder = new StringBuilder();
+        for (Long imageId : mPhotos) {
+            if (builder.length() > 0) builder.append(",");
+            builder.append(imageId);
+        }
+        Cursor cursor = mContext.getContentResolver().query(
+                CONTENT_URI, COUNT_PROJECTION,
+                String.format("%s in (%s)", Media._ID, builder.toString()),
+                null, null);
+        if (cursor == null) return false;
+        try {
+            Utils.assertTrue(cursor.moveToNext());
+            return cursor.getInt(0) == mPhotos.size();
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Override
+    public void reload() {
+        if (!mContentDirty) return;
+        mContentDirty = false;
+
+        ContentResolver resolver = mContext.getContentResolver();
+        int photoCount = getPhotoCount(resolver);
+        if (isContentSound(photoCount)) return;
+
+        int choosedIds[] = getExponentialIndice(photoCount, MAX_PHOTO_COUNT);
+        Arrays.sort(choosedIds);
+
+        mPhotos.clear();
+        Cursor cursor = mContext.getContentResolver().query(
+                CONTENT_URI, PROJECTION, SELECTION, null, ORDER);
+        if (cursor == null) return;
+        try {
+            for (int index : choosedIds) {
+                if (cursor.moveToPosition(index)) {
+                    mPhotos.add(cursor.getLong(0));
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Override
+    public int size() {
+        reload();
+        return mPhotos.size();
+    }
+
+    /**
+     * Builds the bucket ID for the public external storage Downloads directory
+     * @return the bucket ID
+     */
+    private static int getDownloadBucketId() {
+        String downloadsPath = Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+                .getAbsolutePath();
+        return GalleryUtils.getBucketId(downloadsPath);
+    }
+
+    @Override
+    public void setContentListener(ContentListener listener) {
+        mContentListener = listener;
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/MediaSetSource.java b/src/com/android/gallery3d/gadget/MediaSetSource.java
new file mode 100644
index 0000000..458651c
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/MediaSetSource.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.gadget;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class MediaSetSource implements WidgetSource, ContentListener {
+    private static final String TAG = "MediaSetSource";
+
+    private DataManager mDataManager;
+    private Path mAlbumPath;
+
+    private WidgetSource mSource;
+
+    private MediaSet mRootSet;
+    private ContentListener mListener;
+
+    public MediaSetSource(DataManager manager, String albumPath) {
+        MediaSet mediaSet = (MediaSet) manager.getMediaObject(albumPath);
+        if (mediaSet != null) {
+            mSource = new CheckedMediaSetSource(mediaSet);
+            return;
+        }
+
+        // Initialize source to an empty source until the album path can be resolved
+        mDataManager = Utils.checkNotNull(manager);
+        mAlbumPath = Path.fromString(albumPath);
+        mSource = new EmptySource();
+        monitorRootPath();
+    }
+
+    @Override
+    public int size() {
+        return mSource.size();
+    }
+
+    @Override
+    public Bitmap getImage(int index) {
+        return mSource.getImage(index);
+    }
+
+    @Override
+    public Uri getContentUri(int index) {
+        return mSource.getContentUri(index);
+    }
+
+    @Override
+    public synchronized void setContentListener(ContentListener listener) {
+        if (mRootSet != null) {
+            mListener = listener;
+        } else {
+            mSource.setContentListener(listener);
+        }
+    }
+
+    @Override
+    public void reload() {
+        mSource.reload();
+    }
+
+    @Override
+    public void close() {
+        mSource.close();
+    }
+
+    @Override
+    public void onContentDirty() {
+        resolveAlbumPath();
+    }
+
+    private void monitorRootPath() {
+        String rootPath = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL);
+        mRootSet = (MediaSet) mDataManager.getMediaObject(rootPath);
+        mRootSet.addContentListener(this);
+    }
+
+    private synchronized void resolveAlbumPath() {
+        if (mDataManager == null) return;
+        MediaSet mediaSet = (MediaSet) mDataManager.getMediaObject(mAlbumPath);
+        if (mediaSet != null) {
+            // Clear the reference instead of removing the listener
+            // to get around a concurrent modification exception.
+            mRootSet = null;
+
+            mSource = new CheckedMediaSetSource(mediaSet);
+            if (mListener != null) {
+                mListener.onContentDirty();
+                mSource.setContentListener(mListener);
+                mListener = null;
+            }
+            mDataManager = null;
+            mAlbumPath = null;
+        }
+    }
+
+    private static class CheckedMediaSetSource implements WidgetSource, ContentListener {
+        private static final int CACHE_SIZE = 32;
+
+        @SuppressWarnings("unused")
+        private static final String TAG = "CheckedMediaSetSource";
+
+        private MediaSet mSource;
+        private MediaItem mCache[] = new MediaItem[CACHE_SIZE];
+        private int mCacheStart;
+        private int mCacheEnd;
+        private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+
+        private ContentListener mContentListener;
+
+        public CheckedMediaSetSource(MediaSet source) {
+            mSource = Utils.checkNotNull(source);
+            mSource.addContentListener(this);
+        }
+
+        @Override
+        public void close() {
+            mSource.removeContentListener(this);
+        }
+
+        private void ensureCacheRange(int index) {
+            if (index >= mCacheStart && index < mCacheEnd) return;
+
+            long token = Binder.clearCallingIdentity();
+            try {
+                mCacheStart = index;
+                ArrayList<MediaItem> items = mSource.getMediaItem(mCacheStart, CACHE_SIZE);
+                mCacheEnd = mCacheStart + items.size();
+                items.toArray(mCache);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public synchronized Uri getContentUri(int index) {
+            ensureCacheRange(index);
+            if (index < mCacheStart || index >= mCacheEnd) return null;
+            return mCache[index - mCacheStart].getContentUri();
+        }
+
+        @Override
+        public synchronized Bitmap getImage(int index) {
+            ensureCacheRange(index);
+            if (index < mCacheStart || index >= mCacheEnd) return null;
+            return WidgetUtils.createWidgetBitmap(mCache[index - mCacheStart]);
+        }
+
+        @Override
+        public void reload() {
+            long version = mSource.reload();
+            if (mSourceVersion != version) {
+                mSourceVersion = version;
+                mCacheStart = 0;
+                mCacheEnd = 0;
+                Arrays.fill(mCache, null);
+            }
+        }
+
+        @Override
+        public void setContentListener(ContentListener listener) {
+            mContentListener = listener;
+        }
+
+        @Override
+        public int size() {
+            long token = Binder.clearCallingIdentity();
+            try {
+                return mSource.getMediaItemCount();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void onContentDirty() {
+            if (mContentListener != null) mContentListener.onContentDirty();
+        }
+    }
+
+    private static class EmptySource implements WidgetSource {
+
+        @Override
+        public int size() {
+            return 0;
+        }
+
+        @Override
+        public Bitmap getImage(int index) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Uri getContentUri(int index) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void setContentListener(ContentListener listener) {}
+
+        @Override
+        public void reload() {}
+
+        @Override
+        public void close() {}
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
new file mode 100644
index 0000000..58466bf
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.gadget;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry;
+import com.android.gallery3d.onetimeinitializer.GalleryWidgetMigrator;
+
+public class PhotoAppWidgetProvider extends AppWidgetProvider {
+
+    private static final String TAG = "WidgetProvider";
+
+    static RemoteViews buildWidget(Context context, int id, Entry entry) {
+
+        switch (entry.type) {
+            case WidgetDatabaseHelper.TYPE_ALBUM:
+            case WidgetDatabaseHelper.TYPE_SHUFFLE:
+                return buildStackWidget(context, id, entry);
+            case WidgetDatabaseHelper.TYPE_SINGLE_PHOTO:
+                return buildFrameWidget(context, id, entry);
+        }
+        throw new RuntimeException("invalid type - " + entry.type);
+    }
+
+    @Override
+    public void onUpdate(Context context,
+            AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+
+        if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) {
+            // migrate gallery widgets from pre-JB releases to JB due to bucket ID change
+            GalleryWidgetMigrator.migrateGalleryWidgets(context);
+        }
+
+        WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context);
+        try {
+            for (int id : appWidgetIds) {
+                Entry entry = helper.getEntry(id);
+                if (entry != null) {
+                    RemoteViews views = buildWidget(context, id, entry);
+                    appWidgetManager.updateAppWidget(id, views);
+                } else {
+                    Log.e(TAG, "cannot load widget: " + id);
+                }
+            }
+        } finally {
+            helper.close();
+        }
+        super.onUpdate(context, appWidgetManager, appWidgetIds);
+    }
+
+    @SuppressWarnings("deprecation")
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static RemoteViews buildStackWidget(Context context, int widgetId, Entry entry) {
+        RemoteViews views = new RemoteViews(
+                context.getPackageName(), R.layout.appwidget_main);
+
+        Intent intent = new Intent(context, WidgetService.class);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
+        intent.putExtra(WidgetService.EXTRA_WIDGET_TYPE, entry.type);
+        intent.putExtra(WidgetService.EXTRA_ALBUM_PATH, entry.albumPath);
+        intent.setData(Uri.parse("widget://gallery/" + widgetId));
+
+        // We use the deprecated API for backward compatibility
+        // The new API is available in ICE_CREAM_SANDWICH (15)
+        views.setRemoteAdapter(widgetId, R.id.appwidget_stack_view, intent);
+
+        views.setEmptyView(R.id.appwidget_stack_view, R.id.appwidget_empty_view);
+
+        Intent clickIntent = new Intent(context, WidgetClickHandler.class);
+        PendingIntent pendingIntent = PendingIntent.getActivity(
+                context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        views.setPendingIntentTemplate(R.id.appwidget_stack_view, pendingIntent);
+
+        return views;
+    }
+
+    static RemoteViews buildFrameWidget(Context context, int appWidgetId, Entry entry) {
+        RemoteViews views = new RemoteViews(
+                context.getPackageName(), R.layout.photo_frame);
+        try {
+            byte[] data = entry.imageData;
+            Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+            views.setImageViewBitmap(R.id.photo, bitmap);
+        } catch (Throwable t) {
+            Log.w(TAG, "cannot load widget image: " + appWidgetId, t);
+        }
+
+        if (entry.imageUri != null) {
+            try {
+                Uri uri = Uri.parse(entry.imageUri);
+                Intent clickIntent = new Intent(context, WidgetClickHandler.class)
+                        .setData(uri);
+                PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0,
+                        clickIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+                views.setOnClickPendingIntent(R.id.photo, pendingClickIntent);
+            } catch (Throwable t) {
+                Log.w(TAG, "cannot load widget uri: " + appWidgetId, t);
+            }
+        }
+        return views;
+    }
+
+    @Override
+    public void onDeleted(Context context, int[] appWidgetIds) {
+        // Clean deleted photos out of our database
+        WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context);
+        for (int appWidgetId : appWidgetIds) {
+            helper.deleteEntry(appWidgetId);
+        }
+        helper.close();
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetClickHandler.java b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
new file mode 100644
index 0000000..37ee1a6
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.gadget;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.ApiHelper;
+
+public class WidgetClickHandler extends Activity {
+    private static final String TAG = "PhotoAppWidgetClickHandler";
+
+    private boolean isValidDataUri(Uri dataUri) {
+        if (dataUri == null) return false;
+        try {
+            AssetFileDescriptor f = getContentResolver()
+                    .openAssetFileDescriptor(dataUri, "r");
+            f.close();
+            return true;
+        } catch (Throwable e) {
+            Log.w(TAG, "cannot open uri: " + dataUri, e);
+            return false;
+        }
+    }
+
+    @Override
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    protected void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        // The behavior is changed in JB, refer to b/6384492 for more details
+        boolean tediousBack = Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.JELLY_BEAN;
+        Uri uri = getIntent().getData();
+        Intent intent;
+        if (isValidDataUri(uri)) {
+            intent = new Intent(Intent.ACTION_VIEW, uri);
+            if (tediousBack) {
+                intent.putExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+            }
+        } else {
+            Toast.makeText(this,
+                    R.string.no_such_item, Toast.LENGTH_LONG).show();
+            intent = new Intent(this, Gallery.class);
+        }
+        if (tediousBack) {
+            intent.setFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK |
+                    Intent.FLAG_ACTIVITY_CLEAR_TASK |
+                    Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+        }
+        startActivity(intent);
+        finish();
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java
new file mode 100644
index 0000000..2a4c6cf
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.gadget;
+
+import android.app.Activity;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AlbumPicker;
+import com.android.gallery3d.app.DialogPicker;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+
+public class WidgetConfigure extends Activity {
+    @SuppressWarnings("unused")
+    private static final String TAG = "WidgetConfigure";
+
+    public static final String KEY_WIDGET_TYPE = "widget-type";
+    private static final String KEY_PICKED_ITEM = "picked-item";
+
+    private static final int REQUEST_WIDGET_TYPE = 1;
+    private static final int REQUEST_CHOOSE_ALBUM = 2;
+    private static final int REQUEST_CROP_IMAGE = 3;
+    private static final int REQUEST_GET_PHOTO = 4;
+
+    public static final int RESULT_ERROR = RESULT_FIRST_USER;
+
+    // Scale up the widget size since we only specified the minimized
+    // size of the gadget. The real size could be larger.
+    // Note: There is also a limit on the size of data that can be
+    // passed in Binder's transaction.
+    private static float WIDGET_SCALE_FACTOR = 1.5f;
+    private static int MAX_WIDGET_SIDE = 360;
+
+    private int mAppWidgetId = -1;
+    private Uri mPickedItem;
+
+    @Override
+    protected void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        mAppWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+
+        if (mAppWidgetId == -1) {
+            setResult(Activity.RESULT_CANCELED);
+            finish();
+            return;
+        }
+
+        if (savedState == null) {
+            if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) {
+                Intent intent = new Intent(this, WidgetTypeChooser.class);
+                startActivityForResult(intent, REQUEST_WIDGET_TYPE);
+            } else { // Choose the photo type widget
+                setWidgetType(new Intent()
+                        .putExtra(KEY_WIDGET_TYPE, R.id.widget_type_photo));
+            }
+        } else {
+            mPickedItem = savedState.getParcelable(KEY_PICKED_ITEM);
+        }
+    }
+
+    protected void onSaveInstanceStates(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(KEY_PICKED_ITEM, mPickedItem);
+    }
+
+    private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) {
+        AppWidgetManager manager = AppWidgetManager.getInstance(this);
+        RemoteViews views = PhotoAppWidgetProvider.buildWidget(this, mAppWidgetId, entry);
+        manager.updateAppWidget(mAppWidgetId, views);
+        setResult(RESULT_OK, new Intent().putExtra(
+                AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
+        finish();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode != RESULT_OK) {
+            setResult(resultCode, new Intent().putExtra(
+                    AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
+            finish();
+            return;
+        }
+
+        if (requestCode == REQUEST_WIDGET_TYPE) {
+            setWidgetType(data);
+        } else if (requestCode == REQUEST_CHOOSE_ALBUM) {
+            setChoosenAlbum(data);
+        } else if (requestCode == REQUEST_GET_PHOTO) {
+            setChoosenPhoto(data);
+        } else if (requestCode == REQUEST_CROP_IMAGE) {
+            setPhotoWidget(data);
+        } else {
+            throw new AssertionError("unknown request: " + requestCode);
+        }
+    }
+
+    private void setPhotoWidget(Intent data) {
+        // Store the cropped photo in our database
+        Bitmap bitmap = (Bitmap) data.getParcelableExtra("data");
+        WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
+        try {
+            helper.setPhoto(mAppWidgetId, mPickedItem, bitmap);
+            updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
+        } finally {
+            helper.close();
+        }
+    }
+
+    private void setChoosenPhoto(Intent data) {
+        Resources res = getResources();
+
+        float width = res.getDimension(R.dimen.appwidget_width);
+        float height = res.getDimension(R.dimen.appwidget_height);
+
+        // We try to crop a larger image (by scale factor), but there is still
+        // a bound on the binder limit.
+        float scale = Math.min(WIDGET_SCALE_FACTOR,
+                MAX_WIDGET_SIDE / Math.max(width, height));
+
+        int widgetWidth = Math.round(width * scale);
+        int widgetHeight = Math.round(height * scale);
+
+        mPickedItem = data.getData();
+        Intent request = new Intent(CropActivity.CROP_ACTION, mPickedItem)
+                .putExtra(CropExtras.KEY_OUTPUT_X, widgetWidth)
+                .putExtra(CropExtras.KEY_OUTPUT_Y, widgetHeight)
+                .putExtra(CropExtras.KEY_ASPECT_X, widgetWidth)
+                .putExtra(CropExtras.KEY_ASPECT_Y, widgetHeight)
+                .putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true)
+                .putExtra(CropExtras.KEY_SCALE, true)
+                .putExtra(CropExtras.KEY_RETURN_DATA, true);
+        startActivityForResult(request, REQUEST_CROP_IMAGE);
+    }
+
+    private void setChoosenAlbum(Intent data) {
+        String albumPath = data.getStringExtra(AlbumPicker.KEY_ALBUM_PATH);
+        WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
+        try {
+            String relativePath = null;
+            GalleryApp galleryApp = (GalleryApp) getApplicationContext();
+            DataManager manager = galleryApp.getDataManager();
+            Path path = Path.fromString(albumPath);
+            MediaSet mediaSet = (MediaSet) manager.getMediaObject(path);
+            if (mediaSet instanceof LocalAlbum) {
+                int bucketId = Integer.parseInt(path.getSuffix());
+                // If the chosen album is a local album, find relative path
+                // Otherwise, leave the relative path field empty
+                relativePath = LocalAlbum.getRelativePath(bucketId);
+                Log.i(TAG, "Setting widget, album path: " + albumPath
+                        + ", relative path: " + relativePath);
+            }
+            helper.setWidget(mAppWidgetId,
+                    WidgetDatabaseHelper.TYPE_ALBUM, albumPath, relativePath);
+            updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
+        } finally {
+            helper.close();
+        }
+    }
+
+    private void setWidgetType(Intent data) {
+        int widgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle);
+        if (widgetType == R.id.widget_type_album) {
+            Intent intent = new Intent(this, AlbumPicker.class);
+            startActivityForResult(intent, REQUEST_CHOOSE_ALBUM);
+        } else if (widgetType == R.id.widget_type_shuffle) {
+            WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
+            try {
+                helper.setWidget(mAppWidgetId, WidgetDatabaseHelper.TYPE_SHUFFLE, null, null);
+                updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
+            } finally {
+                helper.close();
+            }
+        } else {
+            // Explicitly send the intent to the DialogPhotoPicker
+            Intent request = new Intent(this, DialogPicker.class)
+                    .setAction(Intent.ACTION_GET_CONTENT)
+                    .setType("image/*");
+            startActivityForResult(request, REQUEST_GET_PHOTO);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java
new file mode 100644
index 0000000..c014584
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.gadget;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class WidgetDatabaseHelper extends SQLiteOpenHelper {
+    private static final String TAG = "PhotoDatabaseHelper";
+    private static final String DATABASE_NAME = "launcher.db";
+
+    // Increment the database version to 5. In version 5, we
+    // add a column in widgets table to record relative paths.
+    private static final int DATABASE_VERSION = 5;
+
+    private static final String TABLE_WIDGETS = "widgets";
+
+    private static final String FIELD_APPWIDGET_ID = "appWidgetId";
+    private static final String FIELD_IMAGE_URI = "imageUri";
+    private static final String FIELD_PHOTO_BLOB = "photoBlob";
+    private static final String FIELD_WIDGET_TYPE = "widgetType";
+    private static final String FIELD_ALBUM_PATH = "albumPath";
+    private static final String FIELD_RELATIVE_PATH = "relativePath";
+
+    public static final int TYPE_SINGLE_PHOTO = 0;
+    public static final int TYPE_SHUFFLE = 1;
+    public static final int TYPE_ALBUM = 2;
+
+    private static final String[] PROJECTION = {
+            FIELD_WIDGET_TYPE, FIELD_IMAGE_URI, FIELD_PHOTO_BLOB, FIELD_ALBUM_PATH,
+            FIELD_APPWIDGET_ID, FIELD_RELATIVE_PATH};
+    private static final int INDEX_WIDGET_TYPE = 0;
+    private static final int INDEX_IMAGE_URI = 1;
+    private static final int INDEX_PHOTO_BLOB = 2;
+    private static final int INDEX_ALBUM_PATH = 3;
+    private static final int INDEX_APPWIDGET_ID = 4;
+    private static final int INDEX_RELATIVE_PATH = 5;
+    private static final String WHERE_APPWIDGET_ID = FIELD_APPWIDGET_ID + " = ?";
+    private static final String WHERE_WIDGET_TYPE = FIELD_WIDGET_TYPE + " = ?";
+
+    public static class Entry {
+        public int widgetId;
+        public int type;
+        public String imageUri;
+        public byte imageData[];
+        public String albumPath;
+        public String relativePath;
+
+        private Entry() {}
+
+        private Entry(int id, Cursor cursor) {
+            widgetId = id;
+            type = cursor.getInt(INDEX_WIDGET_TYPE);
+            if (type == TYPE_SINGLE_PHOTO) {
+                imageUri = cursor.getString(INDEX_IMAGE_URI);
+                imageData = cursor.getBlob(INDEX_PHOTO_BLOB);
+            } else if (type == TYPE_ALBUM) {
+                albumPath = cursor.getString(INDEX_ALBUM_PATH);
+                relativePath = cursor.getString(INDEX_RELATIVE_PATH);
+            }
+        }
+
+        private Entry(Cursor cursor) {
+            this(cursor.getInt(INDEX_APPWIDGET_ID), cursor);
+        }
+    }
+
+    public WidgetDatabaseHelper(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + TABLE_WIDGETS + " ("
+                + FIELD_APPWIDGET_ID + " INTEGER PRIMARY KEY, "
+                + FIELD_WIDGET_TYPE + " INTEGER DEFAULT 0, "
+                + FIELD_IMAGE_URI + " TEXT, "
+                + FIELD_ALBUM_PATH + " TEXT, "
+                + FIELD_PHOTO_BLOB + " BLOB, "
+                + FIELD_RELATIVE_PATH + " TEXT)");
+    }
+
+    private void saveData(SQLiteDatabase db, int oldVersion, ArrayList<Entry> data) {
+        if (oldVersion <= 2) {
+            Cursor cursor = db.query("photos",
+                    new String[] {FIELD_APPWIDGET_ID, FIELD_PHOTO_BLOB},
+                    null, null, null, null, null);
+            if (cursor == null) return;
+            try {
+                while (cursor.moveToNext()) {
+                    Entry entry = new Entry();
+                    entry.type = TYPE_SINGLE_PHOTO;
+                    entry.widgetId = cursor.getInt(0);
+                    entry.imageData = cursor.getBlob(1);
+                    data.add(entry);
+                }
+            } finally {
+                cursor.close();
+            }
+        } else if (oldVersion == 3) {
+            Cursor cursor = db.query("photos",
+                    new String[] {FIELD_APPWIDGET_ID, FIELD_PHOTO_BLOB, FIELD_IMAGE_URI},
+                    null, null, null, null, null);
+            if (cursor == null) return;
+            try {
+                while (cursor.moveToNext()) {
+                    Entry entry = new Entry();
+                    entry.type = TYPE_SINGLE_PHOTO;
+                    entry.widgetId = cursor.getInt(0);
+                    entry.imageData = cursor.getBlob(1);
+                    entry.imageUri = cursor.getString(2);
+                    data.add(entry);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    private void restoreData(SQLiteDatabase db, ArrayList<Entry> data) {
+        db.beginTransaction();
+        try {
+            for (Entry entry : data) {
+                ContentValues values = new ContentValues();
+                values.put(FIELD_APPWIDGET_ID, entry.widgetId);
+                values.put(FIELD_WIDGET_TYPE, entry.type);
+                values.put(FIELD_IMAGE_URI, entry.imageUri);
+                values.put(FIELD_PHOTO_BLOB, entry.imageData);
+                values.put(FIELD_ALBUM_PATH, entry.albumPath);
+                db.insert(TABLE_WIDGETS, null, values);
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        if (oldVersion < 4) {
+            // Table "photos" is renamed to "widget" in version 4
+            ArrayList<Entry> data = new ArrayList<Entry>();
+            saveData(db, oldVersion, data);
+
+            Log.w(TAG, "destroying all old data.");
+            db.execSQL("DROP TABLE IF EXISTS photos");
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_WIDGETS);
+            onCreate(db);
+
+            restoreData(db, data);
+        }
+        // Add a column for relative path
+        if (oldVersion < DATABASE_VERSION) {
+            try {
+                db.execSQL("ALTER TABLE widgets ADD COLUMN relativePath TEXT");
+            } catch (Throwable t) {
+                Log.e(TAG, "Failed to add the column for relative path.");
+                return;
+            }
+        }
+    }
+
+    /**
+     * Store the given bitmap in this database for the given appWidgetId.
+     */
+    public boolean setPhoto(int appWidgetId, Uri imageUri, Bitmap bitmap) {
+        try {
+            // Try go guesstimate how much space the icon will take when
+            // serialized to avoid unnecessary allocations/copies during
+            // the write.
+            int size = bitmap.getWidth() * bitmap.getHeight() * 4;
+            ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+            out.close();
+
+            ContentValues values = new ContentValues();
+            values.put(FIELD_APPWIDGET_ID, appWidgetId);
+            values.put(FIELD_WIDGET_TYPE, TYPE_SINGLE_PHOTO);
+            values.put(FIELD_IMAGE_URI, imageUri.toString());
+            values.put(FIELD_PHOTO_BLOB, out.toByteArray());
+
+            SQLiteDatabase db = getWritableDatabase();
+            db.replaceOrThrow(TABLE_WIDGETS, null, values);
+            return true;
+        } catch (Throwable e) {
+            Log.e(TAG, "set widget photo fail", e);
+            return false;
+        }
+    }
+
+    public boolean setWidget(int id, int type, String albumPath, String relativePath) {
+        try {
+            ContentValues values = new ContentValues();
+            values.put(FIELD_APPWIDGET_ID, id);
+            values.put(FIELD_WIDGET_TYPE, type);
+            values.put(FIELD_ALBUM_PATH, Utils.ensureNotNull(albumPath));
+            values.put(FIELD_RELATIVE_PATH, relativePath);
+            getWritableDatabase().replaceOrThrow(TABLE_WIDGETS, null, values);
+            return true;
+        } catch (Throwable e) {
+            Log.e(TAG, "set widget fail", e);
+            return false;
+        }
+    }
+
+    public Entry getEntry(int appWidgetId) {
+        Cursor cursor = null;
+        try {
+            SQLiteDatabase db = getReadableDatabase();
+            cursor = db.query(TABLE_WIDGETS, PROJECTION,
+                    WHERE_APPWIDGET_ID, new String[] {String.valueOf(appWidgetId)},
+                    null, null, null);
+            if (cursor == null || !cursor.moveToNext()) {
+                Log.e(TAG, "query fail: empty cursor: " + cursor + " appWidgetId: "
+                        + appWidgetId);
+                return null;
+            }
+            return new Entry(appWidgetId, cursor);
+        } catch (Throwable e) {
+            Log.e(TAG, "Could not load photo from database", e);
+            return null;
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+    }
+
+    public List<Entry> getEntries(int type) {
+        Cursor cursor = null;
+        try {
+            SQLiteDatabase db = getReadableDatabase();
+            cursor = db.query(TABLE_WIDGETS, PROJECTION,
+                    WHERE_WIDGET_TYPE, new String[] {String.valueOf(type)},
+                    null, null, null);
+            if (cursor == null) {
+                Log.e(TAG, "query fail: null cursor: " + cursor);
+                return null;
+            }
+            ArrayList<Entry> result = new ArrayList<Entry>(cursor.getCount());
+            while (cursor.moveToNext()) {
+                result.add(new Entry(cursor));
+            }
+            return result;
+        } catch (Throwable e) {
+            Log.e(TAG, "Could not load widget from database", e);
+            return null;
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+    }
+
+    /**
+     * Updates the entry in the widget database.
+     */
+    public void updateEntry(Entry entry) {
+        deleteEntry(entry.widgetId);
+        try {
+            ContentValues values = new ContentValues();
+            values.put(FIELD_APPWIDGET_ID, entry.widgetId);
+            values.put(FIELD_WIDGET_TYPE, entry.type);
+            values.put(FIELD_ALBUM_PATH, entry.albumPath);
+            values.put(FIELD_IMAGE_URI, entry.imageUri);
+            values.put(FIELD_PHOTO_BLOB, entry.imageData);
+            values.put(FIELD_RELATIVE_PATH, entry.relativePath);
+            getWritableDatabase().insert(TABLE_WIDGETS, null, values);
+        } catch (Throwable e) {
+            Log.e(TAG, "set widget fail", e);
+        }
+    }
+
+    /**
+     * Remove any bitmap associated with the given appWidgetId.
+     */
+    public void deleteEntry(int appWidgetId) {
+        try {
+            SQLiteDatabase db = getWritableDatabase();
+            db.delete(TABLE_WIDGETS, WHERE_APPWIDGET_ID,
+                    new String[] {String.valueOf(appWidgetId)});
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Could not delete photo from database", e);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetService.java b/src/com/android/gallery3d/gadget/WidgetService.java
new file mode 100644
index 0000000..94dd164
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetService.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.gadget;
+
+import android.annotation.TargetApi;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.ContentListener;
+
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+public class WidgetService extends RemoteViewsService {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "GalleryAppWidgetService";
+
+    public static final String EXTRA_WIDGET_TYPE = "widget-type";
+    public static final String EXTRA_ALBUM_PATH = "album-path";
+
+    @Override
+    public RemoteViewsFactory onGetViewFactory(Intent intent) {
+        int id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+                AppWidgetManager.INVALID_APPWIDGET_ID);
+        int type = intent.getIntExtra(EXTRA_WIDGET_TYPE, 0);
+        String albumPath = intent.getStringExtra(EXTRA_ALBUM_PATH);
+
+        return new PhotoRVFactory((GalleryApp) getApplicationContext(),
+                id, type, albumPath);
+    }
+
+    private static class PhotoRVFactory implements
+            RemoteViewsService.RemoteViewsFactory, ContentListener {
+
+        private final int mAppWidgetId;
+        private final int mType;
+        private final String mAlbumPath;
+        private final GalleryApp mApp;
+
+        private WidgetSource mSource;
+
+        public PhotoRVFactory(GalleryApp app, int id, int type, String albumPath) {
+            mApp = app;
+            mAppWidgetId = id;
+            mType = type;
+            mAlbumPath = albumPath;
+        }
+
+        @Override
+        public void onCreate() {
+            if (mType == WidgetDatabaseHelper.TYPE_ALBUM) {
+                mSource = new MediaSetSource(mApp.getDataManager(), mAlbumPath);
+            } else {
+                mSource = new LocalPhotoSource(mApp.getAndroidContext());
+            }
+            mSource.setContentListener(this);
+            AppWidgetManager.getInstance(mApp.getAndroidContext())
+                    .notifyAppWidgetViewDataChanged(
+                    mAppWidgetId, R.id.appwidget_stack_view);
+        }
+
+        @Override
+        public void onDestroy() {
+            mSource.close();
+            mSource = null;
+        }
+
+        @Override
+        public int getCount() {
+            return mSource.size();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public RemoteViews getLoadingView() {
+            RemoteViews rv = new RemoteViews(
+                    mApp.getAndroidContext().getPackageName(),
+                    R.layout.appwidget_loading_item);
+            rv.setProgressBar(R.id.appwidget_loading_item, 0, 0, true);
+            return rv;
+        }
+
+        @Override
+        public RemoteViews getViewAt(int position) {
+            Bitmap bitmap = mSource.getImage(position);
+            if (bitmap == null) return getLoadingView();
+            RemoteViews views = new RemoteViews(
+                    mApp.getAndroidContext().getPackageName(),
+                    R.layout.appwidget_photo_item);
+            views.setImageViewBitmap(R.id.appwidget_photo_item, bitmap);
+            views.setOnClickFillInIntent(R.id.appwidget_photo_item, new Intent()
+                    .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                    .setData(mSource.getContentUri(position)));
+            return views;
+        }
+
+        @Override
+        public void onDataSetChanged() {
+            mSource.reload();
+        }
+
+        @Override
+        public void onContentDirty() {
+            AppWidgetManager.getInstance(mApp.getAndroidContext())
+                    .notifyAppWidgetViewDataChanged(
+                    mAppWidgetId, R.id.appwidget_stack_view);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetSource.java b/src/com/android/gallery3d/gadget/WidgetSource.java
new file mode 100644
index 0000000..92874c7
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetSource.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.gadget;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import com.android.gallery3d.data.ContentListener;
+
+public interface WidgetSource {
+    public int size();
+    public Bitmap getImage(int index);
+    public Uri getContentUri(int index);
+    public void setContentListener(ContentListener listener);
+    public void reload();
+    public void close();
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetTypeChooser.java b/src/com/android/gallery3d/gadget/WidgetTypeChooser.java
new file mode 100644
index 0000000..1694f1c
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetTypeChooser.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.gadget;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.RadioGroup;
+import android.widget.RadioGroup.OnCheckedChangeListener;
+
+import com.android.gallery3d.R;
+
+public class WidgetTypeChooser extends Activity {
+
+    private OnCheckedChangeListener mListener = new OnCheckedChangeListener() {
+        @Override
+        public void onCheckedChanged(RadioGroup group, int checkedId) {
+            Intent data = new Intent()
+                    .putExtra(WidgetConfigure.KEY_WIDGET_TYPE, checkedId);
+            setResult(RESULT_OK, data);
+            finish();
+        }
+    };
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setTitle(R.string.widget_type);
+        setContentView(R.layout.choose_widget_type);
+        RadioGroup rg = (RadioGroup) findViewById(R.id.widget_type);
+        rg.setOnCheckedChangeListener(mListener);
+
+        Button cancel = (Button) findViewById(R.id.cancel);
+        cancel.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setResult(RESULT_CANCELED);
+                finish();
+            }
+        });
+    }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetUtils.java b/src/com/android/gallery3d/gadget/WidgetUtils.java
new file mode 100644
index 0000000..c20c186
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.gadget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool;
+
+public class WidgetUtils {
+
+    private static final String TAG = "WidgetUtils";
+
+    private static int sStackPhotoWidth = 220;
+    private static int sStackPhotoHeight = 170;
+
+    private WidgetUtils() {
+    }
+
+    public static void initialize(Context context) {
+        Resources r = context.getResources();
+        sStackPhotoWidth = r.getDimensionPixelSize(R.dimen.stack_photo_width);
+        sStackPhotoHeight = r.getDimensionPixelSize(R.dimen.stack_photo_height);
+    }
+
+    public static Bitmap createWidgetBitmap(MediaItem image) {
+        Bitmap bitmap = image.requestImage(MediaItem.TYPE_THUMBNAIL)
+               .run(ThreadPool.JOB_CONTEXT_STUB);
+        if (bitmap == null) {
+            Log.w(TAG, "fail to get image of " + image.toString());
+            return null;
+        }
+        return createWidgetBitmap(bitmap, image.getRotation());
+    }
+
+    public static Bitmap createWidgetBitmap(Bitmap bitmap, int rotation) {
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+
+        float scale;
+        if (((rotation / 90) & 1) == 0) {
+            scale = Math.max((float) sStackPhotoWidth / w,
+                    (float) sStackPhotoHeight / h);
+        } else {
+            scale = Math.max((float) sStackPhotoWidth / h,
+                    (float) sStackPhotoHeight / w);
+        }
+
+        Bitmap target = Bitmap.createBitmap(
+                sStackPhotoWidth, sStackPhotoHeight, Config.ARGB_8888);
+        Canvas canvas = new Canvas(target);
+        canvas.translate(sStackPhotoWidth / 2, sStackPhotoHeight / 2);
+        canvas.rotate(rotation);
+        canvas.scale(scale, scale);
+        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+        canvas.drawBitmap(bitmap, -w / 2, -h / 2, paint);
+        return target;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/BasicTexture.java b/src/com/android/gallery3d/glrenderer/BasicTexture.java
new file mode 100644
index 0000000..2e77b90
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/BasicTexture.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.util.WeakHashMap;
+
+// BasicTexture is a Texture corresponds to a real GL texture.
+// The state of a BasicTexture indicates whether its data is loaded to GL memory.
+// If a BasicTexture is loaded into GL memory, it has a GL texture id.
+public abstract class BasicTexture implements Texture {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "BasicTexture";
+    protected static final int UNSPECIFIED = -1;
+
+    protected static final int STATE_UNLOADED = 0;
+    protected static final int STATE_LOADED = 1;
+    protected static final int STATE_ERROR = -1;
+
+    // Log a warning if a texture is larger along a dimension
+    private static final int MAX_TEXTURE_SIZE = 4096;
+
+    protected int mId = -1;
+    protected int mState;
+
+    protected int mWidth = UNSPECIFIED;
+    protected int mHeight = UNSPECIFIED;
+
+    protected int mTextureWidth;
+    protected int mTextureHeight;
+
+    private boolean mHasBorder;
+
+    protected GLCanvas mCanvasRef = null;
+    private static WeakHashMap<BasicTexture, Object> sAllTextures
+            = new WeakHashMap<BasicTexture, Object>();
+    private static ThreadLocal sInFinalizer = new ThreadLocal();
+
+    protected BasicTexture(GLCanvas canvas, int id, int state) {
+        setAssociatedCanvas(canvas);
+        mId = id;
+        mState = state;
+        synchronized (sAllTextures) {
+            sAllTextures.put(this, null);
+        }
+    }
+
+    protected BasicTexture() {
+        this(null, 0, STATE_UNLOADED);
+    }
+
+    protected void setAssociatedCanvas(GLCanvas canvas) {
+        mCanvasRef = canvas;
+    }
+
+    /**
+     * Sets the content size of this texture. In OpenGL, the actual texture
+     * size must be of power of 2, the size of the content may be smaller.
+     */
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+        mTextureWidth = width > 0 ? Utils.nextPowerOf2(width) : 0;
+        mTextureHeight = height > 0 ? Utils.nextPowerOf2(height) : 0;
+        if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {
+            Log.w(TAG, String.format("texture is too large: %d x %d",
+                    mTextureWidth, mTextureHeight), new Exception());
+        }
+    }
+
+    public boolean isFlippedVertically() {
+      return false;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    // Returns the width rounded to the next power of 2.
+    public int getTextureWidth() {
+        return mTextureWidth;
+    }
+
+    // Returns the height rounded to the next power of 2.
+    public int getTextureHeight() {
+        return mTextureHeight;
+    }
+
+    // Returns true if the texture has one pixel transparent border around the
+    // actual content. This is used to avoid jigged edges.
+    //
+    // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap
+    // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially
+    // covered by the texture will use the color of the edge texel. If we add
+    // the transparent border, the color of the edge texel will be mixed with
+    // appropriate amount of transparent.
+    //
+    // Currently our background is black, so we can draw the thumbnails without
+    // enabling blending.
+    public boolean hasBorder() {
+        return mHasBorder;
+    }
+
+    protected void setBorder(boolean hasBorder) {
+        mHasBorder = hasBorder;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y) {
+        canvas.drawTexture(this, x, y, getWidth(), getHeight());
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+        canvas.drawTexture(this, x, y, w, h);
+    }
+
+    // onBind is called before GLCanvas binds this texture.
+    // It should make sure the data is uploaded to GL memory.
+    abstract protected boolean onBind(GLCanvas canvas);
+
+    // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
+    abstract protected int getTarget();
+
+    public boolean isLoaded() {
+        return mState == STATE_LOADED;
+    }
+
+    // recycle() is called when the texture will never be used again,
+    // so it can free all resources.
+    public void recycle() {
+        freeResource();
+    }
+
+    // yield() is called when the texture will not be used temporarily,
+    // so it can free some resources.
+    // The default implementation unloads the texture from GL memory, so
+    // the subclass should make sure it can reload the texture to GL memory
+    // later, or it will have to override this method.
+    public void yield() {
+        freeResource();
+    }
+
+    private void freeResource() {
+        GLCanvas canvas = mCanvasRef;
+        if (canvas != null && mId != -1) {
+            canvas.unloadTexture(this);
+            mId = -1; // Don't free it again.
+        }
+        mState = STATE_UNLOADED;
+        setAssociatedCanvas(null);
+    }
+
+    @Override
+    protected void finalize() {
+        sInFinalizer.set(BasicTexture.class);
+        recycle();
+        sInFinalizer.set(null);
+    }
+
+    // This is for deciding if we can call Bitmap's recycle().
+    // We cannot call Bitmap's recycle() in finalizer because at that point
+    // the finalizer of Bitmap may already be called so recycle() will crash.
+    public static boolean inFinalizer() {
+        return sInFinalizer.get() != null;
+    }
+
+    public static void yieldAllTextures() {
+        synchronized (sAllTextures) {
+            for (BasicTexture t : sAllTextures.keySet()) {
+                t.yield();
+            }
+        }
+    }
+
+    public static void invalidateAllTextures() {
+        synchronized (sAllTextures) {
+            for (BasicTexture t : sAllTextures.keySet()) {
+                t.mState = STATE_UNLOADED;
+                t.setAssociatedCanvas(null);
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/BitmapTexture.java b/src/com/android/gallery3d/glrenderer/BitmapTexture.java
new file mode 100644
index 0000000..100b0b3
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/BitmapTexture.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+
+import junit.framework.Assert;
+
+// BitmapTexture is a texture whose content is specified by a fixed Bitmap.
+//
+// The texture does not own the Bitmap. The user should make sure the Bitmap
+// is valid during the texture's lifetime. When the texture is recycled, it
+// does not free the Bitmap.
+public class BitmapTexture extends UploadedTexture {
+    protected Bitmap mContentBitmap;
+
+    public BitmapTexture(Bitmap bitmap) {
+        this(bitmap, false);
+    }
+
+    public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
+        super(hasBorder);
+        Assert.assertTrue(bitmap != null && !bitmap.isRecycled());
+        mContentBitmap = bitmap;
+    }
+
+    @Override
+    protected void onFreeBitmap(Bitmap bitmap) {
+        // Do nothing.
+    }
+
+    @Override
+    protected Bitmap onGetBitmap() {
+        return mContentBitmap;
+    }
+
+    public Bitmap getBitmap() {
+        return mContentBitmap;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/CanvasTexture.java b/src/com/android/gallery3d/glrenderer/CanvasTexture.java
new file mode 100644
index 0000000..bff9d4b
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/CanvasTexture.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+
+// CanvasTexture is a texture whose content is the drawing on a Canvas.
+// The subclasses should override onDraw() to draw on the bitmap.
+// By default CanvasTexture is not opaque.
+abstract class CanvasTexture extends UploadedTexture {
+    protected Canvas mCanvas;
+    private final Config mConfig;
+
+    public CanvasTexture(int width, int height) {
+        mConfig = Config.ARGB_8888;
+        setSize(width, height);
+        setOpaque(false);
+    }
+
+    @Override
+    protected Bitmap onGetBitmap() {
+        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig);
+        mCanvas = new Canvas(bitmap);
+        onDraw(mCanvas, bitmap);
+        return bitmap;
+    }
+
+    @Override
+    protected void onFreeBitmap(Bitmap bitmap) {
+        if (!inFinalizer()) {
+            bitmap.recycle();
+        }
+    }
+
+    abstract protected void onDraw(Canvas canvas, Bitmap backing);
+}
diff --git a/src/com/android/gallery3d/glrenderer/ColorTexture.java b/src/com/android/gallery3d/glrenderer/ColorTexture.java
new file mode 100644
index 0000000..904c78e
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/ColorTexture.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import com.android.gallery3d.common.Utils;
+
+// ColorTexture is a texture which fills the rectangle with the specified color.
+public class ColorTexture implements Texture {
+
+    private final int mColor;
+    private int mWidth;
+    private int mHeight;
+
+    public ColorTexture(int color) {
+        mColor = color;
+        mWidth = 1;
+        mHeight = 1;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y) {
+        draw(canvas, x, y, mWidth, mHeight);
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+        canvas.fillRect(x, y, w, h, mColor);
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return Utils.isOpaque(mColor);
+    }
+
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/ExtTexture.java b/src/com/android/gallery3d/glrenderer/ExtTexture.java
new file mode 100644
index 0000000..af76300
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/ExtTexture.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.glrenderer;
+
+// ExtTexture is a texture whose content comes from a external texture.
+// Before drawing, setSize() should be called.
+public class ExtTexture extends BasicTexture {
+
+    private int mTarget;
+
+    public ExtTexture(GLCanvas canvas, int target) {
+        GLId glId = canvas.getGLId();
+        mId = glId.generateTexture();
+        mTarget = target;
+    }
+
+    private void uploadToCanvas(GLCanvas canvas) {
+        canvas.setTextureParameters(this);
+        setAssociatedCanvas(canvas);
+        mState = STATE_LOADED;
+    }
+
+    @Override
+    protected boolean onBind(GLCanvas canvas) {
+        if (!isLoaded()) {
+            uploadToCanvas(canvas);
+        }
+
+        return true;
+    }
+
+    @Override
+    public int getTarget() {
+        return mTarget;
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return true;
+    }
+
+    @Override
+    public void yield() {
+        // we cannot free the texture because we have no backup.
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/FadeInTexture.java b/src/com/android/gallery3d/glrenderer/FadeInTexture.java
new file mode 100644
index 0000000..838d465
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/FadeInTexture.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.glrenderer;
+
+
+// FadeInTexture is a texture which begins with a color, then gradually animates
+// into a given texture.
+public class FadeInTexture extends FadeTexture implements Texture {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FadeInTexture";
+
+    private final int mColor;
+    private final TiledTexture mTexture;
+
+    public FadeInTexture(int color, TiledTexture texture) {
+        super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
+        mColor = color;
+        mTexture = texture;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+        if (isAnimating()) {
+            mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h);
+        } else {
+            mTexture.draw(canvas, x, y, w, h);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/FadeOutTexture.java b/src/com/android/gallery3d/glrenderer/FadeOutTexture.java
new file mode 100644
index 0000000..b05f3b6
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/FadeOutTexture.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.glrenderer;
+
+
+// FadeOutTexture is a texture which begins with a given texture, then gradually animates
+// into fading out totally.
+public class FadeOutTexture extends FadeTexture {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FadeOutTexture";
+
+    private final BasicTexture mTexture;
+
+    public FadeOutTexture(BasicTexture texture) {
+        super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
+        mTexture = texture;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+        if (isAnimating()) {
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.setAlpha(getRatio());
+            mTexture.draw(canvas, x, y, w, h);
+            canvas.restore();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/FadeTexture.java b/src/com/android/gallery3d/glrenderer/FadeTexture.java
new file mode 100644
index 0000000..002c90f
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/FadeTexture.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.glrenderer;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.AnimationTime;
+
+// FadeTexture is a texture which fades the given texture along the time.
+public abstract class FadeTexture implements Texture {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FadeTexture";
+
+    // The duration of the fading animation in milliseconds
+    public static final int DURATION = 180;
+
+    private final long mStartTime;
+    private final int mWidth;
+    private final int mHeight;
+    private final boolean mIsOpaque;
+    private boolean mIsAnimating;
+
+    public FadeTexture(int width, int height, boolean opaque) {
+        mWidth = width;
+        mHeight = height;
+        mIsOpaque = opaque;
+        mStartTime = now();
+        mIsAnimating = true;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y) {
+        draw(canvas, x, y, mWidth, mHeight);
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return mIsOpaque;
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    public boolean isAnimating() {
+        if (mIsAnimating) {
+            if (now() - mStartTime >= DURATION) {
+                mIsAnimating = false;
+            }
+        }
+        return mIsAnimating;
+    }
+
+    protected float getRatio() {
+        float r = (float)(now() - mStartTime) / DURATION;
+        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+    }
+
+    private long now() {
+        return AnimationTime.get();
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLCanvas.java b/src/com/android/gallery3d/glrenderer/GLCanvas.java
new file mode 100644
index 0000000..305e905
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLCanvas.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import javax.microedition.khronos.opengles.GL11;
+
+//
+// GLCanvas gives a convenient interface to draw using OpenGL.
+//
+// When a rectangle is specified in this interface, it means the region
+// [x, x+width) * [y, y+height)
+//
+public interface GLCanvas {
+
+    public GLId getGLId();
+
+    // Tells GLCanvas the size of the underlying GL surface. This should be
+    // called before first drawing and when the size of GL surface is changed.
+    // This is called by GLRoot and should not be called by the clients
+    // who only want to draw on the GLCanvas. Both width and height must be
+    // nonnegative.
+    public abstract void setSize(int width, int height);
+
+    // Clear the drawing buffers. This should only be used by GLRoot.
+    public abstract void clearBuffer();
+
+    public abstract void clearBuffer(float[] argb);
+
+    // Sets and gets the current alpha, alpha must be in [0, 1].
+    public abstract void setAlpha(float alpha);
+
+    public abstract float getAlpha();
+
+    // (current alpha) = (current alpha) * alpha
+    public abstract void multiplyAlpha(float alpha);
+
+    // Change the current transform matrix.
+    public abstract void translate(float x, float y, float z);
+
+    public abstract void translate(float x, float y);
+
+    public abstract void scale(float sx, float sy, float sz);
+
+    public abstract void rotate(float angle, float x, float y, float z);
+
+    public abstract void multiplyMatrix(float[] mMatrix, int offset);
+
+    // Pushes the configuration state (matrix, and alpha) onto
+    // a private stack.
+    public abstract void save();
+
+    // Same as save(), but only save those specified in saveFlags.
+    public abstract void save(int saveFlags);
+
+    public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
+    public static final int SAVE_FLAG_ALPHA = 0x01;
+    public static final int SAVE_FLAG_MATRIX = 0x02;
+
+    // Pops from the top of the stack as current configuration state (matrix,
+    // alpha, and clip). This call balances a previous call to save(), and is
+    // used to remove all modifications to the configuration state since the
+    // last save call.
+    public abstract void restore();
+
+    // Draws a line using the specified paint from (x1, y1) to (x2, y2).
+    // (Both end points are included).
+    public abstract void drawLine(float x1, float y1, float x2, float y2, GLPaint paint);
+
+    // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2).
+    // (Both end points are included).
+    public abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);
+
+    // Fills the specified rectangle with the specified color.
+    public abstract void fillRect(float x, float y, float width, float height, int color);
+
+    // Draws a texture to the specified rectangle.
+    public abstract void drawTexture(
+            BasicTexture texture, int x, int y, int width, int height);
+
+    public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+            int uvBuffer, int indexBuffer, int indexCount);
+
+    // Draws the source rectangle part of the texture to the target rectangle.
+    public abstract void drawTexture(BasicTexture texture, RectF source, RectF target);
+
+    // Draw a texture with a specified texture transform.
+    public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform,
+                int x, int y, int w, int h);
+
+    // Draw two textures to the specified rectangle. The actual texture used is
+    // from * (1 - ratio) + to * ratio
+    // The two textures must have the same size.
+    public abstract void drawMixed(BasicTexture from, int toColor,
+            float ratio, int x, int y, int w, int h);
+
+    // Draw a region of a texture and a specified color to the specified
+    // rectangle. The actual color used is from * (1 - ratio) + to * ratio.
+    // The region of the texture is defined by parameter "src". The target
+    // rectangle is specified by parameter "target".
+    public abstract void drawMixed(BasicTexture from, int toColor,
+            float ratio, RectF src, RectF target);
+
+    // Unloads the specified texture from the canvas. The resource allocated
+    // to draw the texture will be released. The specified texture will return
+    // to the unloaded state. This function should be called only from
+    // BasicTexture or its descendant
+    public abstract boolean unloadTexture(BasicTexture texture);
+
+    // Delete the specified buffer object, similar to unloadTexture.
+    public abstract void deleteBuffer(int bufferId);
+
+    // Delete the textures and buffers in GL side. This function should only be
+    // called in the GL thread.
+    public abstract void deleteRecycledResources();
+
+    // Dump statistics information and clear the counters. For debug only.
+    public abstract void dumpStatisticsAndClear();
+
+    public abstract void beginRenderTarget(RawTexture texture);
+
+    public abstract void endRenderTarget();
+
+    /**
+     * Sets texture parameters to use GL_CLAMP_TO_EDGE for both
+     * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be
+     * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER.
+     * bindTexture() must be called prior to this.
+     *
+     * @param texture The texture to set parameters on.
+     */
+    public abstract void setTextureParameters(BasicTexture texture);
+
+    /**
+     * Initializes the texture to a size by calling texImage2D on it.
+     *
+     * @param texture The texture to initialize the size.
+     * @param format The texture format (e.g. GL_RGBA)
+     * @param type The texture type (e.g. GL_UNSIGNED_BYTE)
+     */
+    public abstract void initializeTextureSize(BasicTexture texture, int format, int type);
+
+    /**
+     * Initializes the texture to a size by calling texImage2D on it.
+     *
+     * @param texture The texture to initialize the size.
+     * @param bitmap The bitmap to initialize the bitmap with.
+     */
+    public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap);
+
+    /**
+     * Calls glTexSubImage2D to upload a bitmap to the texture.
+     *
+     * @param texture The target texture to write to.
+     * @param xOffset Specifies a texel offset in the x direction within the
+     *            texture array.
+     * @param yOffset Specifies a texel offset in the y direction within the
+     *            texture array.
+     * @param format The texture format (e.g. GL_RGBA)
+     * @param type The texture type (e.g. GL_UNSIGNED_BYTE)
+     */
+    public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset,
+            Bitmap bitmap,
+            int format, int type);
+
+    /**
+     * Generates buffers and uploads the buffer data.
+     *
+     * @param buffer The buffer to upload
+     * @return The buffer ID that was generated.
+     */
+    public abstract int uploadBuffer(java.nio.FloatBuffer buffer);
+
+    /**
+     * Generates buffers and uploads the element array buffer data.
+     *
+     * @param buffer The buffer to upload
+     * @return The buffer ID that was generated.
+     */
+    public abstract int uploadBuffer(java.nio.ByteBuffer buffer);
+
+    /**
+     * After LightCycle makes GL calls, this method is called to restore the GL
+     * configuration to the one expected by GLCanvas.
+     */
+    public abstract void recoverFromLightCycle();
+
+    /**
+     * Gets the bounds given by x, y, width, and height as well as the internal
+     * matrix state. There is no special handling for non-90-degree rotations.
+     * It only considers the lower-left and upper-right corners as the bounds.
+     *
+     * @param bounds The output bounds to write to.
+     * @param x The left side of the input rectangle.
+     * @param y The bottom of the input rectangle.
+     * @param width The width of the input rectangle.
+     * @param height The height of the input rectangle.
+     */
+    public abstract void getBounds(Rect bounds, int x, int y, int width, int height);
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES11Canvas.java b/src/com/android/gallery3d/glrenderer/GLES11Canvas.java
new file mode 100644
index 0000000..7013c3d
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES11Canvas.java
@@ -0,0 +1,997 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLU;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.IntArray;
+
+import junit.framework.Assert;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+public class GLES11Canvas implements GLCanvas {
+    @SuppressWarnings("unused")
+    private static final String TAG = "GLCanvasImp";
+
+    private static final float OPAQUE_ALPHA = 0.95f;
+
+    private static final int OFFSET_FILL_RECT = 0;
+    private static final int OFFSET_DRAW_LINE = 4;
+    private static final int OFFSET_DRAW_RECT = 6;
+    private static final float[] BOX_COORDINATES = {
+            0, 0, 1, 0, 0, 1, 1, 1,  // used for filling a rectangle
+            0, 0, 1, 1,              // used for drawing a line
+            0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle
+
+    private GL11 mGL;
+
+    private final float mMatrixValues[] = new float[16];
+    private final float mTextureMatrixValues[] = new float[16];
+
+    // The results of mapPoints are stored in this buffer, and the order is
+    // x1, y1, x2, y2.
+    private final float mMapPointsBuffer[] = new float[4];
+
+    private final float mTextureColor[] = new float[4];
+
+    private int mBoxCoords;
+
+    private GLState mGLState;
+    private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>();
+
+    private float mAlpha;
+    private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
+    private ConfigState mRecycledRestoreAction;
+
+    private final RectF mDrawTextureSourceRect = new RectF();
+    private final RectF mDrawTextureTargetRect = new RectF();
+    private final float[] mTempMatrix = new float[32];
+    private final IntArray mUnboundTextures = new IntArray();
+    private final IntArray mDeleteBuffers = new IntArray();
+    private int mScreenWidth;
+    private int mScreenHeight;
+    private boolean mBlendEnabled = true;
+    private int mFrameBuffer[] = new int[1];
+    private static float[] sCropRect = new float[4];
+
+    private RawTexture mTargetTexture;
+
+    // Drawing statistics
+    int mCountDrawLine;
+    int mCountFillRect;
+    int mCountDrawMesh;
+    int mCountTextureRect;
+    int mCountTextureOES;
+
+    private static GLId mGLId = new GLES11IdImpl();
+
+    public GLES11Canvas(GL11 gl) {
+        mGL = gl;
+        mGLState = new GLState(gl);
+        // First create an nio buffer, then create a VBO from it.
+        int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE;
+        FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+        xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);
+
+        int[] name = new int[1];
+        mGLId.glGenBuffers(1, name, 0);
+        mBoxCoords = name[0];
+
+        gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
+        gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE),
+                xyBuffer, GL11.GL_STATIC_DRAW);
+
+        gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+        gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+        // Enable the texture coordinate array for Texture 1
+        gl.glClientActiveTexture(GL11.GL_TEXTURE1);
+        gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+        gl.glClientActiveTexture(GL11.GL_TEXTURE0);
+        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
+
+        // mMatrixValues and mAlpha will be initialized in setSize()
+    }
+
+    @Override
+    public void setSize(int width, int height) {
+        Assert.assertTrue(width >= 0 && height >= 0);
+
+        if (mTargetTexture == null) {
+            mScreenWidth = width;
+            mScreenHeight = height;
+        }
+        mAlpha = 1.0f;
+
+        GL11 gl = mGL;
+        gl.glViewport(0, 0, width, height);
+        gl.glMatrixMode(GL11.GL_PROJECTION);
+        gl.glLoadIdentity();
+        GLU.gluOrtho2D(gl, 0, width, 0, height);
+
+        gl.glMatrixMode(GL11.GL_MODELVIEW);
+        gl.glLoadIdentity();
+
+        float matrix[] = mMatrixValues;
+        Matrix.setIdentityM(matrix, 0);
+        // to match the graphic coordinate system in android, we flip it vertically.
+        if (mTargetTexture == null) {
+            Matrix.translateM(matrix, 0, 0, height, 0);
+            Matrix.scaleM(matrix, 0, 1, -1, 1);
+        }
+    }
+
+    @Override
+    public void setAlpha(float alpha) {
+        Assert.assertTrue(alpha >= 0 && alpha <= 1);
+        mAlpha = alpha;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public void multiplyAlpha(float alpha) {
+        Assert.assertTrue(alpha >= 0 && alpha <= 1);
+        mAlpha *= alpha;
+    }
+
+    private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+        return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+    }
+
+    @Override
+    public void drawRect(float x, float y, float width, float height, GLPaint paint) {
+        GL11 gl = mGL;
+
+        mGLState.setColorMode(paint.getColor(), mAlpha);
+        mGLState.setLineWidth(paint.getLineWidth());
+
+        saveTransform();
+        translate(x, y);
+        scale(width, height, 1);
+
+        gl.glLoadMatrixf(mMatrixValues, 0);
+        gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);
+
+        restoreTransform();
+        mCountDrawLine++;
+    }
+
+    @Override
+    public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
+        GL11 gl = mGL;
+
+        mGLState.setColorMode(paint.getColor(), mAlpha);
+        mGLState.setLineWidth(paint.getLineWidth());
+
+        saveTransform();
+        translate(x1, y1);
+        scale(x2 - x1, y2 - y1, 1);
+
+        gl.glLoadMatrixf(mMatrixValues, 0);
+        gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2);
+
+        restoreTransform();
+        mCountDrawLine++;
+    }
+
+    @Override
+    public void fillRect(float x, float y, float width, float height, int color) {
+        mGLState.setColorMode(color, mAlpha);
+        GL11 gl = mGL;
+
+        saveTransform();
+        translate(x, y);
+        scale(width, height, 1);
+
+        gl.glLoadMatrixf(mMatrixValues, 0);
+        gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+        restoreTransform();
+        mCountFillRect++;
+    }
+
+    @Override
+    public void translate(float x, float y, float z) {
+        Matrix.translateM(mMatrixValues, 0, x, y, z);
+    }
+
+    // This is a faster version of translate(x, y, z) because
+    // (1) we knows z = 0, (2) we inline the Matrix.translateM call,
+    // (3) we unroll the loop
+    @Override
+    public void translate(float x, float y) {
+        float[] m = mMatrixValues;
+        m[12] += m[0] * x + m[4] * y;
+        m[13] += m[1] * x + m[5] * y;
+        m[14] += m[2] * x + m[6] * y;
+        m[15] += m[3] * x + m[7] * y;
+    }
+
+    @Override
+    public void scale(float sx, float sy, float sz) {
+        Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
+    }
+
+    @Override
+    public void rotate(float angle, float x, float y, float z) {
+        if (angle == 0) return;
+        float[] temp = mTempMatrix;
+        Matrix.setRotateM(temp, 0, angle, x, y, z);
+        Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);
+        System.arraycopy(temp, 16, mMatrixValues, 0, 16);
+    }
+
+    @Override
+    public void multiplyMatrix(float matrix[], int offset) {
+        float[] temp = mTempMatrix;
+        Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
+        System.arraycopy(temp, 0, mMatrixValues, 0, 16);
+    }
+
+    private void textureRect(float x, float y, float width, float height) {
+        GL11 gl = mGL;
+
+        saveTransform();
+        translate(x, y);
+        scale(width, height, 1);
+
+        gl.glLoadMatrixf(mMatrixValues, 0);
+        gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+        restoreTransform();
+        mCountTextureRect++;
+    }
+
+    @Override
+    public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+            int uvBuffer, int indexBuffer, int indexCount) {
+        float alpha = mAlpha;
+        if (!bindTexture(tex)) return;
+
+        mGLState.setBlendEnabled(mBlendEnabled
+                && (!tex.isOpaque() || alpha < OPAQUE_ALPHA));
+        mGLState.setTextureAlpha(alpha);
+
+        // Reset the texture matrix. We will set our own texture coordinates
+        // below.
+        setTextureCoords(0, 0, 1, 1);
+
+        saveTransform();
+        translate(x, y);
+
+        mGL.glLoadMatrixf(mMatrixValues, 0);
+
+        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer);
+        mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+
+        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer);
+        mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+        mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+        mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP,
+                indexCount, GL11.GL_UNSIGNED_BYTE, 0);
+
+        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
+        mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+        mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+        restoreTransform();
+        mCountDrawMesh++;
+    }
+
+    // Transforms two points by the given matrix m. The result
+    // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.
+    private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) {
+        float[] r = mMapPointsBuffer;
+
+        // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.
+        float x3 = m[0] * x1 + m[4] * y1 + m[12];
+        float y3 = m[1] * x1 + m[5] * y1 + m[13];
+        float w3 = m[3] * x1 + m[7] * y1 + m[15];
+        r[0] = x3 / w3;
+        r[1] = y3 / w3;
+
+        // Same for x2 y2.
+        float x4 = m[0] * x2 + m[4] * y2 + m[12];
+        float y4 = m[1] * x2 + m[5] * y2 + m[13];
+        float w4 = m[3] * x2 + m[7] * y2 + m[15];
+        r[2] = x4 / w4;
+        r[3] = y4 / w4;
+
+        return r;
+    }
+
+    private void drawBoundTexture(
+            BasicTexture texture, int x, int y, int width, int height) {
+        // Test whether it has been rotated or flipped, if so, glDrawTexiOES
+        // won't work
+        if (isMatrixRotatedOrFlipped(mMatrixValues)) {
+            if (texture.hasBorder()) {
+                setTextureCoords(
+                        1.0f / texture.getTextureWidth(),
+                        1.0f / texture.getTextureHeight(),
+                        (texture.getWidth() - 1.0f) / texture.getTextureWidth(),
+                        (texture.getHeight() - 1.0f) / texture.getTextureHeight());
+            } else {
+                setTextureCoords(0, 0,
+                        (float) texture.getWidth() / texture.getTextureWidth(),
+                        (float) texture.getHeight() / texture.getTextureHeight());
+            }
+            textureRect(x, y, width, height);
+        } else {
+            // draw the rect from bottom-left to top-right
+            float points[] = mapPoints(
+                    mMatrixValues, x, y + height, x + width, y);
+            x = (int) (points[0] + 0.5f);
+            y = (int) (points[1] + 0.5f);
+            width = (int) (points[2] + 0.5f) - x;
+            height = (int) (points[3] + 0.5f) - y;
+            if (width > 0 && height > 0) {
+                ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);
+                mCountTextureOES++;
+            }
+        }
+    }
+
+    @Override
+    public void drawTexture(
+            BasicTexture texture, int x, int y, int width, int height) {
+        drawTexture(texture, x, y, width, height, mAlpha);
+    }
+
+    private void drawTexture(BasicTexture texture,
+            int x, int y, int width, int height, float alpha) {
+        if (width <= 0 || height <= 0) return;
+
+        mGLState.setBlendEnabled(mBlendEnabled
+                && (!texture.isOpaque() || alpha < OPAQUE_ALPHA));
+        if (!bindTexture(texture)) return;
+        mGLState.setTextureAlpha(alpha);
+        drawBoundTexture(texture, x, y, width, height);
+    }
+
+    @Override
+    public void drawTexture(BasicTexture texture, RectF source, RectF target) {
+        if (target.width() <= 0 || target.height() <= 0) return;
+
+        // Copy the input to avoid changing it.
+        mDrawTextureSourceRect.set(source);
+        mDrawTextureTargetRect.set(target);
+        source = mDrawTextureSourceRect;
+        target = mDrawTextureTargetRect;
+
+        mGLState.setBlendEnabled(mBlendEnabled
+                && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
+        if (!bindTexture(texture)) return;
+        convertCoordinate(source, target, texture);
+        setTextureCoords(source);
+        mGLState.setTextureAlpha(mAlpha);
+        textureRect(target.left, target.top, target.width(), target.height());
+    }
+
+    @Override
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+            int x, int y, int w, int h) {
+        mGLState.setBlendEnabled(mBlendEnabled
+                && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
+        if (!bindTexture(texture)) return;
+        setTextureCoords(mTextureTransform);
+        mGLState.setTextureAlpha(mAlpha);
+        textureRect(x, y, w, h);
+    }
+
+    // This function changes the source coordinate to the texture coordinates.
+    // It also clips the source and target coordinates if it is beyond the
+    // bound of the texture.
+    private static void convertCoordinate(RectF source, RectF target,
+            BasicTexture texture) {
+
+        int width = texture.getWidth();
+        int height = texture.getHeight();
+        int texWidth = texture.getTextureWidth();
+        int texHeight = texture.getTextureHeight();
+        // Convert to texture coordinates
+        source.left /= texWidth;
+        source.right /= texWidth;
+        source.top /= texHeight;
+        source.bottom /= texHeight;
+
+        // Clip if the rendering range is beyond the bound of the texture.
+        float xBound = (float) width / texWidth;
+        if (source.right > xBound) {
+            target.right = target.left + target.width() *
+                    (xBound - source.left) / source.width();
+            source.right = xBound;
+        }
+        float yBound = (float) height / texHeight;
+        if (source.bottom > yBound) {
+            target.bottom = target.top + target.height() *
+                    (yBound - source.top) / source.height();
+            source.bottom = yBound;
+        }
+    }
+
+    @Override
+    public void drawMixed(BasicTexture from,
+            int toColor, float ratio, int x, int y, int w, int h) {
+        drawMixed(from, toColor, ratio, x, y, w, h, mAlpha);
+    }
+
+    private boolean bindTexture(BasicTexture texture) {
+        if (!texture.onBind(this)) return false;
+        int target = texture.getTarget();
+        mGLState.setTextureTarget(target);
+        mGL.glBindTexture(target, texture.getId());
+        return true;
+    }
+
+    private void setTextureColor(float r, float g, float b, float alpha) {
+        float[] color = mTextureColor;
+        color[0] = r;
+        color[1] = g;
+        color[2] = b;
+        color[3] = alpha;
+    }
+
+    private void setMixedColor(int toColor, float ratio, float alpha) {
+        //
+        // The formula we want:
+        //     alpha * ((1 - ratio) * from + ratio * to)
+        //
+        // The formula that GL supports is in the form of:
+        //     combo * from + (1 - combo) * to * scale
+        //
+        // So, we have combo = alpha * (1 - ratio)
+        //     and     scale = alpha * ratio / (1 - combo)
+        //
+        float combo = alpha * (1 - ratio);
+        float scale = alpha * ratio / (1 - combo);
+
+        // Specify the interpolation factor via the alpha component of
+        // GL_TEXTURE_ENV_COLORs.
+        // RGB component are get from toColor and will used as SRC1
+        float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
+        setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
+                ((toColor >>> 8) & 0xff) * colorScale,
+                (toColor & 0xff) * colorScale, combo);
+        GL11 gl = mGL;
+        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
+
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
+
+        // Wire up the interpolation factor for RGB.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+        // Wire up the interpolation factor for alpha.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+    }
+
+    @Override
+    public void drawMixed(BasicTexture from, int toColor, float ratio,
+            RectF source, RectF target) {
+        if (target.width() <= 0 || target.height() <= 0) return;
+
+        if (ratio <= 0.01f) {
+            drawTexture(from, source, target);
+            return;
+        } else if (ratio >= 1) {
+            fillRect(target.left, target.top, target.width(), target.height(), toColor);
+            return;
+        }
+
+        float alpha = mAlpha;
+
+        // Copy the input to avoid changing it.
+        mDrawTextureSourceRect.set(source);
+        mDrawTextureTargetRect.set(target);
+        source = mDrawTextureSourceRect;
+        target = mDrawTextureTargetRect;
+
+        mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+                || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
+
+        if (!bindTexture(from)) return;
+
+        // Interpolate the RGB and alpha values between both textures.
+        mGLState.setTexEnvMode(GL11.GL_COMBINE);
+        setMixedColor(toColor, ratio, alpha);
+        convertCoordinate(source, target, from);
+        setTextureCoords(source);
+        textureRect(target.left, target.top, target.width(), target.height());
+        mGLState.setTexEnvMode(GL11.GL_REPLACE);
+    }
+
+    private void drawMixed(BasicTexture from, int toColor,
+            float ratio, int x, int y, int width, int height, float alpha) {
+        // change from 0 to 0.01f to prevent getting divided by zero below
+        if (ratio <= 0.01f) {
+            drawTexture(from, x, y, width, height, alpha);
+            return;
+        } else if (ratio >= 1) {
+            fillRect(x, y, width, height, toColor);
+            return;
+        }
+
+        mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+                || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
+
+        final GL11 gl = mGL;
+        if (!bindTexture(from)) return;
+
+        // Interpolate the RGB and alpha values between both textures.
+        mGLState.setTexEnvMode(GL11.GL_COMBINE);
+        setMixedColor(toColor, ratio, alpha);
+
+        drawBoundTexture(from, x, y, width, height);
+        mGLState.setTexEnvMode(GL11.GL_REPLACE);
+    }
+
+    // TODO: the code only work for 2D should get fixed for 3D or removed
+    private static final int MSKEW_X = 4;
+    private static final int MSKEW_Y = 1;
+    private static final int MSCALE_X = 0;
+    private static final int MSCALE_Y = 5;
+
+    private static boolean isMatrixRotatedOrFlipped(float matrix[]) {
+        final float eps = 1e-5f;
+        return Math.abs(matrix[MSKEW_X]) > eps
+                || Math.abs(matrix[MSKEW_Y]) > eps
+                || matrix[MSCALE_X] < -eps
+                || matrix[MSCALE_Y] > eps;
+    }
+
+    private static class GLState {
+
+        private final GL11 mGL;
+
+        private int mTexEnvMode = GL11.GL_REPLACE;
+        private float mTextureAlpha = 1.0f;
+        private int mTextureTarget = GL11.GL_TEXTURE_2D;
+        private boolean mBlendEnabled = true;
+        private float mLineWidth = 1.0f;
+        private boolean mLineSmooth = false;
+
+        public GLState(GL11 gl) {
+            mGL = gl;
+
+            // Disable unused state
+            gl.glDisable(GL11.GL_LIGHTING);
+
+            // Enable used features
+            gl.glEnable(GL11.GL_DITHER);
+
+            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
+            gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
+            gl.glEnable(GL11.GL_TEXTURE_2D);
+
+            gl.glTexEnvf(GL11.GL_TEXTURE_ENV,
+                    GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
+
+            // Set the background color
+            gl.glClearColor(0f, 0f, 0f, 0f);
+
+            gl.glEnable(GL11.GL_BLEND);
+            gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+
+            // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel.
+            gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2);
+        }
+
+        public void setTexEnvMode(int mode) {
+            if (mTexEnvMode == mode) return;
+            mTexEnvMode = mode;
+            mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode);
+        }
+
+        public void setLineWidth(float width) {
+            if (mLineWidth == width) return;
+            mLineWidth = width;
+            mGL.glLineWidth(width);
+        }
+
+        public void setTextureAlpha(float alpha) {
+            if (mTextureAlpha == alpha) return;
+            mTextureAlpha = alpha;
+            if (alpha >= OPAQUE_ALPHA) {
+                // The alpha is need for those texture without alpha channel
+                mGL.glColor4f(1, 1, 1, 1);
+                setTexEnvMode(GL11.GL_REPLACE);
+            } else {
+                mGL.glColor4f(alpha, alpha, alpha, alpha);
+                setTexEnvMode(GL11.GL_MODULATE);
+            }
+        }
+
+        public void setColorMode(int color, float alpha) {
+            setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA);
+
+            // Set mTextureAlpha to an invalid value, so that it will reset
+            // again in setTextureAlpha(float) later.
+            mTextureAlpha = -1.0f;
+
+            setTextureTarget(0);
+
+            float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
+            mGL.glColor4x(
+                    Math.round(((color >> 16) & 0xFF) * prealpha),
+                    Math.round(((color >> 8) & 0xFF) * prealpha),
+                    Math.round((color & 0xFF) * prealpha),
+                    Math.round(255 * prealpha));
+        }
+
+        // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled.
+        public void setTextureTarget(int target) {
+            if (mTextureTarget == target) return;
+            if (mTextureTarget != 0) {
+                mGL.glDisable(mTextureTarget);
+            }
+            mTextureTarget = target;
+            if (mTextureTarget != 0) {
+                mGL.glEnable(mTextureTarget);
+            }
+        }
+
+        public void setBlendEnabled(boolean enabled) {
+            if (mBlendEnabled == enabled) return;
+            mBlendEnabled = enabled;
+            if (enabled) {
+                mGL.glEnable(GL11.GL_BLEND);
+            } else {
+                mGL.glDisable(GL11.GL_BLEND);
+            }
+        }
+    }
+
+    @Override
+    public void clearBuffer(float[] argb) {
+        if(argb != null && argb.length == 4) {
+            mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]);
+        } else {
+            mGL.glClearColor(0, 0, 0, 1);
+        }
+        mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
+    }
+
+    @Override
+    public void clearBuffer() {
+        clearBuffer(null);
+    }
+
+    private void setTextureCoords(RectF source) {
+        setTextureCoords(source.left, source.top, source.right, source.bottom);
+    }
+
+    private void setTextureCoords(float left, float top,
+            float right, float bottom) {
+        mGL.glMatrixMode(GL11.GL_TEXTURE);
+        mTextureMatrixValues[0] = right - left;
+        mTextureMatrixValues[5] = bottom - top;
+        mTextureMatrixValues[10] = 1;
+        mTextureMatrixValues[12] = left;
+        mTextureMatrixValues[13] = top;
+        mTextureMatrixValues[15] = 1;
+        mGL.glLoadMatrixf(mTextureMatrixValues, 0);
+        mGL.glMatrixMode(GL11.GL_MODELVIEW);
+    }
+
+    private void setTextureCoords(float[] mTextureTransform) {
+        mGL.glMatrixMode(GL11.GL_TEXTURE);
+        mGL.glLoadMatrixf(mTextureTransform, 0);
+        mGL.glMatrixMode(GL11.GL_MODELVIEW);
+    }
+
+    // unloadTexture and deleteBuffer can be called from the finalizer thread,
+    // so we synchronized on the mUnboundTextures object.
+    @Override
+    public boolean unloadTexture(BasicTexture t) {
+        synchronized (mUnboundTextures) {
+            if (!t.isLoaded()) return false;
+            mUnboundTextures.add(t.mId);
+            return true;
+        }
+    }
+
+    @Override
+    public void deleteBuffer(int bufferId) {
+        synchronized (mUnboundTextures) {
+            mDeleteBuffers.add(bufferId);
+        }
+    }
+
+    @Override
+    public void deleteRecycledResources() {
+        synchronized (mUnboundTextures) {
+            IntArray ids = mUnboundTextures;
+            if (ids.size() > 0) {
+                mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0);
+                ids.clear();
+            }
+
+            ids = mDeleteBuffers;
+            if (ids.size() > 0) {
+                mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0);
+                ids.clear();
+            }
+        }
+    }
+
+    @Override
+    public void save() {
+        save(SAVE_FLAG_ALL);
+    }
+
+    @Override
+    public void save(int saveFlags) {
+        ConfigState config = obtainRestoreConfig();
+
+        if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
+            config.mAlpha = mAlpha;
+        } else {
+            config.mAlpha = -1;
+        }
+
+        if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
+            System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
+        } else {
+            config.mMatrix[0] = Float.NEGATIVE_INFINITY;
+        }
+
+        mRestoreStack.add(config);
+    }
+
+    @Override
+    public void restore() {
+        if (mRestoreStack.isEmpty()) throw new IllegalStateException();
+        ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1);
+        config.restore(this);
+        freeRestoreConfig(config);
+    }
+
+    private void freeRestoreConfig(ConfigState action) {
+        action.mNextFree = mRecycledRestoreAction;
+        mRecycledRestoreAction = action;
+    }
+
+    private ConfigState obtainRestoreConfig() {
+        if (mRecycledRestoreAction != null) {
+            ConfigState result = mRecycledRestoreAction;
+            mRecycledRestoreAction = result.mNextFree;
+            return result;
+        }
+        return new ConfigState();
+    }
+
+    private static class ConfigState {
+        float mAlpha;
+        float mMatrix[] = new float[16];
+        ConfigState mNextFree;
+
+        public void restore(GLES11Canvas canvas) {
+            if (mAlpha >= 0) canvas.setAlpha(mAlpha);
+            if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
+                System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
+            }
+        }
+    }
+
+    @Override
+    public void dumpStatisticsAndClear() {
+        String line = String.format(
+                "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d",
+                mCountDrawMesh, mCountTextureRect, mCountTextureOES,
+                mCountFillRect, mCountDrawLine);
+        mCountDrawMesh = 0;
+        mCountTextureRect = 0;
+        mCountTextureOES = 0;
+        mCountFillRect = 0;
+        mCountDrawLine = 0;
+        Log.d(TAG, line);
+    }
+
+    private void saveTransform() {
+        System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16);
+    }
+
+    private void restoreTransform() {
+        System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);
+    }
+
+    private void setRenderTarget(RawTexture texture) {
+        GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL;
+
+        if (mTargetTexture == null && texture != null) {
+            mGLId.glGenBuffers(1, mFrameBuffer, 0);
+            gl11ep.glBindFramebufferOES(
+                    GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]);
+        }
+        if (mTargetTexture != null && texture  == null) {
+            gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0);
+            gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0);
+        }
+
+        mTargetTexture = texture;
+        if (texture == null) {
+            setSize(mScreenWidth, mScreenHeight);
+        } else {
+            setSize(texture.getWidth(), texture.getHeight());
+
+            if (!texture.isLoaded()) texture.prepare(this);
+
+            gl11ep.glFramebufferTexture2DOES(
+                    GL11ExtensionPack.GL_FRAMEBUFFER_OES,
+                    GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,
+                    GL11.GL_TEXTURE_2D, texture.getId(), 0);
+
+            checkFramebufferStatus(gl11ep);
+        }
+    }
+
+    @Override
+    public void endRenderTarget() {
+        RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1);
+        setRenderTarget(texture);
+        restore(); // restore matrix and alpha
+    }
+
+    @Override
+    public void beginRenderTarget(RawTexture texture) {
+        save(); // save matrix and alpha
+        mTargetStack.add(mTargetTexture);
+        setRenderTarget(texture);
+    }
+
+    private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) {
+        int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES);
+        if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) {
+            String msg = "";
+            switch (status) {
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
+                    msg = "FRAMEBUFFER_FORMATS";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
+                    msg = "FRAMEBUFFER_ATTACHMENT";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
+                    msg = "FRAMEBUFFER_MISSING_ATTACHMENT";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES:
+                    msg = "FRAMEBUFFER_DRAW_BUFFER";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES:
+                    msg = "FRAMEBUFFER_READ_BUFFER";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES:
+                    msg = "FRAMEBUFFER_UNSUPPORTED";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
+                    msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
+                    break;
+            }
+            throw new RuntimeException(msg + ":" + Integer.toHexString(status));
+        }
+    }
+
+    @Override
+    public void setTextureParameters(BasicTexture texture) {
+        int width = texture.getWidth();
+        int height = texture.getHeight();
+        // Define a vertically flipped crop rectangle for OES_draw_texture.
+        // The four values in sCropRect are: left, bottom, width, and
+        // height. Negative value of width or height means flip.
+        sCropRect[0] = 0;
+        sCropRect[1] = height;
+        sCropRect[2] = width;
+        sCropRect[3] = -height;
+
+        // Set texture parameters.
+        int target = texture.getTarget();
+        mGL.glBindTexture(target, texture.getId());
+        mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
+        mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+        mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+        mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+        mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+    }
+
+    @Override
+    public void initializeTextureSize(BasicTexture texture, int format, int type) {
+        int target = texture.getTarget();
+        mGL.glBindTexture(target, texture.getId());
+        int width = texture.getTextureWidth();
+        int height = texture.getTextureHeight();
+        mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null);
+    }
+
+    @Override
+    public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
+        int target = texture.getTarget();
+        mGL.glBindTexture(target, texture.getId());
+        GLUtils.texImage2D(target, 0, bitmap, 0);
+    }
+
+    @Override
+    public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,
+            int format, int type) {
+        int target = texture.getTarget();
+        mGL.glBindTexture(target, texture.getId());
+        GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
+    }
+
+    @Override
+    public int uploadBuffer(FloatBuffer buf) {
+        return uploadBuffer(buf, Float.SIZE / Byte.SIZE);
+    }
+
+    @Override
+    public int uploadBuffer(ByteBuffer buf) {
+        return uploadBuffer(buf, 1);
+    }
+
+    private int uploadBuffer(Buffer buf, int elementSize) {
+        int[] bufferIds = new int[1];
+        mGLId.glGenBuffers(bufferIds.length, bufferIds, 0);
+        int bufferId = bufferIds[0];
+        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId);
+        mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf,
+                GL11.GL_STATIC_DRAW);
+        return bufferId;
+    }
+
+    @Override
+    public void recoverFromLightCycle() {
+        // This is only required for GLES20
+    }
+
+    @Override
+    public void getBounds(Rect bounds, int x, int y, int width, int height) {
+        // This is only required for GLES20
+    }
+
+    @Override
+    public GLId getGLId() {
+        return mGLId;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java b/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java
new file mode 100644
index 0000000..e479373
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.glrenderer;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+/**
+ * Open GL ES 1.1 implementation for generating and destroying texture IDs and
+ * buffer IDs
+ */
+public class GLES11IdImpl implements GLId {
+    private static int sNextId = 1;
+    // Mutex for sNextId
+    private static Object sLock = new Object();
+
+    @Override
+    public int generateTexture() {
+        synchronized (sLock) {
+            return sNextId++;
+        }
+    }
+
+    @Override
+    public void glGenBuffers(int n, int[] buffers, int offset) {
+        synchronized (sLock) {
+            while (n-- > 0) {
+                buffers[offset + n] = sNextId++;
+            }
+        }
+    }
+
+    @Override
+    public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
+        synchronized (sLock) {
+            gl.glDeleteTextures(n, textures, offset);
+        }
+    }
+
+    @Override
+    public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
+        synchronized (sLock) {
+            gl.glDeleteBuffers(n, buffers, offset);
+        }
+    }
+
+    @Override
+    public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
+        synchronized (sLock) {
+            gl11ep.glDeleteFramebuffersOES(n, buffers, offset);
+        }
+    }
+
+
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES20Canvas.java b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
new file mode 100644
index 0000000..4ead131
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
@@ -0,0 +1,1009 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import com.android.gallery3d.util.IntArray;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class GLES20Canvas implements GLCanvas {
+    // ************** Constants **********************
+    private static final String TAG = GLES20Canvas.class.getSimpleName();
+    private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE;
+    private static final float OPAQUE_ALPHA = 0.95f;
+
+    private static final int COORDS_PER_VERTEX = 2;
+    private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE;
+
+    private static final int COUNT_FILL_VERTEX = 4;
+    private static final int COUNT_LINE_VERTEX = 2;
+    private static final int COUNT_RECT_VERTEX = 4;
+    private static final int OFFSET_FILL_RECT = 0;
+    private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX;
+    private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX;
+
+    private static final float[] BOX_COORDINATES = {
+            0, 0, // Fill rectangle
+            1, 0,
+            0, 1,
+            1, 1,
+            0, 0, // Draw line
+            1, 1,
+            0, 0, // Draw rectangle outline
+            0, 1,
+            1, 1,
+            1, 0,
+    };
+
+    private static final float[] BOUNDS_COORDINATES = {
+        0, 0, 0, 1,
+        1, 1, 0, 1,
+    };
+
+    private static final String POSITION_ATTRIBUTE = "aPosition";
+    private static final String COLOR_UNIFORM = "uColor";
+    private static final String MATRIX_UNIFORM = "uMatrix";
+    private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix";
+    private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler";
+    private static final String ALPHA_UNIFORM = "uAlpha";
+    private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate";
+
+    private static final String DRAW_VERTEX_SHADER = ""
+            + "uniform mat4 " + MATRIX_UNIFORM + ";\n"
+            + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n"
+            + "void main() {\n"
+            + "  vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
+            + "  gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
+            + "}\n";
+
+    private static final String DRAW_FRAGMENT_SHADER = ""
+            + "precision mediump float;\n"
+            + "uniform vec4 " + COLOR_UNIFORM + ";\n"
+            + "void main() {\n"
+            + "  gl_FragColor = " + COLOR_UNIFORM + ";\n"
+            + "}\n";
+
+    private static final String TEXTURE_VERTEX_SHADER = ""
+            + "uniform mat4 " + MATRIX_UNIFORM + ";\n"
+            + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM + ";\n"
+            + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n"
+            + "varying vec2 vTextureCoord;\n"
+            + "void main() {\n"
+            + "  vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
+            + "  gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
+            + "  vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n"
+            + "}\n";
+
+    private static final String MESH_VERTEX_SHADER = ""
+            + "uniform mat4 " + MATRIX_UNIFORM + ";\n"
+            + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n"
+            + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n"
+            + "varying vec2 vTextureCoord;\n"
+            + "void main() {\n"
+            + "  vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
+            + "  gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
+            + "  vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n"
+            + "}\n";
+
+    private static final String TEXTURE_FRAGMENT_SHADER = ""
+            + "precision mediump float;\n"
+            + "varying vec2 vTextureCoord;\n"
+            + "uniform float " + ALPHA_UNIFORM + ";\n"
+            + "uniform sampler2D " + TEXTURE_SAMPLER_UNIFORM + ";\n"
+            + "void main() {\n"
+            + "  gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n"
+            + "  gl_FragColor *= " + ALPHA_UNIFORM + ";\n"
+            + "}\n";
+
+    private static final String OES_TEXTURE_FRAGMENT_SHADER = ""
+            + "#extension GL_OES_EGL_image_external : require\n"
+            + "precision mediump float;\n"
+            + "varying vec2 vTextureCoord;\n"
+            + "uniform float " + ALPHA_UNIFORM + ";\n"
+            + "uniform samplerExternalOES " + TEXTURE_SAMPLER_UNIFORM + ";\n"
+            + "void main() {\n"
+            + "  gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n"
+            + "  gl_FragColor *= " + ALPHA_UNIFORM + ";\n"
+            + "}\n";
+
+    private static final int INITIAL_RESTORE_STATE_SIZE = 8;
+    private static final int MATRIX_SIZE = 16;
+
+    // Keep track of restore state
+    private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE];
+    private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE];
+    private IntArray mSaveFlags = new IntArray();
+
+    private int mCurrentAlphaIndex = 0;
+    private int mCurrentMatrixIndex = 0;
+
+    // Viewport size
+    private int mWidth;
+    private int mHeight;
+
+    // Projection matrix
+    private float[] mProjectionMatrix = new float[MATRIX_SIZE];
+
+    // Screen size for when we aren't bound to a texture
+    private int mScreenWidth;
+    private int mScreenHeight;
+
+    // GL programs
+    private int mDrawProgram;
+    private int mTextureProgram;
+    private int mOesTextureProgram;
+    private int mMeshProgram;
+
+    // GL buffer containing BOX_COORDINATES
+    private int mBoxCoordinates;
+
+    // Handle indices -- common
+    private static final int INDEX_POSITION = 0;
+    private static final int INDEX_MATRIX = 1;
+
+    // Handle indices -- draw
+    private static final int INDEX_COLOR = 2;
+
+    // Handle indices -- texture
+    private static final int INDEX_TEXTURE_MATRIX = 2;
+    private static final int INDEX_TEXTURE_SAMPLER = 3;
+    private static final int INDEX_ALPHA = 4;
+
+    // Handle indices -- mesh
+    private static final int INDEX_TEXTURE_COORD = 2;
+
+    private abstract static class ShaderParameter {
+        public int handle;
+        protected final String mName;
+
+        public ShaderParameter(String name) {
+            mName = name;
+        }
+
+        public abstract void loadHandle(int program);
+    }
+
+    private static class UniformShaderParameter extends ShaderParameter {
+        public UniformShaderParameter(String name) {
+            super(name);
+        }
+
+        @Override
+        public void loadHandle(int program) {
+            handle = GLES20.glGetUniformLocation(program, mName);
+            checkError();
+        }
+    }
+
+    private static class AttributeShaderParameter extends ShaderParameter {
+        public AttributeShaderParameter(String name) {
+            super(name);
+        }
+
+        @Override
+        public void loadHandle(int program) {
+            handle = GLES20.glGetAttribLocation(program, mName);
+            checkError();
+        }
+    }
+
+    ShaderParameter[] mDrawParameters = {
+            new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+            new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+            new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR
+    };
+    ShaderParameter[] mTextureParameters = {
+            new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+            new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+            new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX
+            new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
+            new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
+    };
+    ShaderParameter[] mOesTextureParameters = {
+            new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+            new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+            new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX
+            new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
+            new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
+    };
+    ShaderParameter[] mMeshParameters = {
+            new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+            new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+            new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD
+            new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
+            new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
+    };
+
+    private final IntArray mUnboundTextures = new IntArray();
+    private final IntArray mDeleteBuffers = new IntArray();
+
+    // Keep track of statistics for debugging
+    private int mCountDrawMesh = 0;
+    private int mCountTextureRect = 0;
+    private int mCountFillRect = 0;
+    private int mCountDrawLine = 0;
+
+    // Buffer for framebuffer IDs -- we keep track so we can switch the attached
+    // texture.
+    private int[] mFrameBuffer = new int[1];
+
+    // Bound textures.
+    private ArrayList<RawTexture> mTargetTextures = new ArrayList<RawTexture>();
+
+    // Temporary variables used within calculations
+    private final float[] mTempMatrix = new float[32];
+    private final float[] mTempColor = new float[4];
+    private final RectF mTempSourceRect = new RectF();
+    private final RectF mTempTargetRect = new RectF();
+    private final float[] mTempTextureMatrix = new float[MATRIX_SIZE];
+    private final int[] mTempIntArray = new int[1];
+
+    private static final GLId mGLId = new GLES20IdImpl();
+
+    public GLES20Canvas() {
+        Matrix.setIdentityM(mTempTextureMatrix, 0);
+        Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
+        mAlphas[mCurrentAlphaIndex] = 1f;
+        mTargetTextures.add(null);
+
+        FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES);
+        mBoxCoordinates = uploadBuffer(boxBuffer);
+
+        int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER);
+        int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER);
+        int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER);
+        int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER);
+        int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER);
+        int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
+                OES_TEXTURE_FRAGMENT_SHADER);
+
+        mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters);
+        mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader,
+                mTextureParameters);
+        mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader,
+                mOesTextureParameters);
+        mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters);
+        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+        checkError();
+    }
+
+    private static FloatBuffer createBuffer(float[] values) {
+        // First create an nio buffer, then create a VBO from it.
+        int size = values.length * FLOAT_SIZE;
+        FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
+                .asFloatBuffer();
+        buffer.put(values, 0, values.length).position(0);
+        return buffer;
+    }
+
+    private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) {
+        int program = GLES20.glCreateProgram();
+        checkError();
+        if (program == 0) {
+            throw new RuntimeException("Cannot create GL program: " + GLES20.glGetError());
+        }
+        GLES20.glAttachShader(program, vertexShader);
+        checkError();
+        GLES20.glAttachShader(program, fragmentShader);
+        checkError();
+        GLES20.glLinkProgram(program);
+        checkError();
+        int[] mLinkStatus = mTempIntArray;
+        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0);
+        if (mLinkStatus[0] != GLES20.GL_TRUE) {
+            Log.e(TAG, "Could not link program: ");
+            Log.e(TAG, GLES20.glGetProgramInfoLog(program));
+            GLES20.glDeleteProgram(program);
+            program = 0;
+        }
+        for (int i = 0; i < params.length; i++) {
+            params[i].loadHandle(program);
+        }
+        return program;
+    }
+
+    private static int loadShader(int type, String shaderCode) {
+        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
+        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
+        int shader = GLES20.glCreateShader(type);
+
+        // add the source code to the shader and compile it
+        GLES20.glShaderSource(shader, shaderCode);
+        checkError();
+        GLES20.glCompileShader(shader);
+        checkError();
+
+        return shader;
+    }
+
+    @Override
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+        GLES20.glViewport(0, 0, mWidth, mHeight);
+        checkError();
+        Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
+        Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1);
+        if (getTargetTexture() == null) {
+            mScreenWidth = width;
+            mScreenHeight = height;
+            Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0);
+            Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1);
+        }
+    }
+
+    @Override
+    public void clearBuffer() {
+        GLES20.glClearColor(0f, 0f, 0f, 1f);
+        checkError();
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+        checkError();
+    }
+
+    @Override
+    public void clearBuffer(float[] argb) {
+        GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]);
+        checkError();
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+        checkError();
+    }
+
+    @Override
+    public float getAlpha() {
+        return mAlphas[mCurrentAlphaIndex];
+    }
+
+    @Override
+    public void setAlpha(float alpha) {
+        mAlphas[mCurrentAlphaIndex] = alpha;
+    }
+
+    @Override
+    public void multiplyAlpha(float alpha) {
+        setAlpha(getAlpha() * alpha);
+    }
+
+    @Override
+    public void translate(float x, float y, float z) {
+        Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z);
+    }
+
+    // This is a faster version of translate(x, y, z) because
+    // (1) we knows z = 0, (2) we inline the Matrix.translateM call,
+    // (3) we unroll the loop
+    @Override
+    public void translate(float x, float y) {
+        int index = mCurrentMatrixIndex;
+        float[] m = mMatrices;
+        m[index + 12] += m[index + 0] * x + m[index + 4] * y;
+        m[index + 13] += m[index + 1] * x + m[index + 5] * y;
+        m[index + 14] += m[index + 2] * x + m[index + 6] * y;
+        m[index + 15] += m[index + 3] * x + m[index + 7] * y;
+    }
+
+    @Override
+    public void scale(float sx, float sy, float sz) {
+        Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz);
+    }
+
+    @Override
+    public void rotate(float angle, float x, float y, float z) {
+        if (angle == 0f) {
+            return;
+        }
+        float[] temp = mTempMatrix;
+        Matrix.setRotateM(temp, 0, angle, x, y, z);
+        float[] matrix = mMatrices;
+        int index = mCurrentMatrixIndex;
+        Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0);
+        System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE);
+    }
+
+    @Override
+    public void multiplyMatrix(float[] matrix, int offset) {
+        float[] temp = mTempMatrix;
+        float[] currentMatrix = mMatrices;
+        int index = mCurrentMatrixIndex;
+        Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset);
+        System.arraycopy(temp, 0, currentMatrix, index, 16);
+    }
+
+    @Override
+    public void save() {
+        save(SAVE_FLAG_ALL);
+    }
+
+    @Override
+    public void save(int saveFlags) {
+        boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;
+        if (saveAlpha) {
+            float currentAlpha = getAlpha();
+            mCurrentAlphaIndex++;
+            if (mAlphas.length <= mCurrentAlphaIndex) {
+                mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2);
+            }
+            mAlphas[mCurrentAlphaIndex] = currentAlpha;
+        }
+        boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;
+        if (saveMatrix) {
+            int currentIndex = mCurrentMatrixIndex;
+            mCurrentMatrixIndex += MATRIX_SIZE;
+            if (mMatrices.length <= mCurrentMatrixIndex) {
+                mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2);
+            }
+            System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE);
+        }
+        mSaveFlags.add(saveFlags);
+    }
+
+    @Override
+    public void restore() {
+        int restoreFlags = mSaveFlags.removeLast();
+        boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;
+        if (restoreAlpha) {
+            mCurrentAlphaIndex--;
+        }
+        boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;
+        if (restoreMatrix) {
+            mCurrentMatrixIndex -= MATRIX_SIZE;
+        }
+    }
+
+    @Override
+    public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
+        draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1,
+                paint);
+        mCountDrawLine++;
+    }
+
+    @Override
+    public void drawRect(float x, float y, float width, float height, GLPaint paint) {
+        draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint);
+        mCountDrawLine++;
+    }
+
+    private void draw(int type, int offset, int count, float x, float y, float width, float height,
+            GLPaint paint) {
+        draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth());
+    }
+
+    private void draw(int type, int offset, int count, float x, float y, float width, float height,
+            int color, float lineWidth) {
+        prepareDraw(offset, color, lineWidth);
+        draw(mDrawParameters, type, count, x, y, width, height);
+    }
+
+    private void prepareDraw(int offset, int color, float lineWidth) {
+        GLES20.glUseProgram(mDrawProgram);
+        checkError();
+        if (lineWidth > 0) {
+            GLES20.glLineWidth(lineWidth);
+            checkError();
+        }
+        float[] colorArray = getColor(color);
+        boolean blendingEnabled = (colorArray[3] < 1f);
+        enableBlending(blendingEnabled);
+        if (blendingEnabled) {
+            GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]);
+            checkError();
+        }
+
+        GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0);
+        setPosition(mDrawParameters, offset);
+        checkError();
+    }
+
+    private float[] getColor(int color) {
+        float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha();
+        float red = ((color >>> 16) & 0xFF) / 255f * alpha;
+        float green = ((color >>> 8) & 0xFF) / 255f * alpha;
+        float blue = (color & 0xFF) / 255f * alpha;
+        mTempColor[0] = red;
+        mTempColor[1] = green;
+        mTempColor[2] = blue;
+        mTempColor[3] = alpha;
+        return mTempColor;
+    }
+
+    private void enableBlending(boolean enableBlending) {
+        if (enableBlending) {
+            GLES20.glEnable(GLES20.GL_BLEND);
+            checkError();
+        } else {
+            GLES20.glDisable(GLES20.GL_BLEND);
+            checkError();
+        }
+    }
+
+    private void setPosition(ShaderParameter[] params, int offset) {
+        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates);
+        checkError();
+        GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX,
+                GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE);
+        checkError();
+        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+        checkError();
+    }
+
+    private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width,
+            float height) {
+        setMatrix(params, x, y, width, height);
+        int positionHandle = params[INDEX_POSITION].handle;
+        GLES20.glEnableVertexAttribArray(positionHandle);
+        checkError();
+        GLES20.glDrawArrays(type, 0, count);
+        checkError();
+        GLES20.glDisableVertexAttribArray(positionHandle);
+        checkError();
+    }
+
+    private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) {
+        Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f);
+        Matrix.scaleM(mTempMatrix, 0, width, height, 1f);
+        Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0);
+        GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE);
+        checkError();
+    }
+
+    @Override
+    public void fillRect(float x, float y, float width, float height, int color) {
+        draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height,
+                color, 0f);
+        mCountFillRect++;
+    }
+
+    @Override
+    public void drawTexture(BasicTexture texture, int x, int y, int width, int height) {
+        if (width <= 0 || height <= 0) {
+            return;
+        }
+        copyTextureCoordinates(texture, mTempSourceRect);
+        mTempTargetRect.set(x, y, x + width, y + height);
+        convertCoordinate(mTempSourceRect, mTempTargetRect, texture);
+        drawTextureRect(texture, mTempSourceRect, mTempTargetRect);
+    }
+
+    private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) {
+        int left = 0;
+        int top = 0;
+        int right = texture.getWidth();
+        int bottom = texture.getHeight();
+        if (texture.hasBorder()) {
+            left = 1;
+            top = 1;
+            right -= 1;
+            bottom -= 1;
+        }
+        outRect.set(left, top, right, bottom);
+    }
+
+    @Override
+    public void drawTexture(BasicTexture texture, RectF source, RectF target) {
+        if (target.width() <= 0 || target.height() <= 0) {
+            return;
+        }
+        mTempSourceRect.set(source);
+        mTempTargetRect.set(target);
+
+        convertCoordinate(mTempSourceRect, mTempTargetRect, texture);
+        drawTextureRect(texture, mTempSourceRect, mTempTargetRect);
+    }
+
+    @Override
+    public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w,
+            int h) {
+        if (w <= 0 || h <= 0) {
+            return;
+        }
+        mTempTargetRect.set(x, y, x + w, y + h);
+        drawTextureRect(texture, textureTransform, mTempTargetRect);
+    }
+
+    private void drawTextureRect(BasicTexture texture, RectF source, RectF target) {
+        setTextureMatrix(source);
+        drawTextureRect(texture, mTempTextureMatrix, target);
+    }
+
+    private void setTextureMatrix(RectF source) {
+        mTempTextureMatrix[0] = source.width();
+        mTempTextureMatrix[5] = source.height();
+        mTempTextureMatrix[12] = source.left;
+        mTempTextureMatrix[13] = source.top;
+    }
+
+    // This function changes the source coordinate to the texture coordinates.
+    // It also clips the source and target coordinates if it is beyond the
+    // bound of the texture.
+    private static void convertCoordinate(RectF source, RectF target, BasicTexture texture) {
+        int width = texture.getWidth();
+        int height = texture.getHeight();
+        int texWidth = texture.getTextureWidth();
+        int texHeight = texture.getTextureHeight();
+        // Convert to texture coordinates
+        source.left /= texWidth;
+        source.right /= texWidth;
+        source.top /= texHeight;
+        source.bottom /= texHeight;
+
+        // Clip if the rendering range is beyond the bound of the texture.
+        float xBound = (float) width / texWidth;
+        if (source.right > xBound) {
+            target.right = target.left + target.width() * (xBound - source.left) / source.width();
+            source.right = xBound;
+        }
+        float yBound = (float) height / texHeight;
+        if (source.bottom > yBound) {
+            target.bottom = target.top + target.height() * (yBound - source.top) / source.height();
+            source.bottom = yBound;
+        }
+    }
+
+    private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) {
+        ShaderParameter[] params = prepareTexture(texture);
+        setPosition(params, OFFSET_FILL_RECT);
+        GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0);
+        checkError();
+        if (texture.isFlippedVertically()) {
+            save(SAVE_FLAG_MATRIX);
+            translate(0, target.centerY());
+            scale(1, -1, 1);
+            translate(0, -target.centerY());
+        }
+        draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top,
+                target.width(), target.height());
+        if (texture.isFlippedVertically()) {
+            restore();
+        }
+        mCountTextureRect++;
+    }
+
+    private ShaderParameter[] prepareTexture(BasicTexture texture) {
+        ShaderParameter[] params;
+        int program;
+        if (texture.getTarget() == GLES20.GL_TEXTURE_2D) {
+            params = mTextureParameters;
+            program = mTextureProgram;
+        } else {
+            params = mOesTextureParameters;
+            program = mOesTextureProgram;
+        }
+        prepareTexture(texture, program, params);
+        return params;
+    }
+
+    private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) {
+        GLES20.glUseProgram(program);
+        checkError();
+        enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA);
+        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+        checkError();
+        texture.onBind(this);
+        GLES20.glBindTexture(texture.getTarget(), texture.getId());
+        checkError();
+        GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0);
+        checkError();
+        GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha());
+        checkError();
+    }
+
+    @Override
+    public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer,
+            int indexBuffer, int indexCount) {
+        prepareTexture(texture, mMeshProgram, mMeshParameters);
+
+        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+        checkError();
+
+        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer);
+        checkError();
+        int positionHandle = mMeshParameters[INDEX_POSITION].handle;
+        GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false,
+                VERTEX_STRIDE, 0);
+        checkError();
+
+        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer);
+        checkError();
+        int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle;
+        GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
+                false, VERTEX_STRIDE, 0);
+        checkError();
+        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+        checkError();
+
+        GLES20.glEnableVertexAttribArray(positionHandle);
+        checkError();
+        GLES20.glEnableVertexAttribArray(texCoordHandle);
+        checkError();
+
+        setMatrix(mMeshParameters, x, y, 1, 1);
+        GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0);
+        checkError();
+
+        GLES20.glDisableVertexAttribArray(positionHandle);
+        checkError();
+        GLES20.glDisableVertexAttribArray(texCoordHandle);
+        checkError();
+        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
+        checkError();
+        mCountDrawMesh++;
+    }
+
+    @Override
+    public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) {
+        copyTextureCoordinates(texture, mTempSourceRect);
+        mTempTargetRect.set(x, y, x + w, y + h);
+        drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect);
+    }
+
+    @Override
+    public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) {
+        if (target.width() <= 0 || target.height() <= 0) {
+            return;
+        }
+        save(SAVE_FLAG_ALPHA);
+
+        float currentAlpha = getAlpha();
+        float cappedRatio = Math.min(1f, Math.max(0f, ratio));
+
+        float textureAlpha = (1f - cappedRatio) * currentAlpha;
+        setAlpha(textureAlpha);
+        drawTexture(texture, source, target);
+
+        float colorAlpha = cappedRatio * currentAlpha;
+        setAlpha(colorAlpha);
+        fillRect(target.left, target.top, target.width(), target.height(), toColor);
+
+        restore();
+    }
+
+    @Override
+    public boolean unloadTexture(BasicTexture texture) {
+        boolean unload = texture.isLoaded();
+        if (unload) {
+            synchronized (mUnboundTextures) {
+                mUnboundTextures.add(texture.getId());
+            }
+        }
+        return unload;
+    }
+
+    @Override
+    public void deleteBuffer(int bufferId) {
+        synchronized (mUnboundTextures) {
+            mDeleteBuffers.add(bufferId);
+        }
+    }
+
+    @Override
+    public void deleteRecycledResources() {
+        synchronized (mUnboundTextures) {
+            IntArray ids = mUnboundTextures;
+            if (mUnboundTextures.size() > 0) {
+                mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0);
+                ids.clear();
+            }
+
+            ids = mDeleteBuffers;
+            if (ids.size() > 0) {
+                mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0);
+                ids.clear();
+            }
+        }
+    }
+
+    @Override
+    public void dumpStatisticsAndClear() {
+        String line = String.format("MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh,
+                mCountTextureRect, mCountFillRect, mCountDrawLine);
+        mCountDrawMesh = 0;
+        mCountTextureRect = 0;
+        mCountFillRect = 0;
+        mCountDrawLine = 0;
+        Log.d(TAG, line);
+    }
+
+    @Override
+    public void endRenderTarget() {
+        RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1);
+        RawTexture texture = getTargetTexture();
+        setRenderTarget(oldTexture, texture);
+        restore(); // restore matrix and alpha
+    }
+
+    @Override
+    public void beginRenderTarget(RawTexture texture) {
+        save(); // save matrix and alpha and blending
+        RawTexture oldTexture = getTargetTexture();
+        mTargetTextures.add(texture);
+        setRenderTarget(oldTexture, texture);
+    }
+
+    private RawTexture getTargetTexture() {
+        return mTargetTextures.get(mTargetTextures.size() - 1);
+    }
+
+    private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) {
+        if (oldTexture == null && texture != null) {
+            GLES20.glGenFramebuffers(1, mFrameBuffer, 0);
+            checkError();
+            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]);
+            checkError();
+        } else if (oldTexture != null && texture == null) {
+            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+            checkError();
+            GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0);
+            checkError();
+        }
+
+        if (texture == null) {
+            setSize(mScreenWidth, mScreenHeight);
+        } else {
+            setSize(texture.getWidth(), texture.getHeight());
+
+            if (!texture.isLoaded()) {
+                texture.prepare(this);
+            }
+
+            GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
+                    texture.getTarget(), texture.getId(), 0);
+            checkError();
+
+            checkFramebufferStatus();
+        }
+    }
+
+    private static void checkFramebufferStatus() {
+        int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
+        if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
+            String msg = "";
+            switch (status) {
+                case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
+                    msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT";
+                    break;
+                case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
+                    msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
+                    break;
+                case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
+                    msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT";
+                    break;
+                case GLES20.GL_FRAMEBUFFER_UNSUPPORTED:
+                    msg = "GL_FRAMEBUFFER_UNSUPPORTED";
+                    break;
+            }
+            throw new RuntimeException(msg + ":" + Integer.toHexString(status));
+        }
+    }
+
+    @Override
+    public void setTextureParameters(BasicTexture texture) {
+        int target = texture.getTarget();
+        GLES20.glBindTexture(target, texture.getId());
+        checkError();
+        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+        GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+        GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+    }
+
+    @Override
+    public void initializeTextureSize(BasicTexture texture, int format, int type) {
+        int target = texture.getTarget();
+        GLES20.glBindTexture(target, texture.getId());
+        checkError();
+        int width = texture.getTextureWidth();
+        int height = texture.getTextureHeight();
+        GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null);
+    }
+
+    @Override
+    public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
+        int target = texture.getTarget();
+        GLES20.glBindTexture(target, texture.getId());
+        checkError();
+        GLUtils.texImage2D(target, 0, bitmap, 0);
+    }
+
+    @Override
+    public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,
+            int format, int type) {
+        int target = texture.getTarget();
+        GLES20.glBindTexture(target, texture.getId());
+        checkError();
+        GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
+    }
+
+    @Override
+    public int uploadBuffer(FloatBuffer buf) {
+        return uploadBuffer(buf, FLOAT_SIZE);
+    }
+
+    @Override
+    public int uploadBuffer(ByteBuffer buf) {
+        return uploadBuffer(buf, 1);
+    }
+
+    private int uploadBuffer(Buffer buffer, int elementSize) {
+        mGLId.glGenBuffers(1, mTempIntArray, 0);
+        checkError();
+        int bufferId = mTempIntArray[0];
+        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId);
+        checkError();
+        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer,
+                GLES20.GL_STATIC_DRAW);
+        checkError();
+        return bufferId;
+    }
+
+    public static void checkError() {
+        int error = GLES20.glGetError();
+        if (error != 0) {
+            Throwable t = new Throwable();
+            Log.e(TAG, "GL error: " + error, t);
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private static void printMatrix(String message, float[] m, int offset) {
+        StringBuilder b = new StringBuilder(message);
+        for (int i = 0; i < MATRIX_SIZE; i++) {
+            b.append(' ');
+            if (i % 4 == 0) {
+                b.append('\n');
+            }
+            b.append(m[offset + i]);
+        }
+        Log.v(TAG, b.toString());
+    }
+
+    @Override
+    public void recoverFromLightCycle() {
+        GLES20.glViewport(0, 0, mWidth, mHeight);
+        GLES20.glDisable(GLES20.GL_DEPTH_TEST);
+        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+        checkError();
+    }
+
+    @Override
+    public void getBounds(Rect bounds, int x, int y, int width, int height) {
+        Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f);
+        Matrix.scaleM(mTempMatrix, 0, width, height, 1f);
+        Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0);
+        Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4);
+        bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]);
+        bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]);
+        bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]);
+        bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]);
+        bounds.sort();
+    }
+
+    @Override
+    public GLId getGLId() {
+        return mGLId;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java b/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java
new file mode 100644
index 0000000..6cd7149
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java
@@ -0,0 +1,42 @@
+package com.android.gallery3d.glrenderer;
+
+import android.opengl.GLES20;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+public class GLES20IdImpl implements GLId {
+    private final int[] mTempIntArray = new int[1];
+
+    @Override
+    public int generateTexture() {
+        GLES20.glGenTextures(1, mTempIntArray, 0);
+        GLES20Canvas.checkError();
+        return mTempIntArray[0];
+    }
+
+    @Override
+    public void glGenBuffers(int n, int[] buffers, int offset) {
+        GLES20.glGenBuffers(n, buffers, offset);
+        GLES20Canvas.checkError();
+    }
+
+    @Override
+    public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
+        GLES20.glDeleteTextures(n, textures, offset);
+        GLES20Canvas.checkError();
+    }
+
+
+    @Override
+    public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
+        GLES20.glDeleteBuffers(n, buffers, offset);
+        GLES20Canvas.checkError();
+    }
+
+    @Override
+    public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
+        GLES20.glDeleteFramebuffers(n, buffers, offset);
+        GLES20Canvas.checkError();
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLId.java b/src/com/android/gallery3d/glrenderer/GLId.java
new file mode 100644
index 0000000..3cec558
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLId.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.glrenderer;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+// This mimics corresponding GL functions.
+public interface GLId {
+    public int generateTexture();
+
+    public void glGenBuffers(int n, int[] buffers, int offset);
+
+    public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset);
+
+    public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset);
+
+    public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset);
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLPaint.java b/src/com/android/gallery3d/glrenderer/GLPaint.java
new file mode 100644
index 0000000..16b2206
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLPaint.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import junit.framework.Assert;
+
+public class GLPaint {
+    private float mLineWidth = 1f;
+    private int mColor = 0;
+
+    public void setColor(int color) {
+        mColor = color;
+    }
+
+    public int getColor() {
+        return mColor;
+    }
+
+    public void setLineWidth(float width) {
+        Assert.assertTrue(width >= 0);
+        mLineWidth = width;
+    }
+
+    public float getLineWidth() {
+        return mLineWidth;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/MultiLineTexture.java b/src/com/android/gallery3d/glrenderer/MultiLineTexture.java
new file mode 100644
index 0000000..82839f1
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/MultiLineTexture.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+
+
+// MultiLineTexture is a texture shows the content of a specified String.
+//
+// To create a MultiLineTexture, use the newInstance() method and specify
+// the String, the font size, and the color.
+class MultiLineTexture extends CanvasTexture {
+    private final Layout mLayout;
+
+    private MultiLineTexture(Layout layout) {
+        super(layout.getWidth(), layout.getHeight());
+        mLayout = layout;
+    }
+
+    public static MultiLineTexture newInstance(
+            String text, int maxWidth, float textSize, int color,
+            Layout.Alignment alignment) {
+        TextPaint paint = StringTexture.getDefaultPaint(textSize, color);
+        Layout layout = new StaticLayout(text, 0, text.length(), paint,
+                maxWidth, alignment, 1, 0, true, null, 0);
+
+        return new MultiLineTexture(layout);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas, Bitmap backing) {
+        mLayout.draw(canvas);
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/NinePatchChunk.java b/src/com/android/gallery3d/glrenderer/NinePatchChunk.java
new file mode 100644
index 0000000..9dc3266
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/NinePatchChunk.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Rect;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+// See "frameworks/base/include/utils/ResourceTypes.h" for the format of
+// NinePatch chunk.
+class NinePatchChunk {
+
+    public static final int NO_COLOR = 0x00000001;
+    public static final int TRANSPARENT_COLOR = 0x00000000;
+
+    public Rect mPaddings = new Rect();
+
+    public int mDivX[];
+    public int mDivY[];
+    public int mColor[];
+
+    private static void readIntArray(int[] data, ByteBuffer buffer) {
+        for (int i = 0, n = data.length; i < n; ++i) {
+            data[i] = buffer.getInt();
+        }
+    }
+
+    private static void checkDivCount(int length) {
+        if (length == 0 || (length & 0x01) != 0) {
+            throw new RuntimeException("invalid nine-patch: " + length);
+        }
+    }
+
+    public static NinePatchChunk deserialize(byte[] data) {
+        ByteBuffer byteBuffer =
+                ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
+
+        byte wasSerialized = byteBuffer.get();
+        if (wasSerialized == 0) return null;
+
+        NinePatchChunk chunk = new NinePatchChunk();
+        chunk.mDivX = new int[byteBuffer.get()];
+        chunk.mDivY = new int[byteBuffer.get()];
+        chunk.mColor = new int[byteBuffer.get()];
+
+        checkDivCount(chunk.mDivX.length);
+        checkDivCount(chunk.mDivY.length);
+
+        // skip 8 bytes
+        byteBuffer.getInt();
+        byteBuffer.getInt();
+
+        chunk.mPaddings.left = byteBuffer.getInt();
+        chunk.mPaddings.right = byteBuffer.getInt();
+        chunk.mPaddings.top = byteBuffer.getInt();
+        chunk.mPaddings.bottom = byteBuffer.getInt();
+
+        // skip 4 bytes
+        byteBuffer.getInt();
+
+        readIntArray(chunk.mDivX, byteBuffer);
+        readIntArray(chunk.mDivY, byteBuffer);
+        readIntArray(chunk.mColor, byteBuffer);
+
+        return chunk;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/glrenderer/NinePatchTexture.java b/src/com/android/gallery3d/glrenderer/NinePatchTexture.java
new file mode 100644
index 0000000..d0ddc46
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/NinePatchTexture.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+
+import com.android.gallery3d.common.Utils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+// NinePatchTexture is a texture backed by a NinePatch resource.
+//
+// getPaddings() returns paddings specified in the NinePatch.
+// getNinePatchChunk() returns the layout data specified in the NinePatch.
+//
+public class NinePatchTexture extends ResourceTexture {
+    @SuppressWarnings("unused")
+    private static final String TAG = "NinePatchTexture";
+    private NinePatchChunk mChunk;
+    private SmallCache<NinePatchInstance> mInstanceCache
+            = new SmallCache<NinePatchInstance>();
+
+    public NinePatchTexture(Context context, int resId) {
+        super(context, resId);
+    }
+
+    @Override
+    protected Bitmap onGetBitmap() {
+        if (mBitmap != null) return mBitmap;
+
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        Bitmap bitmap = BitmapFactory.decodeResource(
+                mContext.getResources(), mResId, options);
+        mBitmap = bitmap;
+        setSize(bitmap.getWidth(), bitmap.getHeight());
+        byte[] chunkData = bitmap.getNinePatchChunk();
+        mChunk = chunkData == null
+                ? null
+                : NinePatchChunk.deserialize(bitmap.getNinePatchChunk());
+        if (mChunk == null) {
+            throw new RuntimeException("invalid nine-patch image: " + mResId);
+        }
+        return bitmap;
+    }
+
+    public Rect getPaddings() {
+        // get the paddings from nine patch
+        if (mChunk == null) onGetBitmap();
+        return mChunk.mPaddings;
+    }
+
+    public NinePatchChunk getNinePatchChunk() {
+        if (mChunk == null) onGetBitmap();
+        return mChunk;
+    }
+
+    // This is a simple cache for a small number of things. Linear search
+    // is used because the cache is small. It also tries to remove less used
+    // item when the cache is full by moving the often-used items to the front.
+    private static class SmallCache<V> {
+        private static final int CACHE_SIZE = 16;
+        private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
+        private int[] mKey = new int[CACHE_SIZE];
+        private V[] mValue = (V[]) new Object[CACHE_SIZE];
+        private int mCount;  // number of items in this cache
+
+        // Puts a value into the cache. If the cache is full, also returns
+        // a less used item, otherwise returns null.
+        public V put(int key, V value) {
+            if (mCount == CACHE_SIZE) {
+                V old = mValue[CACHE_SIZE - 1];  // remove the last item
+                mKey[CACHE_SIZE - 1] = key;
+                mValue[CACHE_SIZE - 1] = value;
+                return old;
+            } else {
+                mKey[mCount] = key;
+                mValue[mCount] = value;
+                mCount++;
+                return null;
+            }
+        }
+
+        public V get(int key) {
+            for (int i = 0; i < mCount; i++) {
+                if (mKey[i] == key) {
+                    // Move the accessed item one position to the front, so it
+                    // will less likely to be removed when cache is full. Only
+                    // do this if the cache is starting to get full.
+                    if (mCount > CACHE_SIZE_START_MOVE && i > 0) {
+                        int tmpKey = mKey[i];
+                        mKey[i] = mKey[i - 1];
+                        mKey[i - 1] = tmpKey;
+
+                        V tmpValue = mValue[i];
+                        mValue[i] = mValue[i - 1];
+                        mValue[i - 1] = tmpValue;
+                    }
+                    return mValue[i];
+                }
+            }
+            return null;
+        }
+
+        public void clear() {
+            for (int i = 0; i < mCount; i++) {
+                mValue[i] = null;  // make sure it's can be garbage-collected.
+            }
+            mCount = 0;
+        }
+
+        public int size() {
+            return mCount;
+        }
+
+        public V valueAt(int i) {
+            return mValue[i];
+        }
+    }
+
+    private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
+        int key = w;
+        key = (key << 16) | h;
+        NinePatchInstance instance = mInstanceCache.get(key);
+
+        if (instance == null) {
+            instance = new NinePatchInstance(this, w, h);
+            NinePatchInstance removed = mInstanceCache.put(key, instance);
+            if (removed != null) {
+                removed.recycle(canvas);
+            }
+        }
+
+        return instance;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+        if (!isLoaded()) {
+            mInstanceCache.clear();
+        }
+
+        if (w != 0 && h != 0) {
+            findInstance(canvas, w, h).draw(canvas, this, x, y);
+        }
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        GLCanvas canvas = mCanvasRef;
+        if (canvas == null) return;
+        int n = mInstanceCache.size();
+        for (int i = 0; i < n; i++) {
+            NinePatchInstance instance = mInstanceCache.valueAt(i);
+            instance.recycle(canvas);
+        }
+        mInstanceCache.clear();
+    }
+}
+
+// This keeps data for a specialization of NinePatchTexture with the size
+// (width, height). We pre-compute the coordinates for efficiency.
+class NinePatchInstance {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "NinePatchInstance";
+
+    // We need 16 vertices for a normal nine-patch image (the 4x4 vertices)
+    private static final int VERTEX_BUFFER_SIZE = 16 * 2;
+
+    // We need 22 indices for a normal nine-patch image, plus 2 for each
+    // transparent region. Current there are at most 1 transparent region.
+    private static final int INDEX_BUFFER_SIZE = 22 + 2;
+
+    private FloatBuffer mXyBuffer;
+    private FloatBuffer mUvBuffer;
+    private ByteBuffer mIndexBuffer;
+
+    // Names for buffer names: xy, uv, index.
+    private int mXyBufferName = -1;
+    private int mUvBufferName;
+    private int mIndexBufferName;
+
+    private int mIdxCount;
+
+    public NinePatchInstance(NinePatchTexture tex, int width, int height) {
+        NinePatchChunk chunk = tex.getNinePatchChunk();
+
+        if (width <= 0 || height <= 0) {
+            throw new RuntimeException("invalid dimension");
+        }
+
+        // The code should be easily extended to handle the general cases by
+        // allocating more space for buffers. But let's just handle the only
+        // use case.
+        if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) {
+            throw new RuntimeException("unsupported nine patch");
+        }
+
+        float divX[] = new float[4];
+        float divY[] = new float[4];
+        float divU[] = new float[4];
+        float divV[] = new float[4];
+
+        int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
+        int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
+
+        prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor);
+    }
+
+    /**
+     * Stretches the texture according to the nine-patch rules. It will
+     * linearly distribute the strechy parts defined in the nine-patch chunk to
+     * the target area.
+     *
+     * <pre>
+     *                      source
+     *          /--------------^---------------\
+     *         u0    u1       u2  u3     u4   u5
+     * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
+     *          |    div0    div1 div2   div3  |
+     *          |     |       /   /      /    /
+     *          |     |      /   /     /    /
+     *          |     |     /   /    /    /
+     *          |fffff|ssss|fff|sss|ffff| ---> x
+     *         x0    x1   x2  x3  x4   x5
+     *          \----------v------------/
+     *                  target
+     *
+     * f: fixed segment
+     * s: stretchy segment
+     * </pre>
+     *
+     * @param div the stretch parts defined in nine-patch chunk
+     * @param source the length of the texture
+     * @param target the length on the drawing plan
+     * @param u output, the positions of these dividers in the texture
+     *        coordinate
+     * @param x output, the corresponding position of these dividers on the
+     *        drawing plan
+     * @return the number of these dividers.
+     */
+    private static int stretch(
+            float x[], float u[], int div[], int source, int target) {
+        int textureSize = Utils.nextPowerOf2(source);
+        float textureBound = (float) source / textureSize;
+
+        float stretch = 0;
+        for (int i = 0, n = div.length; i < n; i += 2) {
+            stretch += div[i + 1] - div[i];
+        }
+
+        float remaining = target - source + stretch;
+
+        float lastX = 0;
+        float lastU = 0;
+
+        x[0] = 0;
+        u[0] = 0;
+        for (int i = 0, n = div.length; i < n; i += 2) {
+            // Make the stretchy segment a little smaller to prevent sampling
+            // on neighboring fixed segments.
+            // fixed segment
+            x[i + 1] = lastX + (div[i] - lastU) + 0.5f;
+            u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound);
+
+            // stretchy segment
+            float partU = div[i + 1] - div[i];
+            float partX = remaining * partU / stretch;
+            remaining -= partX;
+            stretch -= partU;
+
+            lastX = x[i + 1] + partX;
+            lastU = div[i + 1];
+            x[i + 2] = lastX - 0.5f;
+            u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound);
+        }
+        // the last fixed segment
+        x[div.length + 1] = target;
+        u[div.length + 1] = textureBound;
+
+        // remove segments with length 0.
+        int last = 0;
+        for (int i = 1, n = div.length + 2; i < n; ++i) {
+            if ((x[i] - x[last]) < 1f) continue;
+            x[++last] = x[i];
+            u[last] = u[i];
+        }
+        return last + 1;
+    }
+
+    private void prepareVertexData(float x[], float y[], float u[], float v[],
+            int nx, int ny, int[] color) {
+        /*
+         * Given a 3x3 nine-patch image, the vertex order is defined as the
+         * following graph:
+         *
+         * (0) (1) (2) (3)
+         *  |  /|  /|  /|
+         *  | / | / | / |
+         * (4) (5) (6) (7)
+         *  | \ | \ | \ |
+         *  |  \|  \|  \|
+         * (8) (9) (A) (B)
+         *  |  /|  /|  /|
+         *  | / | / | / |
+         * (C) (D) (E) (F)
+         *
+         * And we draw the triangle strip in the following index order:
+         *
+         * index: 04152637B6A5948C9DAEBF
+         */
+        int pntCount = 0;
+        float xy[] = new float[VERTEX_BUFFER_SIZE];
+        float uv[] = new float[VERTEX_BUFFER_SIZE];
+        for (int j = 0; j < ny; ++j) {
+            for (int i = 0; i < nx; ++i) {
+                int xIndex = (pntCount++) << 1;
+                int yIndex = xIndex + 1;
+                xy[xIndex] = x[i];
+                xy[yIndex] = y[j];
+                uv[xIndex] = u[i];
+                uv[yIndex] = v[j];
+            }
+        }
+
+        int idxCount = 1;
+        boolean isForward = false;
+        byte index[] = new byte[INDEX_BUFFER_SIZE];
+        for (int row = 0; row < ny - 1; row++) {
+            --idxCount;
+            isForward = !isForward;
+
+            int start, end, inc;
+            if (isForward) {
+                start = 0;
+                end = nx;
+                inc = 1;
+            } else {
+                start = nx - 1;
+                end = -1;
+                inc = -1;
+            }
+
+            for (int col = start; col != end; col += inc) {
+                int k = row * nx + col;
+                if (col != start) {
+                    int colorIdx = row * (nx - 1) + col;
+                    if (isForward) colorIdx--;
+                    if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) {
+                        index[idxCount] = index[idxCount - 1];
+                        ++idxCount;
+                        index[idxCount++] = (byte) k;
+                    }
+                }
+
+                index[idxCount++] = (byte) k;
+                index[idxCount++] = (byte) (k + nx);
+            }
+        }
+
+        mIdxCount = idxCount;
+
+        int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE);
+        mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+        mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+        mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount);
+
+        mXyBuffer.put(xy, 0, pntCount * 2).position(0);
+        mUvBuffer.put(uv, 0, pntCount * 2).position(0);
+        mIndexBuffer.put(index, 0, idxCount).position(0);
+    }
+
+    private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+        return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+    }
+
+    private void prepareBuffers(GLCanvas canvas) {
+        mXyBufferName = canvas.uploadBuffer(mXyBuffer);
+        mUvBufferName = canvas.uploadBuffer(mUvBuffer);
+        mIndexBufferName = canvas.uploadBuffer(mIndexBuffer);
+
+        // These buffers are never used again.
+        mXyBuffer = null;
+        mUvBuffer = null;
+        mIndexBuffer = null;
+    }
+
+    public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) {
+        if (mXyBufferName == -1) {
+            prepareBuffers(canvas);
+        }
+        canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount);
+    }
+
+    public void recycle(GLCanvas canvas) {
+        if (mXyBuffer == null) {
+            canvas.deleteBuffer(mXyBufferName);
+            canvas.deleteBuffer(mUvBufferName);
+            canvas.deleteBuffer(mIndexBufferName);
+            mXyBufferName = -1;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/RawTexture.java b/src/com/android/gallery3d/glrenderer/RawTexture.java
new file mode 100644
index 0000000..93f0fdf
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/RawTexture.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.util.Log;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class RawTexture extends BasicTexture {
+    private static final String TAG = "RawTexture";
+
+    private final boolean mOpaque;
+    private boolean mIsFlipped;
+
+    public RawTexture(int width, int height, boolean opaque) {
+        mOpaque = opaque;
+        setSize(width, height);
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return mOpaque;
+    }
+
+    @Override
+    public boolean isFlippedVertically() {
+        return mIsFlipped;
+    }
+
+    public void setIsFlippedVertically(boolean isFlipped) {
+        mIsFlipped = isFlipped;
+    }
+
+    protected void prepare(GLCanvas canvas) {
+        GLId glId = canvas.getGLId();
+        mId = glId.generateTexture();
+        canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE);
+        canvas.setTextureParameters(this);
+        mState = STATE_LOADED;
+        setAssociatedCanvas(canvas);
+    }
+
+    @Override
+    protected boolean onBind(GLCanvas canvas) {
+        if (isLoaded()) return true;
+        Log.w(TAG, "lost the content due to context change");
+        return false;
+    }
+
+    @Override
+     public void yield() {
+         // we cannot free the texture because we have no backup.
+     }
+
+    @Override
+    protected int getTarget() {
+        return GL11.GL_TEXTURE_2D;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/ResourceTexture.java b/src/com/android/gallery3d/glrenderer/ResourceTexture.java
new file mode 100644
index 0000000..eb8e8a5
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/ResourceTexture.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import junit.framework.Assert;
+
+// ResourceTexture is a texture whose Bitmap is decoded from a resource.
+// By default ResourceTexture is not opaque.
+public class ResourceTexture extends UploadedTexture {
+
+    protected final Context mContext;
+    protected final int mResId;
+
+    public ResourceTexture(Context context, int resId) {
+        Assert.assertNotNull(context);
+        mContext = context;
+        mResId = resId;
+        setOpaque(false);
+    }
+
+    @Override
+    protected Bitmap onGetBitmap() {
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        return BitmapFactory.decodeResource(
+                mContext.getResources(), mResId, options);
+    }
+
+    @Override
+    protected void onFreeBitmap(Bitmap bitmap) {
+        if (!inFinalizer()) {
+            bitmap.recycle();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/StringTexture.java b/src/com/android/gallery3d/glrenderer/StringTexture.java
new file mode 100644
index 0000000..56ca297
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/StringTexture.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.util.FloatMath;
+
+// StringTexture is a texture shows the content of a specified String.
+//
+// To create a StringTexture, use the newInstance() method and specify
+// the String, the font size, and the color.
+public class StringTexture extends CanvasTexture {
+    private final String mText;
+    private final TextPaint mPaint;
+    private final FontMetricsInt mMetrics;
+
+    private StringTexture(String text, TextPaint paint,
+            FontMetricsInt metrics, int width, int height) {
+        super(width, height);
+        mText = text;
+        mPaint = paint;
+        mMetrics = metrics;
+    }
+
+    public static TextPaint getDefaultPaint(float textSize, int color) {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(textSize);
+        paint.setAntiAlias(true);
+        paint.setColor(color);
+        paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);
+        return paint;
+    }
+
+    public static StringTexture newInstance(
+            String text, float textSize, int color) {
+        return newInstance(text, getDefaultPaint(textSize, color));
+    }
+
+    public static StringTexture newInstance(
+            String text, float textSize, int color,
+            float lengthLimit, boolean isBold) {
+        TextPaint paint = getDefaultPaint(textSize, color);
+        if (isBold) {
+            paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+        }
+        if (lengthLimit > 0) {
+            text = TextUtils.ellipsize(
+                    text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
+        }
+        return newInstance(text, paint);
+    }
+
+    private static StringTexture newInstance(String text, TextPaint paint) {
+        FontMetricsInt metrics = paint.getFontMetricsInt();
+        int width = (int) FloatMath.ceil(paint.measureText(text));
+        int height = metrics.bottom - metrics.top;
+        // The texture size needs to be at least 1x1.
+        if (width <= 0) width = 1;
+        if (height <= 0) height = 1;
+        return new StringTexture(text, paint, metrics, width, height);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas, Bitmap backing) {
+        canvas.translate(0, -mMetrics.ascent);
+        canvas.drawText(mText, 0, 0, mPaint);
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/Texture.java b/src/com/android/gallery3d/glrenderer/Texture.java
new file mode 100644
index 0000000..3dcae4a
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/Texture.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+
+// Texture is a rectangular image which can be drawn on GLCanvas.
+// The isOpaque() function gives a hint about whether the texture is opaque,
+// so the drawing can be done faster.
+//
+// This is the current texture hierarchy:
+//
+// Texture
+// -- ColorTexture
+// -- FadeInTexture
+// -- BasicTexture
+//    -- UploadedTexture
+//       -- BitmapTexture
+//       -- Tile
+//       -- ResourceTexture
+//          -- NinePatchTexture
+//       -- CanvasTexture
+//          -- StringTexture
+//
+public interface Texture {
+    public int getWidth();
+    public int getHeight();
+    public void draw(GLCanvas canvas, int x, int y);
+    public void draw(GLCanvas canvas, int x, int y, int w, int h);
+    public boolean isOpaque();
+}
diff --git a/src/com/android/gallery3d/glrenderer/TextureUploader.java b/src/com/android/gallery3d/glrenderer/TextureUploader.java
new file mode 100644
index 0000000..f17ab84
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/TextureUploader.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.glrenderer;
+
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+
+public class TextureUploader implements OnGLIdleListener {
+    private static final int INIT_CAPACITY = 64;
+    private static final int QUOTA_PER_FRAME = 1;
+
+    private final ArrayDeque<UploadedTexture> mFgTextures =
+            new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
+    private final ArrayDeque<UploadedTexture> mBgTextures =
+            new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
+    private final GLRoot mGLRoot;
+    private volatile boolean mIsQueued = false;
+
+    public TextureUploader(GLRoot root) {
+        mGLRoot = root;
+    }
+
+    public synchronized void clear() {
+        while (!mFgTextures.isEmpty()) {
+            mFgTextures.pop().setIsUploading(false);
+        }
+        while (!mBgTextures.isEmpty()) {
+            mBgTextures.pop().setIsUploading(false);
+        }
+    }
+
+    // caller should hold synchronized on "this"
+    private void queueSelfIfNeed() {
+        if (mIsQueued) return;
+        mIsQueued = true;
+        mGLRoot.addOnGLIdleListener(this);
+    }
+
+    public synchronized void addBgTexture(UploadedTexture t) {
+        if (t.isContentValid()) return;
+        mBgTextures.addLast(t);
+        t.setIsUploading(true);
+        queueSelfIfNeed();
+    }
+
+    public synchronized void addFgTexture(UploadedTexture t) {
+        if (t.isContentValid()) return;
+        mFgTextures.addLast(t);
+        t.setIsUploading(true);
+        queueSelfIfNeed();
+    }
+
+    private int upload(GLCanvas canvas, ArrayDeque<UploadedTexture> deque,
+            int uploadQuota, boolean isBackground) {
+        while (uploadQuota > 0) {
+            UploadedTexture t;
+            synchronized (this) {
+                if (deque.isEmpty()) break;
+                t = deque.removeFirst();
+                t.setIsUploading(false);
+                if (t.isContentValid()) continue;
+
+                // this has to be protected by the synchronized block
+                // to prevent the inner bitmap get recycled
+                t.updateContent(canvas);
+            }
+
+            // It will took some more time for a texture to be drawn for
+            // the first time.
+            // Thus, when scrolling, if a new column appears on screen,
+            // it may cause a UI jank even these textures are uploaded.
+            if (isBackground) t.draw(canvas, 0, 0);
+            --uploadQuota;
+        }
+        return uploadQuota;
+    }
+
+    @Override
+    public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+        int uploadQuota = QUOTA_PER_FRAME;
+        uploadQuota = upload(canvas, mFgTextures, uploadQuota, false);
+        if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender();
+        upload(canvas, mBgTextures, uploadQuota, true);
+        synchronized (this) {
+            mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty();
+            return mIsQueued;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/TiledTexture.java b/src/com/android/gallery3d/glrenderer/TiledTexture.java
new file mode 100644
index 0000000..6ca1de0
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/TiledTexture.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+import android.os.SystemClock;
+
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+// This class is similar to BitmapTexture, except the bitmap is
+// split into tiles. By doing so, we may increase the time required to
+// upload the whole bitmap but we reduce the time of uploading each tile
+// so it make the animation more smooth and prevents jank.
+public class TiledTexture implements Texture {
+    private static final int CONTENT_SIZE = 254;
+    private static final int BORDER_SIZE = 1;
+    private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;
+    private static final int INIT_CAPACITY = 8;
+
+    // We are targeting at 60fps, so we have 16ms for each frame.
+    // In this 16ms, we use about 4~8 ms to upload tiles.
+    private static final long UPLOAD_TILE_LIMIT = 4; // ms
+
+    private static Tile sFreeTileHead = null;
+    private static final Object sFreeTileLock = new Object();
+
+    private static Bitmap sUploadBitmap;
+    private static Canvas sCanvas;
+    private static Paint sBitmapPaint;
+    private static Paint sPaint;
+
+    private int mUploadIndex = 0;
+
+    private final Tile[] mTiles;  // Can be modified in different threads.
+                                  // Should be protected by "synchronized."
+    private final int mWidth;
+    private final int mHeight;
+    private final RectF mSrcRect = new RectF();
+    private final RectF mDestRect = new RectF();
+
+    public static class Uploader implements OnGLIdleListener {
+        private final ArrayDeque<TiledTexture> mTextures =
+                new ArrayDeque<TiledTexture>(INIT_CAPACITY);
+
+        private final GLRoot mGlRoot;
+        private boolean mIsQueued = false;
+
+        public Uploader(GLRoot glRoot) {
+            mGlRoot = glRoot;
+        }
+
+        public synchronized void clear() {
+            mTextures.clear();
+        }
+
+        public synchronized void addTexture(TiledTexture t) {
+            if (t.isReady()) return;
+            mTextures.addLast(t);
+
+            if (mIsQueued) return;
+            mIsQueued = true;
+            mGlRoot.addOnGLIdleListener(this);
+        }
+
+        @Override
+        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+            ArrayDeque<TiledTexture> deque = mTextures;
+            synchronized (this) {
+                long now = SystemClock.uptimeMillis();
+                long dueTime = now + UPLOAD_TILE_LIMIT;
+                while (now < dueTime && !deque.isEmpty()) {
+                    TiledTexture t = deque.peekFirst();
+                    if (t.uploadNextTile(canvas)) {
+                        deque.removeFirst();
+                        mGlRoot.requestRender();
+                    }
+                    now = SystemClock.uptimeMillis();
+                }
+                mIsQueued = !mTextures.isEmpty();
+
+                // return true to keep this listener in the queue
+                return mIsQueued;
+            }
+        }
+    }
+
+    private static class Tile extends UploadedTexture {
+        public int offsetX;
+        public int offsetY;
+        public Bitmap bitmap;
+        public Tile nextFreeTile;
+        public int contentWidth;
+        public int contentHeight;
+
+        @Override
+        public void setSize(int width, int height) {
+            contentWidth = width;
+            contentHeight = height;
+            mWidth = width + 2 * BORDER_SIZE;
+            mHeight = height + 2 * BORDER_SIZE;
+            mTextureWidth = TILE_SIZE;
+            mTextureHeight = TILE_SIZE;
+        }
+
+        @Override
+        protected Bitmap onGetBitmap() {
+            int x = BORDER_SIZE - offsetX;
+            int y = BORDER_SIZE - offsetY;
+            int r = bitmap.getWidth() + x;
+            int b = bitmap.getHeight() + y;
+            sCanvas.drawBitmap(bitmap, x, y, sBitmapPaint);
+            bitmap = null;
+
+            // draw borders if need
+            if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);
+            if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);
+            if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);
+            if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);
+
+            return sUploadBitmap;
+        }
+
+        @Override
+        protected void onFreeBitmap(Bitmap bitmap) {
+            // do nothing
+        }
+    }
+
+    private static void freeTile(Tile tile) {
+        tile.invalidateContent();
+        tile.bitmap = null;
+        synchronized (sFreeTileLock) {
+            tile.nextFreeTile = sFreeTileHead;
+            sFreeTileHead = tile;
+        }
+    }
+
+    private static Tile obtainTile() {
+        synchronized (sFreeTileLock) {
+            Tile result = sFreeTileHead;
+            if (result == null) return new Tile();
+            sFreeTileHead = result.nextFreeTile;
+            result.nextFreeTile = null;
+            return result;
+        }
+    }
+
+    private boolean uploadNextTile(GLCanvas canvas) {
+        if (mUploadIndex == mTiles.length) return true;
+
+        synchronized (mTiles) {
+            Tile next = mTiles[mUploadIndex++];
+
+            // Make sure tile has not already been recycled by the time
+            // this is called (race condition in onGLIdle)
+            if (next.bitmap != null) {
+                boolean hasBeenLoad = next.isLoaded();
+                next.updateContent(canvas);
+
+                // It will take some time for a texture to be drawn for the first
+                // time. When scrolling, we need to draw several tiles on the screen
+                // at the same time. It may cause a UI jank even these textures has
+                // been uploaded.
+                if (!hasBeenLoad) next.draw(canvas, 0, 0);
+            }
+        }
+        return mUploadIndex == mTiles.length;
+    }
+
+    public TiledTexture(Bitmap bitmap) {
+        mWidth = bitmap.getWidth();
+        mHeight = bitmap.getHeight();
+        ArrayList<Tile> list = new ArrayList<Tile>();
+
+        for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) {
+            for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) {
+                Tile tile = obtainTile();
+                tile.offsetX = x;
+                tile.offsetY = y;
+                tile.bitmap = bitmap;
+                tile.setSize(
+                        Math.min(CONTENT_SIZE, mWidth - x),
+                        Math.min(CONTENT_SIZE, mHeight - y));
+                list.add(tile);
+            }
+        }
+        mTiles = list.toArray(new Tile[list.size()]);
+    }
+
+    public boolean isReady() {
+        return mUploadIndex == mTiles.length;
+    }
+
+    // Can be called in UI thread.
+    public void recycle() {
+        synchronized (mTiles) {
+            for (int i = 0, n = mTiles.length; i < n; ++i) {
+                freeTile(mTiles[i]);
+            }
+        }
+    }
+
+    public static void freeResources() {
+        sUploadBitmap = null;
+        sCanvas = null;
+        sBitmapPaint = null;
+        sPaint = null;
+    }
+
+    public static void prepareResources() {
+        sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888);
+        sCanvas = new Canvas(sUploadBitmap);
+        sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+        sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
+        sPaint = new Paint();
+        sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
+        sPaint.setColor(Color.TRANSPARENT);
+    }
+
+    // We want to draw the "source" on the "target".
+    // This method is to find the "output" rectangle which is
+    // the corresponding area of the "src".
+    //                                   (x,y)  target
+    // (x0,y0)  source                     +---------------+
+    //    +----------+                     |               |
+    //    | src      |                     | output        |
+    //    | +--+     |    linear map       | +----+        |
+    //    | +--+     |    ---------->      | |    |        |
+    //    |          | by (scaleX, scaleY) | +----+        |
+    //    +----------+                     |               |
+    //      Texture                        +---------------+
+    //                                          Canvas
+    private static void mapRect(RectF output,
+            RectF src, float x0, float y0, float x, float y, float scaleX,
+            float scaleY) {
+        output.set(x + (src.left - x0) * scaleX,
+                y + (src.top - y0) * scaleY,
+                x + (src.right - x0) * scaleX,
+                y + (src.bottom - y0) * scaleY);
+    }
+
+    // Draws a mixed color of this texture and a specified color onto the
+    // a rectangle. The used color is: from * (1 - ratio) + to * ratio.
+    public void drawMixed(GLCanvas canvas, int color, float ratio,
+            int x, int y, int width, int height) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float scaleX = (float) width / mWidth;
+        float scaleY = (float) height / mHeight;
+        synchronized (mTiles) {
+            for (int i = 0, n = mTiles.length; i < n; ++i) {
+                Tile t = mTiles[i];
+                src.set(0, 0, t.contentWidth, t.contentHeight);
+                src.offset(t.offsetX, t.offsetY);
+                mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+                canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);
+            }
+        }
+    }
+
+    // Draws the texture on to the specified rectangle.
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float scaleX = (float) width / mWidth;
+        float scaleY = (float) height / mHeight;
+        synchronized (mTiles) {
+            for (int i = 0, n = mTiles.length; i < n; ++i) {
+                Tile t = mTiles[i];
+                src.set(0, 0, t.contentWidth, t.contentHeight);
+                src.offset(t.offsetX, t.offsetY);
+                mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+                canvas.drawTexture(t, mSrcRect, mDestRect);
+            }
+        }
+    }
+
+    // Draws a sub region of this texture on to the specified rectangle.
+    public void draw(GLCanvas canvas, RectF source, RectF target) {
+        RectF src = mSrcRect;
+        RectF dest = mDestRect;
+        float x0 = source.left;
+        float y0 = source.top;
+        float x = target.left;
+        float y = target.top;
+        float scaleX = target.width() / source.width();
+        float scaleY = target.height() / source.height();
+
+        synchronized (mTiles) {
+            for (int i = 0, n = mTiles.length; i < n; ++i) {
+                Tile t = mTiles[i];
+                src.set(0, 0, t.contentWidth, t.contentHeight);
+                src.offset(t.offsetX, t.offsetY);
+                if (!src.intersect(source)) continue;
+                mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);
+                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+                canvas.drawTexture(t, src, dest);
+            }
+        }
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y) {
+        draw(canvas, x, y, mWidth, mHeight);
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return false;
+    }
+}
diff --git a/src/com/android/gallery3d/glrenderer/UploadedTexture.java b/src/com/android/gallery3d/glrenderer/UploadedTexture.java
new file mode 100644
index 0000000..f41a979
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/UploadedTexture.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.opengl.GLUtils;
+
+import junit.framework.Assert;
+
+import java.util.HashMap;
+
+import javax.microedition.khronos.opengles.GL11;
+
+// UploadedTextures use a Bitmap for the content of the texture.
+//
+// Subclasses should implement onGetBitmap() to provide the Bitmap and
+// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
+// is not needed anymore.
+//
+// isContentValid() is meaningful only when the isLoaded() returns true.
+// It means whether the content needs to be updated.
+//
+// The user of this class should call recycle() when the texture is not
+// needed anymore.
+//
+// By default an UploadedTexture is opaque (so it can be drawn faster without
+// blending). The user or subclass can override it using setOpaque().
+public abstract class UploadedTexture extends BasicTexture {
+
+    // To prevent keeping allocation the borders, we store those used borders here.
+    // Since the length will be power of two, it won't use too much memory.
+    private static HashMap<BorderKey, Bitmap> sBorderLines =
+            new HashMap<BorderKey, Bitmap>();
+    private static BorderKey sBorderKey = new BorderKey();
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "Texture";
+    private boolean mContentValid = true;
+
+    // indicate this textures is being uploaded in background
+    private boolean mIsUploading = false;
+    private boolean mOpaque = true;
+    private boolean mThrottled = false;
+    private static int sUploadedCount;
+    private static final int UPLOAD_LIMIT = 100;
+
+    protected Bitmap mBitmap;
+    private int mBorder;
+
+    protected UploadedTexture() {
+        this(false);
+    }
+
+    protected UploadedTexture(boolean hasBorder) {
+        super(null, 0, STATE_UNLOADED);
+        if (hasBorder) {
+            setBorder(true);
+            mBorder = 1;
+        }
+    }
+
+    protected void setIsUploading(boolean uploading) {
+        mIsUploading = uploading;
+    }
+
+    public boolean isUploading() {
+        return mIsUploading;
+    }
+
+    private static class BorderKey implements Cloneable {
+        public boolean vertical;
+        public Config config;
+        public int length;
+
+        @Override
+        public int hashCode() {
+            int x = config.hashCode() ^ length;
+            return vertical ? x : -x;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (!(object instanceof BorderKey)) return false;
+            BorderKey o = (BorderKey) object;
+            return vertical == o.vertical
+                    && config == o.config && length == o.length;
+        }
+
+        @Override
+        public BorderKey clone() {
+            try {
+                return (BorderKey) super.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    protected void setThrottled(boolean throttled) {
+        mThrottled = throttled;
+    }
+
+    private static Bitmap getBorderLine(
+            boolean vertical, Config config, int length) {
+        BorderKey key = sBorderKey;
+        key.vertical = vertical;
+        key.config = config;
+        key.length = length;
+        Bitmap bitmap = sBorderLines.get(key);
+        if (bitmap == null) {
+            bitmap = vertical
+                    ? Bitmap.createBitmap(1, length, config)
+                    : Bitmap.createBitmap(length, 1, config);
+            sBorderLines.put(key.clone(), bitmap);
+        }
+        return bitmap;
+    }
+
+    private Bitmap getBitmap() {
+        if (mBitmap == null) {
+            mBitmap = onGetBitmap();
+            int w = mBitmap.getWidth() + mBorder * 2;
+            int h = mBitmap.getHeight() + mBorder * 2;
+            if (mWidth == UNSPECIFIED) {
+                setSize(w, h);
+            }
+        }
+        return mBitmap;
+    }
+
+    private void freeBitmap() {
+        Assert.assertTrue(mBitmap != null);
+        onFreeBitmap(mBitmap);
+        mBitmap = null;
+    }
+
+    @Override
+    public int getWidth() {
+        if (mWidth == UNSPECIFIED) getBitmap();
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        if (mWidth == UNSPECIFIED) getBitmap();
+        return mHeight;
+    }
+
+    protected abstract Bitmap onGetBitmap();
+
+    protected abstract void onFreeBitmap(Bitmap bitmap);
+
+    protected void invalidateContent() {
+        if (mBitmap != null) freeBitmap();
+        mContentValid = false;
+        mWidth = UNSPECIFIED;
+        mHeight = UNSPECIFIED;
+    }
+
+    /**
+     * Whether the content on GPU is valid.
+     */
+    public boolean isContentValid() {
+        return isLoaded() && mContentValid;
+    }
+
+    /**
+     * Updates the content on GPU's memory.
+     * @param canvas
+     */
+    public void updateContent(GLCanvas canvas) {
+        if (!isLoaded()) {
+            if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
+                return;
+            }
+            uploadToCanvas(canvas);
+        } else if (!mContentValid) {
+            Bitmap bitmap = getBitmap();
+            int format = GLUtils.getInternalFormat(bitmap);
+            int type = GLUtils.getType(bitmap);
+            canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
+            freeBitmap();
+            mContentValid = true;
+        }
+    }
+
+    public static void resetUploadLimit() {
+        sUploadedCount = 0;
+    }
+
+    public static boolean uploadLimitReached() {
+        return sUploadedCount > UPLOAD_LIMIT;
+    }
+
+    private void uploadToCanvas(GLCanvas canvas) {
+
+        Bitmap bitmap = getBitmap();
+        if (bitmap != null) {
+            try {
+                int bWidth = bitmap.getWidth();
+                int bHeight = bitmap.getHeight();
+                int width = bWidth + mBorder * 2;
+                int height = bHeight + mBorder * 2;
+                int texWidth = getTextureWidth();
+                int texHeight = getTextureHeight();
+
+                Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight);
+
+                // Upload the bitmap to a new texture.
+                mId = canvas.getGLId().generateTexture();
+                canvas.setTextureParameters(this);
+
+                if (bWidth == texWidth && bHeight == texHeight) {
+                    canvas.initializeTexture(this, bitmap);
+                } else {
+                    int format = GLUtils.getInternalFormat(bitmap);
+                    int type = GLUtils.getType(bitmap);
+                    Config config = bitmap.getConfig();
+
+                    canvas.initializeTextureSize(this, format, type);
+                    canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
+
+                    if (mBorder > 0) {
+                        // Left border
+                        Bitmap line = getBorderLine(true, config, texHeight);
+                        canvas.texSubImage2D(this, 0, 0, line, format, type);
+
+                        // Top border
+                        line = getBorderLine(false, config, texWidth);
+                        canvas.texSubImage2D(this, 0, 0, line, format, type);
+                    }
+
+                    // Right border
+                    if (mBorder + bWidth < texWidth) {
+                        Bitmap line = getBorderLine(true, config, texHeight);
+                        canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type);
+                    }
+
+                    // Bottom border
+                    if (mBorder + bHeight < texHeight) {
+                        Bitmap line = getBorderLine(false, config, texWidth);
+                        canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type);
+                    }
+                }
+            } finally {
+                freeBitmap();
+            }
+            // Update texture state.
+            setAssociatedCanvas(canvas);
+            mState = STATE_LOADED;
+            mContentValid = true;
+        } else {
+            mState = STATE_ERROR;
+            throw new RuntimeException("Texture load fail, no bitmap");
+        }
+    }
+
+    @Override
+    protected boolean onBind(GLCanvas canvas) {
+        updateContent(canvas);
+        return isContentValid();
+    }
+
+    @Override
+    protected int getTarget() {
+        return GL11.GL_TEXTURE_2D;
+    }
+
+    public void setOpaque(boolean isOpaque) {
+        mOpaque = isOpaque;
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return mOpaque;
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        if (mBitmap != null) freeBitmap();
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/ImportTask.java b/src/com/android/gallery3d/ingest/ImportTask.java
new file mode 100644
index 0000000..7d2d641
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ImportTask.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest;
+
+import android.content.Context;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Environment;
+import android.os.PowerManager;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+public class ImportTask implements Runnable {
+
+    public interface Listener {
+        void onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful);
+
+        void onImportFinish(Collection<MtpObjectInfo> objectsNotImported, int visitedCount);
+    }
+
+    static private final String WAKELOCK_LABEL = "MTP Import Task";
+
+    private Listener mListener;
+    private String mDestAlbumName;
+    private Collection<MtpObjectInfo> mObjectsToImport;
+    private MtpDevice mDevice;
+    private PowerManager.WakeLock mWakeLock;
+
+    public ImportTask(MtpDevice device, Collection<MtpObjectInfo> objectsToImport,
+            String destAlbumName, Context context) {
+        mDestAlbumName = destAlbumName;
+        mObjectsToImport = objectsToImport;
+        mDevice = device;
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, WAKELOCK_LABEL);
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void run() {
+        mWakeLock.acquire();
+        try {
+            List<MtpObjectInfo> objectsNotImported = new LinkedList<MtpObjectInfo>();
+            int visited = 0;
+            int total = mObjectsToImport.size();
+            mListener.onImportProgress(visited, total, null);
+            File dest = new File(Environment.getExternalStorageDirectory(), mDestAlbumName);
+            dest.mkdirs();
+            for (MtpObjectInfo object : mObjectsToImport) {
+                visited++;
+                String importedPath = null;
+                if (GalleryUtils.hasSpaceForSize(object.getCompressedSize())) {
+                    importedPath = new File(dest, object.getName()).getAbsolutePath();
+                    if (!mDevice.importFile(object.getObjectHandle(), importedPath)) {
+                        importedPath = null;
+                    }
+                }
+                if (importedPath == null) {
+                    objectsNotImported.add(object);
+                }
+                if (mListener != null) {
+                    mListener.onImportProgress(visited, total, importedPath);
+                }
+            }
+            if (mListener != null) {
+                mListener.onImportFinish(objectsNotImported, visited);
+            }
+        } finally {
+            mListener = null;
+            mWakeLock.release();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/IngestActivity.java b/src/com/android/gallery3d/ingest/IngestActivity.java
new file mode 100644
index 0000000..687e9fd
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestActivity.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Configuration;
+import android.database.DataSetObserver;
+import android.mtp.MtpObjectInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.support.v4.view.ViewPager;
+import android.util.SparseBooleanArray;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.CheckBroker;
+import com.android.gallery3d.ingest.adapter.MtpAdapter;
+import com.android.gallery3d.ingest.adapter.MtpPagerAdapter;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.IngestGridView;
+import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener;
+
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+
+public class IngestActivity extends Activity implements
+        MtpDeviceIndex.ProgressListener, ImportTask.Listener {
+
+    private IngestService mHelperService;
+    private boolean mActive = false;
+    private IngestGridView mGridView;
+    private MtpAdapter mAdapter;
+    private Handler mHandler;
+    private ProgressDialog mProgressDialog;
+    private ActionMode mActiveActionMode;
+
+    private View mWarningView;
+    private TextView mWarningText;
+    private int mLastCheckedPosition = 0;
+
+    private ViewPager mFullscreenPager;
+    private MtpPagerAdapter mPagerAdapter;
+    private boolean mFullscreenPagerVisible = false;
+
+    private MenuItem mMenuSwitcherItem;
+    private MenuItem mActionMenuSwitcherItem;
+
+    // The MTP framework components don't give us fine-grained file copy
+    // progress updates, so for large photos and videos, we will be stuck
+    // with a dialog not updating for a long time. To give the user feedback,
+    // we switch to the animated indeterminate progress bar after the timeout
+    // specified by INDETERMINATE_SWITCH_TIMEOUT_MS. On the next update from
+    // the framework, we switch back to the normal progress bar.
+    private static final int INDETERMINATE_SWITCH_TIMEOUT_MS = 3000;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        doBindHelperService();
+
+        setContentView(R.layout.ingest_activity_item_list);
+        mGridView = (IngestGridView) findViewById(R.id.ingest_gridview);
+        mAdapter = new MtpAdapter(this);
+        mAdapter.registerDataSetObserver(mMasterObserver);
+        mGridView.setAdapter(mAdapter);
+        mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener);
+        mGridView.setOnItemClickListener(mOnItemClickListener);
+        mGridView.setOnClearChoicesListener(mPositionMappingCheckBroker);
+
+        mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager);
+
+        mHandler = new ItemListHandler(this);
+
+        MtpBitmapFetch.configureForContext(this);
+    }
+
+    private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
+        @Override
+        public void onItemClick(AdapterView<?> adapterView, View itemView, int position, long arg3) {
+            mLastCheckedPosition = position;
+            mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position));
+        }
+    };
+
+    private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() {
+        private boolean mIgnoreItemCheckedStateChanges = false;
+
+        private void updateSelectedTitle(ActionMode mode) {
+            int count = mGridView.getCheckedItemCount();
+            mode.setTitle(getResources().getQuantityString(
+                    R.plurals.number_of_items_selected, count, count));
+        }
+
+        @Override
+        public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+                boolean checked) {
+            if (mIgnoreItemCheckedStateChanges) return;
+            if (mAdapter.itemAtPositionIsBucket(position)) {
+                SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions();
+                mIgnoreItemCheckedStateChanges = true;
+                mGridView.setItemChecked(position, false);
+
+                // Takes advantage of the fact that SectionIndexer imposes the
+                // need to clamp to the valid range
+                int nextSectionStart = mAdapter.getPositionForSection(
+                        mAdapter.getSectionForPosition(position) + 1);
+                if (nextSectionStart == position)
+                    nextSectionStart = mAdapter.getCount();
+
+                boolean rangeValue = false; // Value we want to set all of the bucket items to
+
+                // Determine if all the items in the bucket are currently checked, so that we
+                // can uncheck them, otherwise we will check all items in the bucket.
+                for (int i = position + 1; i < nextSectionStart; i++) {
+                    if (checkedItems.get(i) == false) {
+                        rangeValue = true;
+                        break;
+                    }
+                }
+
+                // Set all items in the bucket to the desired state
+                for (int i = position + 1; i < nextSectionStart; i++) {
+                    if (checkedItems.get(i) != rangeValue)
+                        mGridView.setItemChecked(i, rangeValue);
+                }
+
+                mPositionMappingCheckBroker.onBulkCheckedChange();
+                mIgnoreItemCheckedStateChanges = false;
+            } else {
+                mPositionMappingCheckBroker.onCheckedChange(position, checked);
+            }
+            mLastCheckedPosition = position;
+            updateSelectedTitle(mode);
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            return onOptionsItemSelected(item);
+        }
+
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            MenuInflater inflater = mode.getMenuInflater();
+            inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
+            updateSelectedTitle(mode);
+            mActiveActionMode = mode;
+            mActionMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
+            setSwitcherMenuState(mActionMenuSwitcherItem, mFullscreenPagerVisible);
+            return true;
+        }
+
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+            mActiveActionMode = null;
+            mActionMenuSwitcherItem = null;
+            mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE);
+        }
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            updateSelectedTitle(mode);
+            return false;
+        }
+    };
+
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.import_items:
+                if (mActiveActionMode != null) {
+                    mHelperService.importSelectedItems(
+                            mGridView.getCheckedItemPositions(),
+                            mAdapter);
+                    mActiveActionMode.finish();
+                }
+                return true;
+            case R.id.ingest_switch_view:
+                setFullscreenPagerVisibility(!mFullscreenPagerVisible);
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
+        mMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
+        menu.findItem(R.id.import_items).setVisible(false);
+        setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible);
+        return true;
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        doUnbindHelperService();
+    }
+
+    @Override
+    protected void onResume() {
+        DateTileView.refreshLocale();
+        mActive = true;
+        if (mHelperService != null) mHelperService.setClientActivity(this);
+        updateWarningView();
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        if (mHelperService != null) mHelperService.setClientActivity(null);
+        mActive = false;
+        cleanupProgressDialog();
+        super.onPause();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        MtpBitmapFetch.configureForContext(this);
+    }
+
+    private void showWarningView(int textResId) {
+        if (mWarningView == null) {
+            mWarningView = findViewById(R.id.ingest_warning_view);
+            mWarningText =
+                    (TextView)mWarningView.findViewById(R.id.ingest_warning_view_text);
+        }
+        mWarningText.setText(textResId);
+        mWarningView.setVisibility(View.VISIBLE);
+        setFullscreenPagerVisibility(false);
+        mGridView.setVisibility(View.GONE);
+    }
+
+    private void hideWarningView() {
+        if (mWarningView != null) {
+            mWarningView.setVisibility(View.GONE);
+            setFullscreenPagerVisibility(false);
+        }
+    }
+
+    private PositionMappingCheckBroker mPositionMappingCheckBroker = new PositionMappingCheckBroker();
+
+    private class PositionMappingCheckBroker extends CheckBroker
+        implements OnClearChoicesListener {
+        private int mLastMappingPager = -1;
+        private int mLastMappingGrid = -1;
+
+        private int mapPagerToGridPosition(int position) {
+            if (position != mLastMappingPager) {
+               mLastMappingPager = position;
+               mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position);
+            }
+            return mLastMappingGrid;
+        }
+
+        private int mapGridToPagerPosition(int position) {
+            if (position != mLastMappingGrid) {
+                mLastMappingGrid = position;
+                mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position);
+            }
+            return mLastMappingPager;
+        }
+
+        @Override
+        public void setItemChecked(int position, boolean checked) {
+            mGridView.setItemChecked(mapPagerToGridPosition(position), checked);
+        }
+
+        @Override
+        public void onCheckedChange(int position, boolean checked) {
+            if (mPagerAdapter != null) {
+                super.onCheckedChange(mapGridToPagerPosition(position), checked);
+            }
+        }
+
+        @Override
+        public boolean isItemChecked(int position) {
+            return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position));
+        }
+
+        @Override
+        public void onClearChoices() {
+            onBulkCheckedChange();
+        }
+    };
+
+    private DataSetObserver mMasterObserver = new DataSetObserver() {
+        @Override
+        public void onChanged() {
+            if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged();
+        }
+    };
+
+    private int pickFullscreenStartingPosition() {
+        int firstVisiblePosition = mGridView.getFirstVisiblePosition();
+        if (mLastCheckedPosition <= firstVisiblePosition
+                || mLastCheckedPosition > mGridView.getLastVisiblePosition()) {
+            return firstVisiblePosition;
+        } else {
+            return mLastCheckedPosition;
+        }
+    }
+
+    private void setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) {
+        if (menuItem == null) return;
+        if (!inFullscreenMode) {
+            menuItem.setIcon(android.R.drawable.ic_menu_zoom);
+            menuItem.setTitle(R.string.switch_photo_fullscreen);
+        } else {
+            menuItem.setIcon(android.R.drawable.ic_dialog_dialer);
+            menuItem.setTitle(R.string.switch_photo_grid);
+        }
+    }
+
+    private void setFullscreenPagerVisibility(boolean visible) {
+        mFullscreenPagerVisible = visible;
+        if (visible) {
+            if (mPagerAdapter == null) {
+                mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker);
+                mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex());
+            }
+            mFullscreenPager.setAdapter(mPagerAdapter);
+            mFullscreenPager.setCurrentItem(mPagerAdapter.translatePositionWithLabels(
+                    pickFullscreenStartingPosition()), false);
+        } else if (mPagerAdapter != null) {
+            mGridView.setSelection(mAdapter.translatePositionWithoutLabels(
+                    mFullscreenPager.getCurrentItem()));
+            mFullscreenPager.setAdapter(null);
+        }
+        mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE);
+        mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        if (mActionMenuSwitcherItem != null) {
+            setSwitcherMenuState(mActionMenuSwitcherItem, visible);
+        }
+        setSwitcherMenuState(mMenuSwitcherItem, visible);
+    }
+
+    private void updateWarningView() {
+        if (!mAdapter.deviceConnected()) {
+            showWarningView(R.string.ingest_no_device);
+        } else if (mAdapter.indexReady() && mAdapter.getCount() == 0) {
+            showWarningView(R.string.ingest_empty_device);
+        } else {
+            hideWarningView();
+        }
+    }
+
+    private void UiThreadNotifyIndexChanged() {
+        mAdapter.notifyDataSetChanged();
+        if (mActiveActionMode != null) {
+            mActiveActionMode.finish();
+            mActiveActionMode = null;
+        }
+        updateWarningView();
+    }
+
+    protected void notifyIndexChanged() {
+        mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+    }
+
+    private static class ProgressState {
+        String message;
+        String title;
+        int current;
+        int max;
+
+        public void reset() {
+            title = null;
+            message = null;
+            current = 0;
+            max = 0;
+        }
+    }
+
+    private ProgressState mProgressState = new ProgressState();
+
+    @Override
+    public void onObjectIndexed(MtpObjectInfo object, int numVisited) {
+        // Not guaranteed to be called on the UI thread
+        mProgressState.reset();
+        mProgressState.max = 0;
+        mProgressState.message = getResources().getQuantityString(
+                R.plurals.ingest_number_of_items_scanned, numVisited, numVisited);
+        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+    }
+
+    @Override
+    public void onSorting() {
+        // Not guaranteed to be called on the UI thread
+        mProgressState.reset();
+        mProgressState.max = 0;
+        mProgressState.message = getResources().getString(R.string.ingest_sorting);
+        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+    }
+
+    @Override
+    public void onIndexFinish() {
+        // Not guaranteed to be called on the UI thread
+        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
+        mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+    }
+
+    @Override
+    public void onImportProgress(final int visitedCount, final int totalCount,
+            String pathIfSuccessful) {
+        // Not guaranteed to be called on the UI thread
+        mProgressState.reset();
+        mProgressState.max = totalCount;
+        mProgressState.current = visitedCount;
+        mProgressState.title = getResources().getString(R.string.ingest_importing);
+        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+        mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
+        mHandler.sendEmptyMessageDelayed(ItemListHandler.MSG_PROGRESS_INDETERMINATE,
+                INDETERMINATE_SWITCH_TIMEOUT_MS);
+    }
+
+    @Override
+    public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported,
+            int numVisited) {
+        // Not guaranteed to be called on the UI thread
+        mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
+        mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
+        // TODO: maybe show an extra dialog listing the ones that failed
+        // importing, if any?
+    }
+
+    private ProgressDialog getProgressDialog() {
+        if (mProgressDialog == null || !mProgressDialog.isShowing()) {
+            mProgressDialog = new ProgressDialog(this);
+            mProgressDialog.setCancelable(false);
+        }
+        return mProgressDialog;
+    }
+
+    private void updateProgressDialog() {
+        ProgressDialog dialog = getProgressDialog();
+        boolean indeterminate = (mProgressState.max == 0);
+        dialog.setIndeterminate(indeterminate);
+        dialog.setProgressStyle(indeterminate ? ProgressDialog.STYLE_SPINNER
+                : ProgressDialog.STYLE_HORIZONTAL);
+        if (mProgressState.title != null) {
+            dialog.setTitle(mProgressState.title);
+        }
+        if (mProgressState.message != null) {
+            dialog.setMessage(mProgressState.message);
+        }
+        if (!indeterminate) {
+            dialog.setProgress(mProgressState.current);
+            dialog.setMax(mProgressState.max);
+        }
+        if (!dialog.isShowing()) {
+            dialog.show();
+        }
+    }
+
+    private void makeProgressDialogIndeterminate() {
+        ProgressDialog dialog = getProgressDialog();
+        dialog.setIndeterminate(true);
+    }
+
+    private void cleanupProgressDialog() {
+        if (mProgressDialog != null) {
+            mProgressDialog.hide();
+            mProgressDialog = null;
+        }
+    }
+
+    // This is static and uses a WeakReference in order to avoid leaking the Activity
+    private static class ItemListHandler extends Handler {
+        public static final int MSG_PROGRESS_UPDATE = 0;
+        public static final int MSG_PROGRESS_HIDE = 1;
+        public static final int MSG_NOTIFY_CHANGED = 2;
+        public static final int MSG_BULK_CHECKED_CHANGE = 3;
+        public static final int MSG_PROGRESS_INDETERMINATE = 4;
+
+        WeakReference<IngestActivity> mParentReference;
+
+        public ItemListHandler(IngestActivity parent) {
+            super();
+            mParentReference = new WeakReference<IngestActivity>(parent);
+        }
+
+        public void handleMessage(Message message) {
+            IngestActivity parent = mParentReference.get();
+            if (parent == null || !parent.mActive)
+                return;
+            switch (message.what) {
+                case MSG_PROGRESS_HIDE:
+                    parent.cleanupProgressDialog();
+                    break;
+                case MSG_PROGRESS_UPDATE:
+                    parent.updateProgressDialog();
+                    break;
+                case MSG_NOTIFY_CHANGED:
+                    parent.UiThreadNotifyIndexChanged();
+                    break;
+                case MSG_BULK_CHECKED_CHANGE:
+                    parent.mPositionMappingCheckBroker.onBulkCheckedChange();
+                    break;
+                case MSG_PROGRESS_INDETERMINATE:
+                    parent.makeProgressDialogIndeterminate();
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private ServiceConnection mHelperServiceConnection = new ServiceConnection() {
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            mHelperService = ((IngestService.LocalBinder) service).getService();
+            mHelperService.setClientActivity(IngestActivity.this);
+            MtpDeviceIndex index = mHelperService.getIndex();
+            mAdapter.setMtpDeviceIndex(index);
+            if (mPagerAdapter != null) mPagerAdapter.setMtpDeviceIndex(index);
+        }
+
+        public void onServiceDisconnected(ComponentName className) {
+            mHelperService = null;
+        }
+    };
+
+    private void doBindHelperService() {
+        bindService(new Intent(getApplicationContext(), IngestService.class),
+                mHelperServiceConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    private void doUnbindHelperService() {
+        if (mHelperService != null) {
+            mHelperService.setClientActivity(null);
+            unbindService(mHelperServiceConnection);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/IngestService.java b/src/com/android/gallery3d/ingest/IngestService.java
new file mode 100644
index 0000000..0ce3ab6
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestService.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.mtp.MtpDevice;
+import android.mtp.MtpDeviceInfo;
+import android.mtp.MtpObjectInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.util.SparseBooleanArray;
+import android.widget.Adapter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.NotificationIds;
+import com.android.gallery3d.data.MtpClient;
+import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class IngestService extends Service implements ImportTask.Listener,
+        MtpDeviceIndex.ProgressListener, MtpClient.Listener {
+
+    public class LocalBinder extends Binder {
+        IngestService getService() {
+            return IngestService.this;
+        }
+    }
+
+    private static final int PROGRESS_UPDATE_INTERVAL_MS = 180;
+
+    private static MtpClient sClient;
+
+    private final IBinder mBinder = new LocalBinder();
+    private ScannerClient mScannerClient;
+    private MtpDevice mDevice;
+    private String mDevicePrettyName;
+    private MtpDeviceIndex mIndex;
+    private IngestActivity mClientActivity;
+    private boolean mRedeliverImportFinish = false;
+    private int mRedeliverImportFinishCount = 0;
+    private Collection<MtpObjectInfo> mRedeliverObjectsNotImported;
+    private boolean mRedeliverNotifyIndexChanged = false;
+    private boolean mRedeliverIndexFinish = false;
+    private NotificationManager mNotificationManager;
+    private NotificationCompat.Builder mNotificationBuilder;
+    private long mLastProgressIndexTime = 0;
+    private boolean mNeedRelaunchNotification = false;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mScannerClient = new ScannerClient(this);
+        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+        mNotificationBuilder = new NotificationCompat.Builder(this);
+        mNotificationBuilder.setSmallIcon(android.R.drawable.stat_notify_sync) // TODO drawable
+                .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, IngestActivity.class), 0));
+        mIndex = MtpDeviceIndex.getInstance();
+        mIndex.setProgressListener(this);
+
+        if (sClient == null) {
+            sClient = new MtpClient(getApplicationContext());
+        }
+        List<MtpDevice> devices = sClient.getDeviceList();
+        if (devices.size() > 0) {
+            setDevice(devices.get(0));
+        }
+        sClient.addListener(this);
+    }
+
+    @Override
+    public void onDestroy() {
+        sClient.removeListener(this);
+        mIndex.unsetProgressListener(this);
+        super.onDestroy();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    private void setDevice(MtpDevice device) {
+        if (mDevice == device) return;
+        mRedeliverImportFinish = false;
+        mRedeliverObjectsNotImported = null;
+        mRedeliverNotifyIndexChanged = false;
+        mRedeliverIndexFinish = false;
+        mDevice = device;
+        mIndex.setDevice(mDevice);
+        if (mDevice != null) {
+            MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo();
+            if (deviceInfo == null) {
+                setDevice(null);
+                return;
+            } else {
+                mDevicePrettyName = deviceInfo.getModel();
+                mNotificationBuilder.setContentTitle(mDevicePrettyName);
+                new Thread(mIndex.getIndexRunnable()).start();
+            }
+        } else {
+            mDevicePrettyName = null;
+        }
+        if (mClientActivity != null) {
+            mClientActivity.notifyIndexChanged();
+        } else {
+            mRedeliverNotifyIndexChanged = true;
+        }
+    }
+
+    protected MtpDeviceIndex getIndex() {
+        return mIndex;
+    }
+
+    protected void setClientActivity(IngestActivity activity) {
+        if (mClientActivity == activity) return;
+        mClientActivity = activity;
+        if (mClientActivity == null) {
+            if (mNeedRelaunchNotification) {
+                mNotificationBuilder.setProgress(0, 0, false)
+                    .setContentText(getResources().getText(R.string.ingest_scanning_done));
+                mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+                    mNotificationBuilder.build());
+            }
+            return;
+        }
+        mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_IMPORTING);
+        mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
+        if (mRedeliverImportFinish) {
+            mClientActivity.onImportFinish(mRedeliverObjectsNotImported,
+                    mRedeliverImportFinishCount);
+            mRedeliverImportFinish = false;
+            mRedeliverObjectsNotImported = null;
+        }
+        if (mRedeliverNotifyIndexChanged) {
+            mClientActivity.notifyIndexChanged();
+            mRedeliverNotifyIndexChanged = false;
+        }
+        if (mRedeliverIndexFinish) {
+            mClientActivity.onIndexFinish();
+            mRedeliverIndexFinish = false;
+        }
+    }
+
+    protected void importSelectedItems(SparseBooleanArray selected, Adapter adapter) {
+        List<MtpObjectInfo> importHandles = new ArrayList<MtpObjectInfo>();
+        for (int i = 0; i < selected.size(); i++) {
+            if (selected.valueAt(i)) {
+                Object item = adapter.getItem(selected.keyAt(i));
+                if (item instanceof MtpObjectInfo) {
+                    importHandles.add(((MtpObjectInfo) item));
+                }
+            }
+        }
+        ImportTask task = new ImportTask(mDevice, importHandles, BucketNames.IMPORTED, this);
+        task.setListener(this);
+        mNotificationBuilder.setProgress(0, 0, true)
+            .setContentText(getResources().getText(R.string.ingest_importing));
+        startForeground(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+                    mNotificationBuilder.build());
+        new Thread(task).start();
+    }
+
+    @Override
+    public void deviceAdded(MtpDevice device) {
+        if (mDevice == null) {
+            setDevice(device);
+            UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
+                    "DeviceConnected", null);
+        }
+    }
+
+    @Override
+    public void deviceRemoved(MtpDevice device) {
+        if (device == mDevice) {
+            setDevice(null);
+            mNeedRelaunchNotification = false;
+            mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
+        }
+    }
+
+    @Override
+    public void onImportProgress(int visitedCount, int totalCount,
+            String pathIfSuccessful) {
+        if (pathIfSuccessful != null) {
+            mScannerClient.scanPath(pathIfSuccessful);
+        }
+        mNeedRelaunchNotification = false;
+        if (mClientActivity != null) {
+            mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful);
+        }
+        mNotificationBuilder.setProgress(totalCount, visitedCount, false)
+            .setContentText(getResources().getText(R.string.ingest_importing));
+        mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+                mNotificationBuilder.build());
+    }
+
+    @Override
+    public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported,
+            int visitedCount) {
+        stopForeground(true);
+        mNeedRelaunchNotification = true;
+        if (mClientActivity != null) {
+            mClientActivity.onImportFinish(objectsNotImported, visitedCount);
+        } else {
+            mRedeliverImportFinish = true;
+            mRedeliverObjectsNotImported = objectsNotImported;
+            mRedeliverImportFinishCount = visitedCount;
+            mNotificationBuilder.setProgress(0, 0, false)
+                .setContentText(getResources().getText(R.string.import_complete));
+            mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+                    mNotificationBuilder.build());
+        }
+        UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
+                "ImportFinished", null, visitedCount);
+    }
+
+    @Override
+    public void onObjectIndexed(MtpObjectInfo object, int numVisited) {
+        mNeedRelaunchNotification = false;
+        if (mClientActivity != null) {
+            mClientActivity.onObjectIndexed(object, numVisited);
+        } else {
+            // Throttle the updates to one every PROGRESS_UPDATE_INTERVAL_MS milliseconds
+            long currentTime = SystemClock.uptimeMillis();
+            if (currentTime > mLastProgressIndexTime + PROGRESS_UPDATE_INTERVAL_MS) {
+                mLastProgressIndexTime = currentTime;
+                mNotificationBuilder.setProgress(0, numVisited, true)
+                        .setContentText(getResources().getText(R.string.ingest_scanning));
+                mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+                        mNotificationBuilder.build());
+            }
+        }
+    }
+
+    @Override
+    public void onSorting() {
+        if (mClientActivity != null) mClientActivity.onSorting();
+    }
+
+    @Override
+    public void onIndexFinish() {
+        mNeedRelaunchNotification = true;
+        if (mClientActivity != null) {
+            mClientActivity.onIndexFinish();
+        } else {
+            mNotificationBuilder.setProgress(0, 0, false)
+                .setContentText(getResources().getText(R.string.ingest_scanning_done));
+            mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+                    mNotificationBuilder.build());
+            mRedeliverIndexFinish = true;
+        }
+    }
+
+    // Copied from old Gallery3d code
+    private static final class ScannerClient implements MediaScannerConnectionClient {
+        ArrayList<String> mPaths = new ArrayList<String>();
+        MediaScannerConnection mScannerConnection;
+        boolean mConnected;
+        Object mLock = new Object();
+
+        public ScannerClient(Context context) {
+            mScannerConnection = new MediaScannerConnection(context, this);
+        }
+
+        public void scanPath(String path) {
+            synchronized (mLock) {
+                if (mConnected) {
+                    mScannerConnection.scanFile(path, null);
+                } else {
+                    mPaths.add(path);
+                    mScannerConnection.connect();
+                }
+            }
+        }
+
+        @Override
+        public void onMediaScannerConnected() {
+            synchronized (mLock) {
+                mConnected = true;
+                if (!mPaths.isEmpty()) {
+                    for (String path : mPaths) {
+                        mScannerConnection.scanFile(path, null);
+                    }
+                    mPaths.clear();
+                }
+            }
+        }
+
+        @Override
+        public void onScanCompleted(String path, Uri uri) {
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/MtpDeviceIndex.java b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java
new file mode 100644
index 0000000..d30f94a
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest;
+
+import android.mtp.MtpConstants;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * MTP objects in the index are organized into "buckets," or groupings.
+ * At present, these buckets are based on the date an item was created.
+ *
+ * When the index is created, the buckets are sorted in their natural
+ * order, and the items within the buckets sorted by the date they are taken.
+ *
+ * The index enables the access of items and bucket labels as one unified list.
+ * For example, let's say we have the following data in the index:
+ *    [Bucket A]: [photo 1], [photo 2]
+ *    [Bucket B]: [photo 3]
+ *
+ * Then the items can be thought of as being organized as a 5 element list:
+ *   [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3]
+ *
+ * The data can also be accessed in descending order, in which case the list
+ * would be a bit different from simply reversing the ascending list, since the
+ * bucket labels need to always be at the beginning:
+ *   [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1]
+ *
+ * The index enables all the following operations in constant time, both for
+ * ascending and descending views of the data:
+ *   - get/getAscending/getDescending: get an item at a specified list position
+ *   - size: get the total number of items (bucket labels and MTP objects)
+ *   - getFirstPositionForBucketNumber
+ *   - getBucketNumberForPosition
+ *   - isFirstInBucket
+ *
+ * See the comments in buildLookupIndex for implementation notes.
+ */
+public class MtpDeviceIndex {
+
+    public static final int FORMAT_MOV = 0x300D; // For some reason this is not in MtpConstants
+
+    public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
+    public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
+
+    static {
+        SUPPORTED_IMAGE_FORMATS = new HashSet<Integer>();
+        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_JFIF);
+        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_EXIF_JPEG);
+        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_PNG);
+        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_GIF);
+        SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_BMP);
+
+        SUPPORTED_VIDEO_FORMATS = new HashSet<Integer>();
+        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_3GP_CONTAINER);
+        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_AVI);
+        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MP4_CONTAINER);
+        SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MPEG);
+        // TODO: add FORMAT_MOV once Media Scanner supports .mov files
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId());
+        result = prime * result + mGeneration;
+        return result;
+    }
+
+    public interface ProgressListener {
+        public void onObjectIndexed(MtpObjectInfo object, int numVisited);
+
+        public void onSorting();
+
+        public void onIndexFinish();
+    }
+
+    public enum SortOrder {
+        Ascending, Descending
+    }
+
+    private MtpDevice mDevice;
+    private int[] mUnifiedLookupIndex;
+    private MtpObjectInfo[] mMtpObjects;
+    private DateBucket[] mBuckets;
+    private int mGeneration = 0;
+
+    public enum Progress {
+        Uninitialized, Initialized, Pending, Started, Sorting, Finished
+    }
+
+    private Progress mProgress = Progress.Uninitialized;
+    private ProgressListener mProgressListener;
+
+    private static final MtpDeviceIndex sInstance = new MtpDeviceIndex();
+    private static final MtpObjectTimestampComparator sMtpObjectComparator =
+            new MtpObjectTimestampComparator();
+
+    public static MtpDeviceIndex getInstance() {
+        return sInstance;
+    }
+
+    private MtpDeviceIndex() {
+    }
+
+    synchronized public MtpDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * Sets the MtpDevice that should be indexed and initializes state, but does
+     * not kick off the actual indexing task, which is instead done by using
+     * {@link #getIndexRunnable()}
+     *
+     * @param device The MtpDevice that should be indexed
+     */
+    synchronized public void setDevice(MtpDevice device) {
+        if (device == mDevice) return;
+        mDevice = device;
+        resetState();
+    }
+
+    /**
+     * Provides a Runnable for the indexing task assuming the state has already
+     * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and
+     * has not already been run.
+     *
+     * @return Runnable for the main indexing task
+     */
+    synchronized public Runnable getIndexRunnable() {
+        if (mProgress != Progress.Initialized) return null;
+        mProgress = Progress.Pending;
+        return new IndexRunnable(mDevice);
+    }
+
+    synchronized public boolean indexReady() {
+        return mProgress == Progress.Finished;
+    }
+
+    synchronized public Progress getProgress() {
+        return mProgress;
+    }
+
+    /**
+     * @param listener Listener to change to
+     * @return Progress at the time the listener was added (useful for
+     *         configuring initial UI state)
+     */
+    synchronized public Progress setProgressListener(ProgressListener listener) {
+        mProgressListener = listener;
+        return mProgress;
+    }
+
+    /**
+     * Make the listener null if it matches the argument
+     *
+     * @param listener Listener to unset, if currently registered
+     */
+    synchronized public void unsetProgressListener(ProgressListener listener) {
+        if (mProgressListener == listener)
+            mProgressListener = null;
+    }
+
+    /**
+     * @return The total number of elements in the index (labels and items)
+     */
+    public int size() {
+        return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0;
+    }
+
+    /**
+     * @param position Index of item to fetch, where 0 is the first item in the
+     *            specified order
+     * @param order
+     * @return the bucket label or MtpObjectInfo at the specified position and
+     *         order
+     */
+    public Object get(int position, SortOrder order) {
+        if (mProgress != Progress.Finished) return null;
+        if(order == SortOrder.Ascending) {
+            DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
+            if (bucket.unifiedStartIndex == position) {
+                return bucket.bucket;
+            } else {
+                return mMtpObjects[bucket.itemsStartIndex + position - 1
+                                   - bucket.unifiedStartIndex];
+            }
+        } else {
+            int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
+            DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
+            if (bucket.unifiedEndIndex == zeroIndex) {
+                return bucket.bucket;
+            } else {
+                return mMtpObjects[bucket.itemsStartIndex + zeroIndex
+                                   - bucket.unifiedStartIndex];
+            }
+        }
+    }
+
+    /**
+     * @param position Index of item to fetch from a view of the data that doesn't
+     *            include labels and is in the specified order
+     * @return position-th item in specified order, when not including labels
+     */
+    public MtpObjectInfo getWithoutLabels(int position, SortOrder order) {
+        if (mProgress != Progress.Finished) return null;
+        if (order == SortOrder.Ascending) {
+            return mMtpObjects[position];
+        } else {
+            return mMtpObjects[mMtpObjects.length - 1 - position];
+        }
+    }
+
+    /**
+     * Although this is O(log(number of buckets)), and thus should not be used
+     * in hotspots, even if the attached device has items for every day for
+     * a five-year timeframe, it would still only take 11 iterations at most,
+     * so shouldn't be a huge issue.
+     * @param position Index of item to map from a view of the data that doesn't
+     *            include labels and is in the specified order
+     * @param order
+     * @return position in a view of the data that does include labels
+     */
+    public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
+        if (mProgress != Progress.Finished) return -1;
+        if (order == SortOrder.Descending) {
+            position = mMtpObjects.length - 1 - position;
+        }
+        int bucketNumber = 0;
+        int iMin = 0;
+        int iMax = mBuckets.length - 1;
+        while (iMax >= iMin) {
+            int iMid = (iMax + iMin) / 2;
+            if (mBuckets[iMid].itemsStartIndex + mBuckets[iMid].numItems <= position) {
+                iMin = iMid + 1;
+            } else if (mBuckets[iMid].itemsStartIndex > position) {
+                iMax = iMid - 1;
+            } else {
+                bucketNumber = iMid;
+                break;
+            }
+        }
+        int mappedPos = mBuckets[bucketNumber].unifiedStartIndex
+                + position - mBuckets[bucketNumber].itemsStartIndex;
+        if (order == SortOrder.Descending) {
+            mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos;
+        }
+        return mappedPos;
+    }
+
+    public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
+        if (mProgress != Progress.Finished) return -1;
+        if(order == SortOrder.Ascending) {
+            DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
+            if (bucket.unifiedStartIndex == position) position++;
+            return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
+        } else {
+            int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
+            DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
+            if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--;
+            return mMtpObjects.length - 1 - bucket.itemsStartIndex
+                    - zeroIndex + bucket.unifiedStartIndex;
+        }
+    }
+
+    /**
+     * @return The number of MTP items in the index (without labels)
+     */
+    public int sizeWithoutLabels() {
+        return mProgress == Progress.Finished ? mMtpObjects.length : 0;
+    }
+
+    public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
+        if (order == SortOrder.Ascending) {
+            return mBuckets[bucketNumber].unifiedStartIndex;
+        } else {
+            return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1;
+        }
+    }
+
+    public int getBucketNumberForPosition(int position, SortOrder order) {
+        if (order == SortOrder.Ascending) {
+            return mUnifiedLookupIndex[position];
+        } else {
+            return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position];
+        }
+    }
+
+    public boolean isFirstInBucket(int position, SortOrder order) {
+        if (order == SortOrder.Ascending) {
+            return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position;
+        } else {
+            position = mUnifiedLookupIndex.length - 1 - position;
+            return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position;
+        }
+    }
+
+    private Object[] mCachedReverseBuckets;
+
+    public Object[] getBuckets(SortOrder order) {
+        if (mBuckets == null) return null;
+        if (order == SortOrder.Ascending) {
+            return mBuckets;
+        } else {
+            if (mCachedReverseBuckets == null) {
+                computeReversedBuckets();
+            }
+            return mCachedReverseBuckets;
+        }
+    }
+
+    /*
+     * See the comments for buildLookupIndex for notes on the specific fields of
+     * this class.
+     */
+    private class DateBucket implements Comparable<DateBucket> {
+        SimpleDate bucket;
+        List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>();
+        int unifiedStartIndex;
+        int unifiedEndIndex;
+        int itemsStartIndex;
+        int numItems;
+
+        public DateBucket(SimpleDate bucket) {
+            this.bucket = bucket;
+        }
+
+        public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) {
+            this(bucket);
+            tempElementsList.add(firstElement);
+        }
+
+        void sortElements(Comparator<MtpObjectInfo> comparator) {
+            Collections.sort(tempElementsList, comparator);
+        }
+
+        @Override
+        public String toString() {
+            return bucket.toString();
+        }
+
+        @Override
+        public int hashCode() {
+            return bucket.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (!(obj instanceof DateBucket)) return false;
+            DateBucket other = (DateBucket) obj;
+            if (bucket == null) {
+                if (other.bucket != null) return false;
+            } else if (!bucket.equals(other.bucket)) {
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public int compareTo(DateBucket another) {
+            return this.bucket.compareTo(another.bucket);
+        }
+    }
+
+    /**
+     * Comparator to sort MtpObjectInfo objects by date created.
+     */
+    private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> {
+        @Override
+        public int compare(MtpObjectInfo o1, MtpObjectInfo o2) {
+            long diff = o1.getDateCreated() - o2.getDateCreated();
+            if (diff < 0) {
+                return -1;
+            } else if (diff == 0) {
+                return 0;
+            } else {
+                return 1;
+            }
+        }
+    }
+
+    private void resetState() {
+        mGeneration++;
+        mUnifiedLookupIndex = null;
+        mMtpObjects = null;
+        mBuckets = null;
+        mCachedReverseBuckets = null;
+        mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized;
+    }
+
+
+    private class IndexRunnable implements Runnable {
+        private int[] mUnifiedLookupIndex;
+        private MtpObjectInfo[] mMtpObjects;
+        private DateBucket[] mBuckets;
+        private Map<SimpleDate, DateBucket> mBucketsTemp;
+        private MtpDevice mDevice;
+        private int mNumObjects = 0;
+
+        private class IndexingException extends Exception {};
+
+        public IndexRunnable(MtpDevice device) {
+            mDevice = device;
+        }
+
+        /*
+         * Implementation note: this is the way the index supports a lot of its operations in
+         * constant time and respecting the need to have bucket names always come before items
+         * in that bucket when accessing the list sequentially, both in ascending and descending
+         * orders.
+         *
+         * Let's say the data we have in the index is the following:
+         *  [Bucket A]: [photo 1], [photo 2]
+         *  [Bucket B]: [photo 3]
+         *
+         *  In this case, the lookup index array would be
+         *  [0, 0, 0, 1, 1]
+         *
+         *  Now, whether we access the list in ascending or descending order, we know which bucket
+         *  to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first
+         *  item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex
+         *  that correspond to indices in this lookup index array, allowing us to calculate the
+         *  offset of the specific item we want from within a specific bucket.
+         */
+        private void buildLookupIndex() {
+            int numBuckets = mBuckets.length;
+            mUnifiedLookupIndex = new int[mNumObjects + numBuckets];
+            int currentUnifiedIndexEntry = 0;
+            int nextUnifiedEntry;
+
+            mMtpObjects = new MtpObjectInfo[mNumObjects];
+            int currentItemsEntry = 0;
+            for (int i = 0; i < numBuckets; i++) {
+                DateBucket bucket = mBuckets[i];
+                nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1;
+                Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i);
+                bucket.unifiedStartIndex = currentUnifiedIndexEntry;
+                bucket.unifiedEndIndex = nextUnifiedEntry - 1;
+                currentUnifiedIndexEntry = nextUnifiedEntry;
+
+                bucket.itemsStartIndex = currentItemsEntry;
+                bucket.numItems = bucket.tempElementsList.size();
+                for (int j = 0; j < bucket.numItems; j++) {
+                    mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j);
+                    currentItemsEntry++;
+                }
+                bucket.tempElementsList = null;
+            }
+        }
+
+        private void copyResults() {
+            MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex;
+            MtpDeviceIndex.this.mMtpObjects = mMtpObjects;
+            MtpDeviceIndex.this.mBuckets = mBuckets;
+            mUnifiedLookupIndex = null;
+            mMtpObjects = null;
+            mBuckets = null;
+        }
+
+        @Override
+        public void run() {
+            try {
+                indexDevice();
+            } catch (IndexingException e) {
+                synchronized (MtpDeviceIndex.this) {
+                    resetState();
+                    if (mProgressListener != null) {
+                        mProgressListener.onIndexFinish();
+                    }
+                }
+            }
+        }
+
+        private void indexDevice() throws IndexingException {
+            synchronized (MtpDeviceIndex.this) {
+                mProgress = Progress.Started;
+            }
+            mBucketsTemp = new HashMap<SimpleDate, DateBucket>();
+            for (int storageId : mDevice.getStorageIds()) {
+                if (mDevice != getDevice()) throw new IndexingException();
+                Stack<Integer> pendingDirectories = new Stack<Integer>();
+                pendingDirectories.add(0xFFFFFFFF); // start at the root of the device
+                while (!pendingDirectories.isEmpty()) {
+                    if (mDevice != getDevice()) throw new IndexingException();
+                    int dirHandle = pendingDirectories.pop();
+                    for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
+                        MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle);
+                        if (objectInfo == null) throw new IndexingException();
+                        int format = objectInfo.getFormat();
+                        if (format == MtpConstants.FORMAT_ASSOCIATION) {
+                            pendingDirectories.add(objectHandle);
+                        } else if (SUPPORTED_IMAGE_FORMATS.contains(format)
+                                || SUPPORTED_VIDEO_FORMATS.contains(format)) {
+                            addObject(objectInfo);
+                        }
+                    }
+                }
+            }
+            Collection<DateBucket> values = mBucketsTemp.values();
+            mBucketsTemp = null;
+            mBuckets = values.toArray(new DateBucket[values.size()]);
+            values = null;
+            synchronized (MtpDeviceIndex.this) {
+                mProgress = Progress.Sorting;
+                if (mProgressListener != null) {
+                    mProgressListener.onSorting();
+                }
+            }
+            sortAll();
+            buildLookupIndex();
+            synchronized (MtpDeviceIndex.this) {
+                if (mDevice != getDevice()) throw new IndexingException();
+                copyResults();
+
+                /*
+                 * In order for getBuckets to operate in constant time for descending
+                 * order, we must precompute a reversed array of the buckets, mainly
+                 * because the android.widget.SectionIndexer interface which adapters
+                 * that call getBuckets implement depends on section numbers to be
+                 * ascending relative to the scroll position, so we must have this for
+                 * descending order or the scrollbar goes crazy.
+                 */
+                computeReversedBuckets();
+
+                mProgress = Progress.Finished;
+                if (mProgressListener != null) {
+                    mProgressListener.onIndexFinish();
+                }
+            }
+        }
+
+        private SimpleDate mDateInstance = new SimpleDate();
+
+        private void addObject(MtpObjectInfo objectInfo) {
+            mNumObjects++;
+            mDateInstance.setTimestamp(objectInfo.getDateCreated());
+            DateBucket bucket = mBucketsTemp.get(mDateInstance);
+            if (bucket == null) {
+                bucket = new DateBucket(mDateInstance, objectInfo);
+                mBucketsTemp.put(mDateInstance, bucket);
+                mDateInstance = new SimpleDate(); // only create new date
+                                                  // objects when they are used
+                return;
+            } else {
+                bucket.tempElementsList.add(objectInfo);
+            }
+            if (mProgressListener != null) {
+                mProgressListener.onObjectIndexed(objectInfo, mNumObjects);
+            }
+        }
+
+        private void sortAll() {
+            Arrays.sort(mBuckets);
+            for (DateBucket bucket : mBuckets) {
+                bucket.sortElements(sMtpObjectComparator);
+            }
+        }
+
+    }
+
+    private void computeReversedBuckets() {
+        mCachedReverseBuckets = new Object[mBuckets.length];
+        for (int i = 0; i < mCachedReverseBuckets.length; i++) {
+            mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i];
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/SimpleDate.java b/src/com/android/gallery3d/ingest/SimpleDate.java
new file mode 100644
index 0000000..05db2cd
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/SimpleDate.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+
+/**
+ * Represents a date (year, month, day)
+ */
+public class SimpleDate implements Comparable<SimpleDate> {
+    public int month; // MM
+    public int day; // DD
+    public int year; // YYYY
+    private long timestamp;
+    private String mCachedStringRepresentation;
+
+    public SimpleDate() {
+    }
+
+    public SimpleDate(long timestamp) {
+        setTimestamp(timestamp);
+    }
+
+    private static Calendar sCalendarInstance = Calendar.getInstance();
+
+    public void setTimestamp(long timestamp) {
+        synchronized (sCalendarInstance) {
+            // TODO find a more efficient way to convert a timestamp to a date?
+            sCalendarInstance.setTimeInMillis(timestamp);
+            this.day = sCalendarInstance.get(Calendar.DATE);
+            this.month = sCalendarInstance.get(Calendar.MONTH);
+            this.year = sCalendarInstance.get(Calendar.YEAR);
+            this.timestamp = timestamp;
+            mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + day;
+        result = prime * result + month;
+        result = prime * result + year;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (!(obj instanceof SimpleDate))
+            return false;
+        SimpleDate other = (SimpleDate) obj;
+        if (year != other.year)
+            return false;
+        if (month != other.month)
+            return false;
+        if (day != other.day)
+            return false;
+        return true;
+    }
+
+    @Override
+    public int compareTo(SimpleDate other) {
+        int yearDiff = this.year - other.getYear();
+        if (yearDiff != 0)
+            return yearDiff;
+        else {
+            int monthDiff = this.month - other.getMonth();
+            if (monthDiff != 0)
+                return monthDiff;
+            else
+                return this.day - other.getDay();
+        }
+    }
+
+    public int getDay() {
+        return day;
+    }
+
+    public int getMonth() {
+        return month;
+    }
+
+    public int getYear() {
+        return year;
+    }
+
+    @Override
+    public String toString() {
+        if (mCachedStringRepresentation == null) {
+            mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp);
+        }
+        return mCachedStringRepresentation;
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/CheckBroker.java b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java
new file mode 100644
index 0000000..6783f23
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.adapter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public abstract class CheckBroker {
+    private Collection<OnCheckedChangedListener> mListeners =
+            new ArrayList<OnCheckedChangedListener>();
+
+    public interface OnCheckedChangedListener {
+        public void onCheckedChanged(int position, boolean isChecked);
+        public void onBulkCheckedChanged();
+    }
+
+    public abstract void setItemChecked(int position, boolean checked);
+
+    public void onCheckedChange(int position, boolean checked) {
+        if (isItemChecked(position) != checked) {
+            for (OnCheckedChangedListener l : mListeners) {
+                l.onCheckedChanged(position, checked);
+            }
+        }
+    }
+
+    public void onBulkCheckedChange() {
+        for (OnCheckedChangedListener l : mListeners) {
+            l.onBulkCheckedChanged();
+        }
+    }
+
+    public abstract boolean isItemChecked(int position);
+
+    public void registerOnCheckedChangeListener(OnCheckedChangedListener l) {
+        mListeners.add(l);
+    }
+
+    public void unregisterOnCheckedChangeListener(OnCheckedChangedListener l) {
+        mListeners.remove(l);
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
new file mode 100644
index 0000000..e8dd69f
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.adapter;
+
+import android.app.Activity;
+import android.content.Context;
+import android.mtp.MtpObjectInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.SectionIndexer;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.SimpleDate;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.MtpThumbnailTileView;
+
+public class MtpAdapter extends BaseAdapter implements SectionIndexer {
+    public static final int ITEM_TYPE_MEDIA = 0;
+    public static final int ITEM_TYPE_BUCKET = 1;
+
+    private Context mContext;
+    private MtpDeviceIndex mModel;
+    private SortOrder mSortOrder = SortOrder.Descending;
+    private LayoutInflater mInflater;
+    private int mGeneration = 0;
+
+    public MtpAdapter(Activity context) {
+        super();
+        mContext = context;
+        mInflater = LayoutInflater.from(context);
+    }
+
+    public void setMtpDeviceIndex(MtpDeviceIndex index) {
+        mModel = index;
+        notifyDataSetChanged();
+    }
+
+    public MtpDeviceIndex getMtpDeviceIndex() {
+        return mModel;
+    }
+
+    @Override
+    public void notifyDataSetChanged() {
+        mGeneration++;
+        super.notifyDataSetChanged();
+    }
+
+    @Override
+    public void notifyDataSetInvalidated() {
+        mGeneration++;
+        super.notifyDataSetInvalidated();
+    }
+
+    public boolean deviceConnected() {
+        return (mModel != null) && (mModel.getDevice() != null);
+    }
+
+    public boolean indexReady() {
+        return (mModel != null) && mModel.indexReady();
+    }
+
+    @Override
+    public int getCount() {
+        return mModel != null ? mModel.size() : 0;
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return mModel.get(position, mSortOrder);
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        return true;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 2;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        // If the position is the first in its section, then it corresponds to
+        // a title tile, if not it's a media tile
+        if (position == getPositionForSection(getSectionForPosition(position))) {
+            return ITEM_TYPE_BUCKET;
+        } else {
+            return ITEM_TYPE_MEDIA;
+        }
+    }
+
+    public boolean itemAtPositionIsBucket(int position) {
+        return getItemViewType(position) == ITEM_TYPE_BUCKET;
+    }
+
+    public boolean itemAtPositionIsMedia(int position) {
+        return getItemViewType(position) == ITEM_TYPE_MEDIA;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        int type = getItemViewType(position);
+        if (type == ITEM_TYPE_MEDIA) {
+            MtpThumbnailTileView imageView;
+            if (convertView == null) {
+                imageView = (MtpThumbnailTileView) mInflater.inflate(
+                        R.layout.ingest_thumbnail, parent, false);
+            } else {
+                imageView = (MtpThumbnailTileView) convertView;
+            }
+            imageView.setMtpDeviceAndObjectInfo(mModel.getDevice(), (MtpObjectInfo)getItem(position), mGeneration);
+            return imageView;
+        } else {
+            DateTileView dateTile;
+            if (convertView == null) {
+                dateTile = (DateTileView) mInflater.inflate(
+                        R.layout.ingest_date_tile, parent, false);
+            } else {
+                dateTile = (DateTileView) convertView;
+            }
+            dateTile.setDate((SimpleDate)getItem(position));
+            return dateTile;
+        }
+    }
+
+    @Override
+    public int getPositionForSection(int section) {
+        if (getCount() == 0) {
+            return 0;
+        }
+        int numSections = getSections().length;
+        if (section >= numSections) {
+            section = numSections - 1;
+        }
+        return mModel.getFirstPositionForBucketNumber(section, mSortOrder);
+    }
+
+    @Override
+    public int getSectionForPosition(int position) {
+        int count = getCount();
+        if (count == 0) {
+            return 0;
+        }
+        if (position >= count) {
+            position = count - 1;
+        }
+        return mModel.getBucketNumberForPosition(position, mSortOrder);
+    }
+
+    @Override
+    public Object[] getSections() {
+        return getCount() > 0 ? mModel.getBuckets(mSortOrder) : null;
+    }
+
+    public SortOrder getSortOrder() {
+        return mSortOrder;
+    }
+
+    public int translatePositionWithoutLabels(int position) {
+        if (mModel == null) return -1;
+        return mModel.getPositionFromPositionWithoutLabels(position, mSortOrder);
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java
new file mode 100644
index 0000000..9e7abc0
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.adapter;
+
+import android.content.Context;
+import android.mtp.MtpObjectInfo;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.ui.MtpFullscreenView;
+
+public class MtpPagerAdapter extends PagerAdapter {
+
+    private LayoutInflater mInflater;
+    private int mGeneration = 0;
+    private CheckBroker mBroker;
+    private MtpDeviceIndex mModel;
+    private SortOrder mSortOrder = SortOrder.Descending;
+
+    private MtpFullscreenView mReusableView = null;
+
+    public MtpPagerAdapter(Context context, CheckBroker broker) {
+        super();
+        mInflater = LayoutInflater.from(context);
+        mBroker = broker;
+    }
+
+    public void setMtpDeviceIndex(MtpDeviceIndex index) {
+        mModel = index;
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public int getCount() {
+        return mModel != null ? mModel.sizeWithoutLabels() : 0;
+    }
+
+    @Override
+    public void notifyDataSetChanged() {
+        mGeneration++;
+        super.notifyDataSetChanged();
+    }
+
+    public int translatePositionWithLabels(int position) {
+        if (mModel == null) return -1;
+        return mModel.getPositionWithoutLabelsFromPosition(position, mSortOrder);
+    }
+
+    @Override
+    public void finishUpdate(ViewGroup container) {
+        mReusableView = null;
+        super.finishUpdate(container);
+    }
+
+    @Override
+    public boolean isViewFromObject(View view, Object object) {
+        return view == object;
+    }
+
+    @Override
+    public void destroyItem(ViewGroup container, int position, Object object) {
+        MtpFullscreenView v = (MtpFullscreenView)object;
+        container.removeView(v);
+        mBroker.unregisterOnCheckedChangeListener(v);
+        mReusableView = v;
+    }
+
+    @Override
+    public Object instantiateItem(ViewGroup container, int position) {
+        MtpFullscreenView v;
+        if (mReusableView != null) {
+            v = mReusableView;
+            mReusableView = null;
+        } else {
+            v = (MtpFullscreenView) mInflater.inflate(R.layout.ingest_fullsize, container, false);
+        }
+        MtpObjectInfo i = mModel.getWithoutLabels(position, mSortOrder);
+        v.getImageView().setMtpDeviceAndObjectInfo(mModel.getDevice(), i, mGeneration);
+        v.setPositionAndBroker(position, mBroker);
+        container.addView(v);
+        return v;
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
new file mode 100644
index 0000000..bbc90f6
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.data;
+
+import android.graphics.Bitmap;
+
+public class BitmapWithMetadata {
+    public Bitmap bitmap;
+    public int rotationDegrees;
+
+    public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) {
+        this.bitmap = bitmap;
+        this.rotationDegrees = rotationDegrees;
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
new file mode 100644
index 0000000..30868c2
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.data;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import com.android.camera.Exif;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class MtpBitmapFetch {
+    private static int sMaxSize = 0;
+
+    public static void recycleThumbnail(Bitmap b) {
+        if (b != null) {
+            GalleryBitmapPool.getInstance().put(b);
+        }
+    }
+
+    public static Bitmap getThumbnail(MtpDevice device, MtpObjectInfo info) {
+        byte[] imageBytes = device.getThumbnail(info.getObjectHandle());
+        if (imageBytes == null) {
+            return null;
+        }
+        BitmapFactory.Options o = new BitmapFactory.Options();
+        o.inJustDecodeBounds = true;
+        BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+        if (o.outWidth == 0 || o.outHeight == 0) {
+            return null;
+        }
+        o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight);
+        o.inMutable = true;
+        o.inJustDecodeBounds = false;
+        o.inSampleSize = 1;
+        try {
+            return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+        } catch (IllegalArgumentException e) {
+            // BitmapFactory throws an exception rather than returning null
+            // when image decoding fails and an existing bitmap was supplied
+            // for recycling, even if the failure was not caused by the use
+            // of that bitmap.
+            return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+        }
+    }
+
+    public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info) {
+        return getFullsize(device, info, sMaxSize);
+    }
+
+    public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info, int maxSide) {
+        byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize());
+        if (imageBytes == null) {
+            return null;
+        }
+        Bitmap created;
+        if (maxSide > 0) {
+            BitmapFactory.Options o = new BitmapFactory.Options();
+            o.inJustDecodeBounds = true;
+            BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+            int w = o.outWidth;
+            int h = o.outHeight;
+            int comp = Math.max(h, w);
+            int sampleSize = 1;
+            while ((comp >> 1) >= maxSide) {
+                comp = comp >> 1;
+                sampleSize++;
+            }
+            o.inSampleSize = sampleSize;
+            o.inJustDecodeBounds = false;
+            created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+        } else {
+            created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+        }
+        if (created == null) {
+            return null;
+        }
+
+        return new BitmapWithMetadata(created, Exif.getOrientation(imageBytes));
+    }
+
+    public static void configureForContext(Context context) {
+        DisplayMetrics metrics = new DisplayMetrics();
+        WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+        wm.getDefaultDisplay().getMetrics(metrics);
+        sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels);
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/DateTileView.java b/src/com/android/gallery3d/ingest/ui/DateTileView.java
new file mode 100644
index 0000000..52fe9b8
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/DateTileView.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.SimpleDate;
+
+import java.text.DateFormatSymbols;
+import java.util.Locale;
+
+public class DateTileView extends FrameLayout {
+    private static String[] sMonthNames = DateFormatSymbols.getInstance().getShortMonths();
+    private static Locale sLocale;
+
+    static {
+        refreshLocale();
+    }
+
+    public static boolean refreshLocale() {
+        Locale currentLocale = Locale.getDefault();
+        if (!currentLocale.equals(sLocale)) {
+            sLocale = currentLocale;
+            sMonthNames = DateFormatSymbols.getInstance(sLocale).getShortMonths();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private TextView mDateTextView;
+    private TextView mMonthTextView;
+    private TextView mYearTextView;
+    private int mMonth = -1;
+    private int mYear = -1;
+    private int mDate = -1;
+    private String[] mMonthNames = sMonthNames;
+
+    public DateTileView(Context context) {
+        super(context);
+    }
+
+    public DateTileView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DateTileView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Force this to be square
+        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mDateTextView = (TextView) findViewById(R.id.date_tile_day);
+        mMonthTextView = (TextView) findViewById(R.id.date_tile_month);
+        mYearTextView = (TextView) findViewById(R.id.date_tile_year);
+    }
+
+    public void setDate(SimpleDate date) {
+        setDate(date.getDay(), date.getMonth(), date.getYear());
+    }
+
+    public void setDate(int date, int month, int year) {
+        if (date != mDate) {
+            mDate = date;
+            mDateTextView.setText(mDate > 9 ? Integer.toString(mDate) : "0" + mDate);
+        }
+        if (mMonthNames != sMonthNames) {
+            mMonthNames = sMonthNames;
+            if (month == mMonth) {
+                mMonthTextView.setText(mMonthNames[mMonth]);
+            }
+        }
+        if (month != mMonth) {
+            mMonth = month;
+            mMonthTextView.setText(mMonthNames[mMonth]);
+        }
+        if (year != mYear) {
+            mYear = year;
+            mYearTextView.setText(Integer.toString(mYear));
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/IngestGridView.java b/src/com/android/gallery3d/ingest/ui/IngestGridView.java
new file mode 100644
index 0000000..c821259
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/IngestGridView.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+/**
+ * This just extends GridView with the ability to listen for calls
+ * to clearChoices()
+ */
+public class IngestGridView extends GridView {
+
+    public interface OnClearChoicesListener {
+        public void onClearChoices();
+    }
+
+    private OnClearChoicesListener mOnClearChoicesListener = null;
+
+    public IngestGridView(Context context) {
+        super(context);
+    }
+
+    public IngestGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public IngestGridView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void setOnClearChoicesListener(OnClearChoicesListener l) {
+        mOnClearChoicesListener = l;
+    }
+
+    @Override
+    public void clearChoices() {
+        super.clearChoices();
+        if (mOnClearChoicesListener != null) {
+            mOnClearChoicesListener.onClearChoices();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java
new file mode 100644
index 0000000..8d3884d
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.CheckBox;
+import android.widget.Checkable;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.CheckBroker;
+
+public class MtpFullscreenView extends RelativeLayout implements Checkable,
+    CompoundButton.OnCheckedChangeListener, CheckBroker.OnCheckedChangedListener {
+
+    private MtpImageView mImageView;
+    private CheckBox mCheckbox;
+    private int mPosition = -1;
+    private CheckBroker mBroker;
+
+    public MtpFullscreenView(Context context) {
+        super(context);
+    }
+
+    public MtpFullscreenView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MtpFullscreenView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mImageView = (MtpImageView) findViewById(R.id.ingest_fullsize_image);
+        mCheckbox = (CheckBox) findViewById(R.id.ingest_fullsize_image_checkbox);
+        mCheckbox.setOnCheckedChangeListener(this);
+    }
+
+    @Override
+    public boolean isChecked() {
+        return mCheckbox.isChecked();
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        mCheckbox.setChecked(checked);
+    }
+
+    @Override
+    public void toggle() {
+        mCheckbox.toggle();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        setPositionAndBroker(-1, null);
+        super.onDetachedFromWindow();
+    }
+
+    public MtpImageView getImageView() {
+        return mImageView;
+    }
+
+    public int getPosition() {
+        return mPosition;
+    }
+
+    public void setPositionAndBroker(int position, CheckBroker b) {
+        if (mBroker != null) {
+            mBroker.unregisterOnCheckedChangeListener(this);
+        }
+        mPosition = position;
+        mBroker = b;
+        if (mBroker != null) {
+            setChecked(mBroker.isItemChecked(position));
+            mBroker.registerOnCheckedChangeListener(this);
+        }
+    }
+
+    @Override
+    public void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
+        if (mBroker != null) mBroker.setItemChecked(mPosition, isChecked);
+    }
+
+    @Override
+    public void onCheckedChanged(int position, boolean isChecked) {
+        if (position == mPosition) {
+            setChecked(isChecked);
+        }
+    }
+
+    @Override
+    public void onBulkCheckedChanged() {
+        if(mBroker != null) setChecked(mBroker.isItemChecked(mPosition));
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpImageView.java b/src/com/android/gallery3d/ingest/ui/MtpImageView.java
new file mode 100644
index 0000000..80c1051
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpImageView.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.data.BitmapWithMetadata;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+
+import java.lang.ref.WeakReference;
+
+public class MtpImageView extends ImageView {
+    // We will use the thumbnail for images larger than this threshold
+    private static final int MAX_FULLSIZE_PREVIEW_SIZE = 8388608; // 8 megabytes
+
+    private int mObjectHandle;
+    private int mGeneration;
+
+    private WeakReference<MtpImageView> mWeakReference = new WeakReference<MtpImageView>(this);
+    private Object mFetchLock = new Object();
+    private boolean mFetchPending = false;
+    private MtpObjectInfo mFetchObjectInfo;
+    private MtpDevice mFetchDevice;
+    private Object mFetchResult;
+    private Drawable mOverlayIcon;
+    private boolean mShowOverlayIcon;
+
+    private static final FetchImageHandler sFetchHandler = FetchImageHandler.createOnNewThread();
+    private static final ShowImageHandler sFetchCompleteHandler = new ShowImageHandler();
+
+    private void init() {
+         showPlaceholder();
+    }
+
+    public MtpImageView(Context context) {
+        super(context);
+        init();
+    }
+
+    public MtpImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public MtpImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    private void showPlaceholder() {
+        setImageResource(android.R.color.transparent);
+    }
+
+    public void setMtpDeviceAndObjectInfo(MtpDevice device, MtpObjectInfo object, int gen) {
+        int handle = object.getObjectHandle();
+        if (handle == mObjectHandle && gen == mGeneration) {
+            return;
+        }
+        cancelLoadingAndClear();
+        showPlaceholder();
+        mGeneration = gen;
+        mObjectHandle = handle;
+        mShowOverlayIcon = MtpDeviceIndex.SUPPORTED_VIDEO_FORMATS.contains(object.getFormat());
+        if (mShowOverlayIcon && mOverlayIcon == null) {
+            mOverlayIcon = getResources().getDrawable(R.drawable.ic_control_play);
+            updateOverlayIconBounds();
+        }
+        synchronized (mFetchLock) {
+            mFetchObjectInfo = object;
+            mFetchDevice = device;
+            if (mFetchPending) return;
+            mFetchPending = true;
+            sFetchHandler.sendMessage(
+                    sFetchHandler.obtainMessage(0, mWeakReference));
+        }
+    }
+
+    protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
+        if (info.getCompressedSize() <= MAX_FULLSIZE_PREVIEW_SIZE
+                && MtpDeviceIndex.SUPPORTED_IMAGE_FORMATS.contains(info.getFormat())) {
+            return MtpBitmapFetch.getFullsize(device, info);
+        } else {
+            return new BitmapWithMetadata(MtpBitmapFetch.getThumbnail(device, info), 0);
+        }
+    }
+
+    private float mLastBitmapWidth;
+    private float mLastBitmapHeight;
+    private int mLastRotationDegrees;
+    private Matrix mDrawMatrix = new Matrix();
+
+    private void updateDrawMatrix() {
+        mDrawMatrix.reset();
+        float dwidth;
+        float dheight;
+        float vheight = getHeight();
+        float vwidth = getWidth();
+        float scale;
+        boolean rotated90 = (mLastRotationDegrees % 180 != 0);
+        if (rotated90) {
+            dwidth = mLastBitmapHeight;
+            dheight = mLastBitmapWidth;
+        } else {
+            dwidth = mLastBitmapWidth;
+            dheight = mLastBitmapHeight;
+        }
+        if (dwidth <= vwidth && dheight <= vheight) {
+            scale = 1.0f;
+        } else {
+            scale = Math.min(vwidth / dwidth, vheight / dheight);
+        }
+        mDrawMatrix.setScale(scale, scale);
+        if (rotated90) {
+            mDrawMatrix.postTranslate(-dheight * scale * 0.5f,
+                    -dwidth * scale * 0.5f);
+            mDrawMatrix.postRotate(mLastRotationDegrees);
+            mDrawMatrix.postTranslate(dwidth * scale * 0.5f,
+                    dheight * scale * 0.5f);
+        }
+        mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f,
+                (vheight - dheight * scale) * 0.5f);
+        if (!rotated90 && mLastRotationDegrees > 0) {
+            // rotated by a multiple of 180
+            mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2);
+        }
+        setImageMatrix(mDrawMatrix);
+    }
+
+    private static final int OVERLAY_ICON_SIZE_DENOMINATOR = 4;
+
+    private void updateOverlayIconBounds() {
+        int iheight = mOverlayIcon.getIntrinsicHeight();
+        int iwidth = mOverlayIcon.getIntrinsicWidth();
+        int vheight = getHeight();
+        int vwidth = getWidth();
+        float scale_height = ((float) vheight) / (iheight * OVERLAY_ICON_SIZE_DENOMINATOR);
+        float scale_width = ((float) vwidth) / (iwidth * OVERLAY_ICON_SIZE_DENOMINATOR);
+        if (scale_height >= 1f && scale_width >= 1f) {
+            mOverlayIcon.setBounds((vwidth - iwidth) / 2,
+                    (vheight - iheight) / 2,
+                    (vwidth + iwidth) / 2,
+                    (vheight + iheight) / 2);
+        } else {
+            float scale = Math.min(scale_height, scale_width);
+            mOverlayIcon.setBounds((int) (vwidth - scale * iwidth) / 2,
+                    (int) (vheight - scale * iheight) / 2,
+                    (int) (vwidth + scale * iwidth) / 2,
+                    (int) (vheight + scale * iheight) / 2);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (changed && getScaleType() == ScaleType.MATRIX) {
+            updateDrawMatrix();
+        }
+        if (mShowOverlayIcon && changed && mOverlayIcon != null) {
+            updateOverlayIconBounds();
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mShowOverlayIcon && mOverlayIcon != null) {
+            mOverlayIcon.draw(canvas);
+        }
+    }
+
+    protected void onMtpImageDataFetchedFromDevice(Object result) {
+        BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata)result;
+        if (getScaleType() == ScaleType.MATRIX) {
+            mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight();
+            mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth();
+            mLastRotationDegrees = bitmapWithMetadata.rotationDegrees;
+            updateDrawMatrix();
+        } else {
+            setRotation(bitmapWithMetadata.rotationDegrees);
+        }
+        setAlpha(0f);
+        setImageBitmap(bitmapWithMetadata.bitmap);
+        animate().alpha(1f);
+    }
+
+    protected void cancelLoadingAndClear() {
+        synchronized (mFetchLock) {
+            mFetchDevice = null;
+            mFetchObjectInfo = null;
+            mFetchResult = null;
+        }
+        animate().cancel();
+        setImageResource(android.R.color.transparent);
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        cancelLoadingAndClear();
+        super.onDetachedFromWindow();
+    }
+
+    private static class FetchImageHandler extends Handler {
+        public FetchImageHandler(Looper l) {
+            super(l);
+        }
+
+        public static FetchImageHandler createOnNewThread() {
+            HandlerThread t = new HandlerThread("MtpImageView Fetch");
+            t.start();
+            return new FetchImageHandler(t.getLooper());
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            @SuppressWarnings("unchecked")
+            MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+            if (parent == null) return;
+            MtpObjectInfo objectInfo;
+            MtpDevice device;
+            synchronized (parent.mFetchLock) {
+                parent.mFetchPending = false;
+                device = parent.mFetchDevice;
+                objectInfo = parent.mFetchObjectInfo;
+            }
+            if (device == null) return;
+            Object result = parent.fetchMtpImageDataFromDevice(device, objectInfo);
+            if (result == null) return;
+            synchronized (parent.mFetchLock) {
+                if (parent.mFetchObjectInfo != objectInfo) return;
+                parent.mFetchResult = result;
+                parent.mFetchDevice = null;
+                parent.mFetchObjectInfo = null;
+                sFetchCompleteHandler.sendMessage(
+                        sFetchCompleteHandler.obtainMessage(0, parent.mWeakReference));
+            }
+        }
+    }
+
+    private static class ShowImageHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            @SuppressWarnings("unchecked")
+            MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+            if (parent == null) return;
+            Object result;
+            synchronized (parent.mFetchLock) {
+                result = parent.mFetchResult;
+            }
+            if (result == null) return;
+            parent.onMtpImageDataFetchedFromDevice(result);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java
new file mode 100644
index 0000000..3307e78
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.ingest.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+
+
+public class MtpThumbnailTileView extends MtpImageView implements Checkable {
+
+    private Paint mForegroundPaint;
+    private boolean mIsChecked;
+    private Bitmap mBitmap;
+
+    private void init() {
+        mForegroundPaint = new Paint();
+        mForegroundPaint.setColor(getResources().getColor(R.color.ingest_highlight_semitransparent));
+    }
+
+    public MtpThumbnailTileView(Context context) {
+        super(context);
+        init();
+    }
+
+    public MtpThumbnailTileView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public MtpThumbnailTileView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Force this to be square
+        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+    }
+
+    @Override
+    protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
+        return MtpBitmapFetch.getThumbnail(device, info);
+    }
+
+    @Override
+    protected void onMtpImageDataFetchedFromDevice(Object result) {
+        mBitmap = (Bitmap)result;
+        setImageBitmap(mBitmap);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        if (isChecked()) {
+            canvas.drawRect(canvas.getClipBounds(), mForegroundPaint);
+        }
+    }
+
+    @Override
+    public boolean isChecked() {
+        return mIsChecked;
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        mIsChecked = checked;
+    }
+
+    @Override
+    public void toggle() {
+        setChecked(!mIsChecked);
+    }
+
+    @Override
+    protected void cancelLoadingAndClear() {
+        super.cancelLoadingAndClear();
+        if (mBitmap != null) {
+            MtpBitmapFetch.recycleThumbnail(mBitmap);
+            mBitmap = null;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java b/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java
new file mode 100644
index 0000000..ef26b1b
--- /dev/null
+++ b/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.onetimeinitializer;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.gadget.WidgetDatabaseHelper;
+import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This one-timer migrates local-album gallery app widgets from old paths from prior releases 
+ * to updated paths in the current build version. This migration is needed because of
+ * bucket ID (i.e., directory hash) change in JB and JB MR1 (The external storage path has changed
+ * from /mnt/sdcard in pre-JB releases, to /storage/sdcard0 in JB, then again
+ * to /external/storage/sdcard/0 in JB MR1).
+ */
+public class GalleryWidgetMigrator {
+    private static final String TAG = "GalleryWidgetMigrator";
+    private static final String PRE_JB_EXT_PATH = "/mnt/sdcard";
+    private static final String JB_EXT_PATH = "/storage/sdcard0";
+    private static final String NEW_EXT_PATH =
+            Environment.getExternalStorageDirectory().getAbsolutePath();
+    private static final int RELATIVE_PATH_START = NEW_EXT_PATH.length();
+    private static final String KEY_EXT_PATH = "external_storage_path";
+
+    /**
+     * Migrates local-album gallery widgets from prior releases to current release
+     * due to bucket ID (i.e., directory hash) change.
+     */
+    public static void migrateGalleryWidgets(Context context) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        // Migration is only needed when external storage path has changed
+        String extPath = prefs.getString(KEY_EXT_PATH, null);
+        boolean isDone = NEW_EXT_PATH.equals(extPath);
+        if (isDone) return;
+
+        try {
+            migrateGalleryWidgetsInternal(context);
+            prefs.edit().putString(KEY_EXT_PATH, NEW_EXT_PATH).commit();
+        } catch (Throwable t) {
+            // exception may be thrown if external storage is not available(?)
+            Log.w(TAG, "migrateGalleryWidgets", t);
+        }
+    }
+
+    private static void migrateGalleryWidgetsInternal(Context context) {
+        GalleryApp galleryApp = (GalleryApp) context.getApplicationContext();
+        DataManager manager = galleryApp.getDataManager();
+        WidgetDatabaseHelper dbHelper = new WidgetDatabaseHelper(context);
+
+        // only need to migrate local-album entries of type TYPE_ALBUM
+        List<Entry> entries = dbHelper.getEntries(WidgetDatabaseHelper.TYPE_ALBUM);
+        if (entries == null) return;
+
+        // Check each entry's relativePath. If exists, update bucket id using relative
+        // path combined with external storage path. Otherwise, iterate through old external
+        // storage paths to find the relative path that matches the old bucket id, and then update
+        // bucket id and relative path
+        HashMap<Integer, Entry> localEntries = new HashMap<Integer, Entry>(entries.size());
+        for (Entry entry : entries) {
+            Path path = Path.fromString(entry.albumPath);
+            MediaSet mediaSet = (MediaSet) manager.getMediaObject(path);
+            if (mediaSet instanceof LocalAlbum) {
+                if (entry.relativePath != null && entry.relativePath.length() > 0) {
+                    // update entry using relative path + external storage path
+                    updateEntryUsingRelativePath(entry, dbHelper);
+                } else {
+                    int bucketId = Integer.parseInt(path.getSuffix());
+                    localEntries.put(bucketId, entry);
+                }
+            }
+        }
+        if (!localEntries.isEmpty()) migrateLocalEntries(context, localEntries, dbHelper);
+    }
+
+    private static void migrateLocalEntries(Context context,
+            HashMap<Integer, Entry> entries, WidgetDatabaseHelper dbHelper) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        String oldExtPath = prefs.getString(KEY_EXT_PATH, null);
+        if (oldExtPath != null) {
+            migrateLocalEntries(entries, dbHelper, oldExtPath);
+            return;
+        }
+        // If old external storage path is unknown, it could be either Pre-JB or JB version
+        // we need to try both.
+        migrateLocalEntries(entries, dbHelper, PRE_JB_EXT_PATH);
+        if (!entries.isEmpty() &&
+                Build.VERSION.SDK_INT > ApiHelper.VERSION_CODES.JELLY_BEAN) {
+            migrateLocalEntries(entries, dbHelper, JB_EXT_PATH);
+        }
+    }
+
+    private static void migrateLocalEntries(HashMap<Integer, Entry> entries,
+             WidgetDatabaseHelper dbHelper, String oldExtPath) {
+        File root = Environment.getExternalStorageDirectory();
+        // check the DCIM directory first; this should take care of 99% use cases
+        updatePath(new File(root, "DCIM"), entries, dbHelper, oldExtPath);
+        // check other directories if DCIM doesn't cut it
+        if (!entries.isEmpty()) updatePath(root, entries, dbHelper, oldExtPath);
+    }
+    private static void updatePath(File root, HashMap<Integer, Entry> entries,
+            WidgetDatabaseHelper dbHelper, String oldExtStorage) {
+        File[] files = root.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                if (file.isDirectory() && !entries.isEmpty()) {
+                    String path = file.getAbsolutePath();
+                    String oldPath = oldExtStorage + path.substring(RELATIVE_PATH_START);
+                    int oldBucketId = GalleryUtils.getBucketId(oldPath);
+                    Entry entry = entries.remove(oldBucketId);
+                    if (entry != null) {
+                        int newBucketId = GalleryUtils.getBucketId(path);
+                        String newAlbumPath = Path.fromString(entry.albumPath)
+                                .getParent()
+                                .getChild(newBucketId)
+                                .toString();
+                        Log.d(TAG, "migrate from " + entry.albumPath + " to " + newAlbumPath);
+                        entry.albumPath = newAlbumPath;
+                        // update entry's relative path
+                        entry.relativePath = path.substring(RELATIVE_PATH_START);
+                        dbHelper.updateEntry(entry);
+                    }
+                    updatePath(file, entries, dbHelper, oldExtStorage); // recursion
+                }
+            }
+        }
+    }
+
+    private static void updateEntryUsingRelativePath(Entry entry, WidgetDatabaseHelper dbHelper) {
+        String newPath = NEW_EXT_PATH + entry.relativePath;
+        int newBucketId = GalleryUtils.getBucketId(newPath);
+        String newAlbumPath = Path.fromString(entry.albumPath)
+                .getParent()
+                .getChild(newBucketId)
+                .toString();
+        entry.albumPath = newAlbumPath;
+        dbHelper.updateEntry(entry);
+    }
+}
diff --git a/src/com/android/gallery3d/provider/GalleryProvider.java b/src/com/android/gallery3d/provider/GalleryProvider.java
new file mode 100644
index 0000000..d6c7ccd
--- /dev/null
+++ b/src/com/android/gallery3d/provider/GalleryProvider.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.AsyncTaskUtil;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class GalleryProvider extends ContentProvider {
+    private static final String TAG = "GalleryProvider";
+
+    public static final String AUTHORITY = "com.android.gallery3d.provider";
+    public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+
+    public static interface PicasaColumns {
+        public static final String USER_ACCOUNT = "user_account";
+        public static final String PICASA_ID = "picasa_id";
+    }
+
+    private static final String[] SUPPORTED_PICASA_COLUMNS = {
+            PicasaColumns.USER_ACCOUNT,
+            PicasaColumns.PICASA_ID,
+            ImageColumns.DISPLAY_NAME,
+            ImageColumns.SIZE,
+            ImageColumns.MIME_TYPE,
+            ImageColumns.DATE_TAKEN,
+            ImageColumns.LATITUDE,
+            ImageColumns.LONGITUDE,
+            ImageColumns.ORIENTATION};
+
+    private DataManager mDataManager;
+    private static Uri sBaseUri;
+
+    public static String getAuthority(Context context) {
+        return context.getPackageName() + ".provider";
+    }
+
+    public static Uri getUriFor(Context context, Path path) {
+        if (sBaseUri == null) {
+            sBaseUri = Uri.parse("content://" + context.getPackageName() + ".provider");
+        }
+        return sBaseUri.buildUpon()
+                .appendEncodedPath(path.toString().substring(1)) // ignore the leading '/'
+                .build();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    // TODO: consider concurrent access
+    @Override
+    public String getType(Uri uri) {
+        long token = Binder.clearCallingIdentity();
+        try {
+            Path path = Path.fromString(uri.getPath());
+            MediaItem item = (MediaItem) mDataManager.getMediaObject(path);
+            return item != null ? item.getMimeType() : null;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean onCreate() {
+        GalleryApp app = (GalleryApp) getContext().getApplicationContext();
+        mDataManager = app.getDataManager();
+        return true;
+    }
+
+    // TODO: consider concurrent access
+    @Override
+    public Cursor query(Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        long token = Binder.clearCallingIdentity();
+        try {
+            Path path = Path.fromString(uri.getPath());
+            MediaObject object = mDataManager.getMediaObject(path);
+            if (object == null) {
+                Log.w(TAG, "cannot find: " + uri);
+                return null;
+            }
+            if (PicasaSource.isPicasaImage(object)) {
+                return queryPicasaItem(object,
+                        projection, selection, selectionArgs, sortOrder);
+            } else {
+                    return null;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private Cursor queryPicasaItem(MediaObject image, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        if (projection == null) projection = SUPPORTED_PICASA_COLUMNS;
+        Object[] columnValues = new Object[projection.length];
+        double latitude = PicasaSource.getLatitude(image);
+        double longitude = PicasaSource.getLongitude(image);
+        boolean isValidLatlong = GalleryUtils.isValidLocation(latitude, longitude);
+
+        for (int i = 0, n = projection.length; i < n; ++i) {
+            String column = projection[i];
+            if (PicasaColumns.USER_ACCOUNT.equals(column)) {
+                columnValues[i] = PicasaSource.getUserAccount(getContext(), image);
+            } else if (PicasaColumns.PICASA_ID.equals(column)) {
+                columnValues[i] = PicasaSource.getPicasaId(image);
+            } else if (ImageColumns.DISPLAY_NAME.equals(column)) {
+                columnValues[i] = PicasaSource.getImageTitle(image);
+            } else if (ImageColumns.SIZE.equals(column)){
+                columnValues[i] = PicasaSource.getImageSize(image);
+            } else if (ImageColumns.MIME_TYPE.equals(column)) {
+                columnValues[i] = PicasaSource.getContentType(image);
+            } else if (ImageColumns.DATE_TAKEN.equals(column)) {
+                columnValues[i] = PicasaSource.getDateTaken(image);
+            } else if (ImageColumns.LATITUDE.equals(column)) {
+                columnValues[i] = isValidLatlong ? latitude : null;
+            } else if (ImageColumns.LONGITUDE.equals(column)) {
+                columnValues[i] = isValidLatlong ? longitude : null;
+            } else if (ImageColumns.ORIENTATION.equals(column)) {
+                columnValues[i] = PicasaSource.getRotation(image);
+            } else {
+                Log.w(TAG, "unsupported column: " + column);
+            }
+        }
+        MatrixCursor cursor = new MatrixCursor(projection);
+        cursor.addRow(columnValues);
+        return cursor;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode)
+            throws FileNotFoundException {
+        long token = Binder.clearCallingIdentity();
+        try {
+            if (mode.contains("w")) {
+                throw new FileNotFoundException("cannot open file for write");
+            }
+            Path path = Path.fromString(uri.getPath());
+            MediaObject object = mDataManager.getMediaObject(path);
+            if (object == null) {
+                throw new FileNotFoundException(uri.toString());
+            }
+            if (PicasaSource.isPicasaImage(object)) {
+                return PicasaSource.openFile(getContext(), object, mode);
+            } else {
+                throw new FileNotFoundException("unspported type: " + object);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    private static interface PipeDataWriter<T> {
+        void writeDataToPipe(ParcelFileDescriptor output, T args);
+    }
+
+    // Modified from ContentProvider.openPipeHelper. We are target at API LEVEL 10.
+    // But openPipeHelper is available in API LEVEL 11.
+    private static <T> ParcelFileDescriptor openPipeHelper(
+            final T args, final PipeDataWriter<T> func) throws FileNotFoundException {
+        try {
+            final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+            AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() {
+                @Override
+                protected Object doInBackground(Object... params) {
+                    try {
+                        func.writeDataToPipe(pipe[1], args);
+                        return null;
+                    } finally {
+                        Utils.closeSilently(pipe[1]);
+                    }
+                }
+            };
+            AsyncTaskUtil.executeInParallel(task, (Object[]) null);
+            return pipe[0];
+        } catch (IOException e) {
+            throw new FileNotFoundException("failure making pipe");
+        }
+    }
+
+}
diff --git a/src/com/android/gallery3d/ui/AbstractSlotRenderer.java b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
new file mode 100644
index 0000000..729439d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.FadeOutTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.Texture;
+
+public abstract class AbstractSlotRenderer implements SlotView.SlotRenderer {
+
+    private final ResourceTexture mVideoOverlay;
+    private final ResourceTexture mVideoPlayIcon;
+    private final ResourceTexture mPanoramaIcon;
+    private final NinePatchTexture mFramePressed;
+    private final NinePatchTexture mFrameSelected;
+    private FadeOutTexture mFramePressedUp;
+
+    protected AbstractSlotRenderer(Context context) {
+        mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb);
+        mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play);
+        mPanoramaIcon = new ResourceTexture(context, R.drawable.ic_360pano_holo_light);
+        mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
+        mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+    }
+
+    protected void drawContent(GLCanvas canvas,
+            Texture content, int width, int height, int rotation) {
+        canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+        // The content is always rendered in to the largest square that fits
+        // inside the slot, aligned to the top of the slot.
+        width = height = Math.min(width, height);
+        if (rotation != 0) {
+            canvas.translate(width / 2, height / 2);
+            canvas.rotate(rotation, 0, 0, 1);
+            canvas.translate(-width / 2, -height / 2);
+        }
+
+        // Fit the content into the box
+        float scale = Math.min(
+                (float) width / content.getWidth(),
+                (float) height / content.getHeight());
+        canvas.scale(scale, scale, 1);
+        content.draw(canvas, 0, 0);
+
+        canvas.restore();
+    }
+
+    protected void drawVideoOverlay(GLCanvas canvas, int width, int height) {
+        // Scale the video overlay to the height of the thumbnail and put it
+        // on the left side.
+        ResourceTexture v = mVideoOverlay;
+        float scale = (float) height / v.getHeight();
+        int w = Math.round(scale * v.getWidth());
+        int h = Math.round(scale * v.getHeight());
+        v.draw(canvas, 0, 0, w, h);
+
+        int s = Math.min(width, height) / 6;
+        mVideoPlayIcon.draw(canvas, (width - s) / 2, (height - s) / 2, s, s);
+    }
+
+    protected void drawPanoramaIcon(GLCanvas canvas, int width, int height) {
+        int iconSize = Math.min(width, height) / 6;
+        mPanoramaIcon.draw(canvas, (width - iconSize) / 2, (height - iconSize) / 2,
+                iconSize, iconSize);
+    }
+
+    protected boolean isPressedUpFrameFinished() {
+        if (mFramePressedUp != null) {
+            if (mFramePressedUp.isAnimating()) {
+                return false;
+            } else {
+                mFramePressedUp = null;
+            }
+        }
+        return true;
+    }
+
+    protected void drawPressedUpFrame(GLCanvas canvas, int width, int height) {
+        if (mFramePressedUp == null) {
+            mFramePressedUp = new FadeOutTexture(mFramePressed);
+        }
+        drawFrame(canvas, mFramePressed.getPaddings(), mFramePressedUp, 0, 0, width, height);
+    }
+
+    protected void drawPressedFrame(GLCanvas canvas, int width, int height) {
+        drawFrame(canvas, mFramePressed.getPaddings(), mFramePressed, 0, 0, width, height);
+    }
+
+    protected void drawSelectedFrame(GLCanvas canvas, int width, int height) {
+        drawFrame(canvas, mFrameSelected.getPaddings(), mFrameSelected, 0, 0, width, height);
+    }
+
+    protected static void drawFrame(GLCanvas canvas, Rect padding, Texture frame,
+            int x, int y, int width, int height) {
+        frame.draw(canvas, x - padding.left, y - padding.top, width + padding.left + padding.right,
+                 height + padding.top + padding.bottom);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
new file mode 100644
index 0000000..6b4f103
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.os.Handler;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ShareActionProvider;
+import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.MenuExecutor.ProgressListener;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+
+public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "ActionModeHandler";
+
+    private static final int MAX_SELECTED_ITEMS_FOR_SHARE_INTENT = 300;
+    private static final int MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT = 10;
+
+    private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE
+            | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE
+            | MediaObject.SUPPORT_CACHE;
+
+    public interface ActionModeListener {
+        public boolean onActionItemClicked(MenuItem item);
+    }
+
+    private final AbstractGalleryActivity mActivity;
+    private final MenuExecutor mMenuExecutor;
+    private final SelectionManager mSelectionManager;
+    private final NfcAdapter mNfcAdapter;
+    private Menu mMenu;
+    private MenuItem mSharePanoramaMenuItem;
+    private MenuItem mShareMenuItem;
+    private ShareActionProvider mSharePanoramaActionProvider;
+    private ShareActionProvider mShareActionProvider;
+    private SelectionMenu mSelectionMenu;
+    private ActionModeListener mListener;
+    private Future<?> mMenuTask;
+    private final Handler mMainHandler;
+    private ActionMode mActionMode;
+
+    private static class GetAllPanoramaSupports implements PanoramaSupportCallback {
+        private int mNumInfoRequired;
+        private JobContext mJobContext;
+        public boolean mAllPanoramas = true;
+        public boolean mAllPanorama360 = true;
+        public boolean mHasPanorama360 = false;
+        private Object mLock = new Object();
+
+        public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) {
+            mJobContext = jc;
+            mNumInfoRequired = mediaObjects.size();
+            for (MediaObject mediaObject : mediaObjects) {
+                mediaObject.getPanoramaSupport(this);
+            }
+        }
+
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            synchronized (mLock) {
+                mNumInfoRequired--;
+                mAllPanoramas = isPanorama && mAllPanoramas;
+                mAllPanorama360 = isPanorama360 && mAllPanorama360;
+                mHasPanorama360 = mHasPanorama360 || isPanorama360;
+                if (mNumInfoRequired == 0 || mJobContext.isCancelled()) {
+                    mLock.notifyAll();
+                }
+            }
+        }
+
+        public void waitForPanoramaSupport() {
+            synchronized (mLock) {
+                while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) {
+                    try {
+                        mLock.wait();
+                    } catch (InterruptedException e) {
+                        // May be a cancelled job context
+                    }
+                }
+            }
+        }
+    }
+
+    public ActionModeHandler(
+            AbstractGalleryActivity activity, SelectionManager selectionManager) {
+        mActivity = Utils.checkNotNull(activity);
+        mSelectionManager = Utils.checkNotNull(selectionManager);
+        mMenuExecutor = new MenuExecutor(activity, selectionManager);
+        mMainHandler = new Handler(activity.getMainLooper());
+        mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
+    }
+
+    public void startActionMode() {
+        Activity a = mActivity;
+        mActionMode = a.startActionMode(this);
+        View customView = LayoutInflater.from(a).inflate(
+                R.layout.action_mode, null);
+        mActionMode.setCustomView(customView);
+        mSelectionMenu = new SelectionMenu(a,
+                (Button) customView.findViewById(R.id.selection_menu), this);
+        updateSelectionMenu();
+    }
+
+    public void finishActionMode() {
+        mActionMode.finish();
+    }
+
+    public void setTitle(String title) {
+        mSelectionMenu.setTitle(title);
+    }
+
+    public void setActionModeListener(ActionModeListener listener) {
+        mListener = listener;
+    }
+
+    private WakeLockHoldingProgressListener mDeleteProgressListener;
+
+    @Override
+    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+        GLRoot root = mActivity.getGLRoot();
+        root.lockRenderThread();
+        try {
+            boolean result;
+            // Give listener a chance to process this command before it's routed to
+            // ActionModeHandler, which handles command only based on the action id.
+            // Sometimes the listener may have more background information to handle
+            // an action command.
+            if (mListener != null) {
+                result = mListener.onActionItemClicked(item);
+                if (result) {
+                    mSelectionManager.leaveSelectionMode();
+                    return result;
+                }
+            }
+            ProgressListener listener = null;
+            String confirmMsg = null;
+            int action = item.getItemId();
+            if (action == R.id.action_delete) {
+                confirmMsg = mActivity.getResources().getQuantityString(
+                        R.plurals.delete_selection, mSelectionManager.getSelectedCount());
+                if (mDeleteProgressListener == null) {
+                    mDeleteProgressListener = new WakeLockHoldingProgressListener(mActivity,
+                            "Gallery Delete Progress Listener");
+                }
+                listener = mDeleteProgressListener;
+            }
+            mMenuExecutor.onMenuClicked(item, confirmMsg, listener);
+        } finally {
+            root.unlockRenderThread();
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onPopupItemClick(int itemId) {
+        GLRoot root = mActivity.getGLRoot();
+        root.lockRenderThread();
+        try {
+            if (itemId == R.id.action_select_all) {
+                updateSupportedOperation();
+                mMenuExecutor.onMenuClicked(itemId, null, false, true);
+            }
+            return true;
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    private void updateSelectionMenu() {
+        // update title
+        int count = mSelectionManager.getSelectedCount();
+        String format = mActivity.getResources().getQuantityString(
+                R.plurals.number_of_items_selected, count);
+        setTitle(String.format(format, count));
+
+        // For clients who call SelectionManager.selectAll() directly, we need to ensure the
+        // menu status is consistent with selection manager.
+        mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode());
+    }
+
+    private final OnShareTargetSelectedListener mShareTargetSelectedListener =
+            new OnShareTargetSelectedListener() {
+        @Override
+        public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
+            mSelectionManager.leaveSelectionMode();
+            return false;
+        }
+    };
+
+    @Override
+    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+        return false;
+    }
+
+    @Override
+    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+        mode.getMenuInflater().inflate(R.menu.operation, menu);
+
+        mMenu = menu;
+        mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama);
+        if (mSharePanoramaMenuItem != null) {
+            mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem
+                .getActionProvider();
+            mSharePanoramaActionProvider.setOnShareTargetSelectedListener(
+                    mShareTargetSelectedListener);
+            mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml");
+        }
+        mShareMenuItem = menu.findItem(R.id.action_share);
+        if (mShareMenuItem != null) {
+            mShareActionProvider = (ShareActionProvider) mShareMenuItem
+                .getActionProvider();
+            mShareActionProvider.setOnShareTargetSelectedListener(
+                    mShareTargetSelectedListener);
+            mShareActionProvider.setShareHistoryFileName("share_history.xml");
+        }
+        return true;
+    }
+
+    @Override
+    public void onDestroyActionMode(ActionMode mode) {
+        mSelectionManager.leaveSelectionMode();
+    }
+
+    private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) {
+        ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false);
+        if (unexpandedPaths.isEmpty()) {
+            // This happens when starting selection mode from overflow menu
+            // (instead of long press a media object)
+            return null;
+        }
+        ArrayList<MediaObject> selected = new ArrayList<MediaObject>();
+        DataManager manager = mActivity.getDataManager();
+        for (Path path : unexpandedPaths) {
+            if (jc.isCancelled()) {
+                return null;
+            }
+            selected.add(manager.getMediaObject(path));
+        }
+
+        return selected;
+    }
+    // Menu options are determined by selection set itself.
+    // We cannot expand it because MenuExecuter executes it based on
+    // the selection set instead of the expanded result.
+    // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
+    private int computeMenuOptions(ArrayList<MediaObject> selected) {
+        int operation = MediaObject.SUPPORT_ALL;
+        int type = 0;
+        for (MediaObject mediaObject: selected) {
+            int support = mediaObject.getSupportedOperations();
+            type |= mediaObject.getMediaType();
+            operation &= support;
+        }
+
+        switch (selected.size()) {
+            case 1:
+                final String mimeType = MenuExecutor.getMimeType(type);
+                if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) {
+                    operation &= ~MediaObject.SUPPORT_EDIT;
+                }
+                break;
+            default:
+                operation &= SUPPORT_MULTIPLE_MASK;
+        }
+
+        return operation;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    private void setNfcBeamPushUris(Uri[] uris) {
+        if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
+            mNfcAdapter.setBeamPushUrisCallback(null, mActivity);
+            mNfcAdapter.setBeamPushUris(uris, mActivity);
+        }
+    }
+
+    // Share intent needs to expand the selection set so we can get URI of
+    // each media item
+    private Intent computePanoramaSharingIntent(JobContext jc, int maxItems) {
+        ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems);
+        if (expandedPaths == null || expandedPaths.size() == 0) {
+            return new Intent();
+        }
+        final ArrayList<Uri> uris = new ArrayList<Uri>();
+        DataManager manager = mActivity.getDataManager();
+        final Intent intent = new Intent();
+        for (Path path : expandedPaths) {
+            if (jc.isCancelled()) return null;
+            uris.add(manager.getContentUri(path));
+        }
+
+        final int size = uris.size();
+        if (size > 0) {
+            if (size > 1) {
+                intent.setAction(Intent.ACTION_SEND_MULTIPLE);
+                intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
+                intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+            } else {
+                intent.setAction(Intent.ACTION_SEND);
+                intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
+                intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+            }
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        }
+
+        return intent;
+    }
+
+    private Intent computeSharingIntent(JobContext jc, int maxItems) {
+        ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems);
+        if (expandedPaths == null || expandedPaths.size() == 0) {
+            setNfcBeamPushUris(null);
+            return new Intent();
+        }
+        final ArrayList<Uri> uris = new ArrayList<Uri>();
+        DataManager manager = mActivity.getDataManager();
+        int type = 0;
+        final Intent intent = new Intent();
+        for (Path path : expandedPaths) {
+            if (jc.isCancelled()) return null;
+            int support = manager.getSupportedOperations(path);
+            type |= manager.getMediaType(path);
+
+            if ((support & MediaObject.SUPPORT_SHARE) != 0) {
+                uris.add(manager.getContentUri(path));
+            }
+        }
+
+        final int size = uris.size();
+        if (size > 0) {
+            final String mimeType = MenuExecutor.getMimeType(type);
+            if (size > 1) {
+                intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType);
+                intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+            } else {
+                intent.setAction(Intent.ACTION_SEND).setType(mimeType);
+                intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+            }
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            setNfcBeamPushUris(uris.toArray(new Uri[uris.size()]));
+        } else {
+            setNfcBeamPushUris(null);
+        }
+
+        return intent;
+    }
+
+    public void updateSupportedOperation(Path path, boolean selected) {
+        // TODO: We need to improve the performance
+        updateSupportedOperation();
+    }
+
+    public void updateSupportedOperation() {
+        // Interrupt previous unfinished task, mMenuTask is only accessed in main thread
+        if (mMenuTask != null) mMenuTask.cancel();
+
+        updateSelectionMenu();
+
+        // Disable share actions until share intent is in good shape
+        if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false);
+        if (mShareMenuItem != null) mShareMenuItem.setEnabled(false);
+
+        // Generate sharing intent and update supported operations in the background
+        // The task can take a long time and be canceled in the mean time.
+        mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
+            @Override
+            public Void run(final JobContext jc) {
+                // Pass1: Deal with unexpanded media object list for menu operation.
+                ArrayList<MediaObject> selected = getSelectedMediaObjects(jc);
+                if (selected == null) {
+                    mMainHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mMenuTask = null;
+                            if (jc.isCancelled()) return;
+                            // Disable all the operations when no item is selected
+                            MenuExecutor.updateMenuOperation(mMenu, 0);
+                        }
+                    });
+                    return null;
+                }
+                final int operation = computeMenuOptions(selected);
+                if (jc.isCancelled()) {
+                    return null;
+                }
+                int numSelected = selected.size();
+                final boolean canSharePanoramas =
+                        numSelected < MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT;
+                final boolean canShare =
+                        numSelected < MAX_SELECTED_ITEMS_FOR_SHARE_INTENT;
+
+                final GetAllPanoramaSupports supportCallback = canSharePanoramas ?
+                        new GetAllPanoramaSupports(selected, jc)
+                        : null;
+
+                // Pass2: Deal with expanded media object list for sharing operation.
+                final Intent share_panorama_intent = canSharePanoramas ?
+                        computePanoramaSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT)
+                        : new Intent();
+                final Intent share_intent = canShare ?
+                        computeSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT)
+                        : new Intent();
+
+                if (canSharePanoramas) {
+                    supportCallback.waitForPanoramaSupport();
+                }
+                if (jc.isCancelled()) {
+                    return null;
+                }
+                mMainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mMenuTask = null;
+                        if (jc.isCancelled()) return;
+                        MenuExecutor.updateMenuOperation(mMenu, operation);
+                        MenuExecutor.updateMenuForPanorama(mMenu,
+                                canSharePanoramas && supportCallback.mAllPanorama360,
+                                canSharePanoramas && supportCallback.mHasPanorama360);
+                        if (mSharePanoramaMenuItem != null) {
+                            mSharePanoramaMenuItem.setEnabled(true);
+                            if (canSharePanoramas && supportCallback.mAllPanorama360) {
+                                mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+                                mShareMenuItem.setTitle(
+                                    mActivity.getResources().getString(R.string.share_as_photo));
+                            } else {
+                                mSharePanoramaMenuItem.setVisible(false);
+                                mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+                                mShareMenuItem.setTitle(
+                                    mActivity.getResources().getString(R.string.share));
+                            }
+                            mSharePanoramaActionProvider.setShareIntent(share_panorama_intent);
+                        }
+                        if (mShareMenuItem != null) {
+                            mShareMenuItem.setEnabled(canShare);
+                            mShareActionProvider.setShareIntent(share_intent);
+                        }
+                    }
+                });
+                return null;
+            }
+        });
+    }
+
+    public void pause() {
+        if (mMenuTask != null) {
+            mMenuTask.cancel();
+            mMenuTask = null;
+        }
+        mMenuExecutor.pause();
+    }
+
+    public void destroy() {
+        mMenuExecutor.destroy();
+    }
+
+    public void resume() {
+        if (mSelectionManager.inSelectionMode()) updateSupportedOperation();
+        mMenuExecutor.resume();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumLabelMaker.java b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
new file mode 100644
index 0000000..da1cac0
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class AlbumLabelMaker {
+    private static final int BORDER_SIZE = 0;
+
+    private final AlbumSetSlotRenderer.LabelSpec mSpec;
+    private final TextPaint mTitlePaint;
+    private final TextPaint mCountPaint;
+    private final Context mContext;
+
+    private int mLabelWidth;
+    private int mBitmapWidth;
+    private int mBitmapHeight;
+
+    private final LazyLoadedBitmap mLocalSetIcon;
+    private final LazyLoadedBitmap mPicasaIcon;
+    private final LazyLoadedBitmap mCameraIcon;
+
+    public AlbumLabelMaker(Context context, AlbumSetSlotRenderer.LabelSpec spec) {
+        mContext = context;
+        mSpec = spec;
+        mTitlePaint = getTextPaint(spec.titleFontSize, spec.titleColor, false);
+        mCountPaint = getTextPaint(spec.countFontSize, spec.countColor, false);
+
+        mLocalSetIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_folder);
+        mPicasaIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_picasa);
+        mCameraIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_camera);
+    }
+
+    public static int getBorderSize() {
+        return BORDER_SIZE;
+    }
+
+    private Bitmap getOverlayAlbumIcon(int sourceType) {
+        switch (sourceType) {
+            case DataSourceType.TYPE_CAMERA:
+                return mCameraIcon.get();
+            case DataSourceType.TYPE_LOCAL:
+                return mLocalSetIcon.get();
+            case DataSourceType.TYPE_PICASA:
+                return mPicasaIcon.get();
+        }
+        return null;
+    }
+
+    private static TextPaint getTextPaint(int textSize, int color, boolean isBold) {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(textSize);
+        paint.setAntiAlias(true);
+        paint.setColor(color);
+        //paint.setShadowLayer(2f, 0f, 0f, Color.LTGRAY);
+        if (isBold) {
+            paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+        }
+        return paint;
+    }
+
+    private class LazyLoadedBitmap {
+        private Bitmap mBitmap;
+        private int mResId;
+
+        public LazyLoadedBitmap(int resId) {
+            mResId = resId;
+        }
+
+        public synchronized Bitmap get() {
+            if (mBitmap == null) {
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+                mBitmap = BitmapFactory.decodeResource(
+                        mContext.getResources(), mResId, options);
+            }
+            return mBitmap;
+        }
+    }
+
+    public synchronized void setLabelWidth(int width) {
+        if (mLabelWidth == width) return;
+        mLabelWidth = width;
+        int borders = 2 * BORDER_SIZE;
+        mBitmapWidth = width + borders;
+        mBitmapHeight = mSpec.labelBackgroundHeight + borders;
+    }
+
+    public ThreadPool.Job<Bitmap> requestLabel(
+            String title, String count, int sourceType) {
+        return new AlbumLabelJob(title, count, sourceType);
+    }
+
+    static void drawText(Canvas canvas,
+            int x, int y, String text, int lengthLimit, TextPaint p) {
+        // The TextPaint cannot be used concurrently
+        synchronized (p) {
+            text = TextUtils.ellipsize(
+                    text, p, lengthLimit, TextUtils.TruncateAt.END).toString();
+            canvas.drawText(text, x, y - p.getFontMetricsInt().ascent, p);
+        }
+    }
+
+    private class AlbumLabelJob implements ThreadPool.Job<Bitmap> {
+        private final String mTitle;
+        private final String mCount;
+        private final int mSourceType;
+
+        public AlbumLabelJob(String title, String count, int sourceType) {
+            mTitle = title;
+            mCount = count;
+            mSourceType = sourceType;
+        }
+
+        @Override
+        public Bitmap run(JobContext jc) {
+            AlbumSetSlotRenderer.LabelSpec s = mSpec;
+
+            String title = mTitle;
+            String count = mCount;
+            Bitmap icon = getOverlayAlbumIcon(mSourceType);
+
+            Bitmap bitmap;
+            int labelWidth;
+
+            synchronized (this) {
+                labelWidth = mLabelWidth;
+                bitmap = GalleryBitmapPool.getInstance().get(mBitmapWidth, mBitmapHeight);
+            }
+
+            if (bitmap == null) {
+                int borders = 2 * BORDER_SIZE;
+                bitmap = Bitmap.createBitmap(labelWidth + borders,
+                        s.labelBackgroundHeight + borders, Config.ARGB_8888);
+            }
+
+            Canvas canvas = new Canvas(bitmap);
+            canvas.clipRect(BORDER_SIZE, BORDER_SIZE,
+                    bitmap.getWidth() - BORDER_SIZE,
+                    bitmap.getHeight() - BORDER_SIZE);
+            canvas.drawColor(mSpec.backgroundColor, PorterDuff.Mode.SRC);
+
+            canvas.translate(BORDER_SIZE, BORDER_SIZE);
+
+            // draw title
+            if (jc.isCancelled()) return null;
+            int x = s.leftMargin + s.iconSize;
+            // TODO: is the offset relevant in new reskin?
+            // int y = s.titleOffset;
+            int y = (s.labelBackgroundHeight - s.titleFontSize) / 2;
+            drawText(canvas, x, y, title, labelWidth - s.leftMargin - x - 
+                    s.titleRightMargin, mTitlePaint);
+
+            // draw count
+            if (jc.isCancelled()) return null;
+            x = labelWidth - s.titleRightMargin;
+            y = (s.labelBackgroundHeight - s.countFontSize) / 2;
+            drawText(canvas, x, y, count,
+                    labelWidth - x , mCountPaint);
+
+            // draw the icon
+            if (icon != null) {
+                if (jc.isCancelled()) return null;
+                float scale = (float) s.iconSize / icon.getWidth();
+                canvas.translate(s.leftMargin, (s.labelBackgroundHeight -
+                        Math.round(scale * icon.getHeight()))/2f);
+                canvas.scale(scale, scale);
+                canvas.drawBitmap(icon, 0, 0, null);
+            }
+
+            return bitmap;
+        }
+    }
+
+    public void recycleLabel(Bitmap label) {
+        GalleryBitmapPool.getInstance().put(label);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
new file mode 100644
index 0000000..8149df4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -0,0 +1,549 @@
+/*T
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.os.Message;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumSetDataLoader;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TextureUploader;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
+public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener {
+    private static final String TAG = "AlbumSetSlidingWindow";
+    private static final int MSG_UPDATE_ALBUM_ENTRY = 1;
+
+    public static interface Listener {
+        public void onSizeChanged(int size);
+        public void onContentChanged();
+    }
+
+    private final AlbumSetDataLoader mSource;
+    private int mSize;
+
+    private int mContentStart = 0;
+    private int mContentEnd = 0;
+
+    private int mActiveStart = 0;
+    private int mActiveEnd = 0;
+
+    private Listener mListener;
+
+    private final AlbumSetEntry mData[];
+    private final SynchronizedHandler mHandler;
+    private final ThreadPool mThreadPool;
+    private final AlbumLabelMaker mLabelMaker;
+    private final String mLoadingText;
+
+    private final TiledTexture.Uploader mContentUploader;
+    private final TextureUploader mLabelUploader;
+
+    private int mActiveRequestCount = 0;
+    private boolean mIsActive = false;
+    private BitmapTexture mLoadingLabel;
+
+    private int mSlotWidth;
+
+    public static class AlbumSetEntry {
+        public MediaSet album;
+        public MediaItem coverItem;
+        public Texture content;
+        public BitmapTexture labelTexture;
+        public TiledTexture bitmapTexture;
+        public Path setPath;
+        public String title;
+        public int totalCount;
+        public int sourceType;
+        public int cacheFlag;
+        public int cacheStatus;
+        public int rotation;
+        public boolean isWaitLoadingDisplayed;
+        public long setDataVersion;
+        public long coverDataVersion;
+        private BitmapLoader labelLoader;
+        private BitmapLoader coverLoader;
+    }
+
+    public AlbumSetSlidingWindow(AbstractGalleryActivity activity,
+            AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) {
+        source.setModelListener(this);
+        mSource = source;
+        mData = new AlbumSetEntry[cacheSize];
+        mSize = source.size();
+        mThreadPool = activity.getThreadPool();
+
+        mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec);
+        mLoadingText = activity.getAndroidContext().getString(R.string.loading);
+        mContentUploader = new TiledTexture.Uploader(activity.getGLRoot());
+        mLabelUploader = new TextureUploader(activity.getGLRoot());
+
+        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY);
+                ((EntryUpdater) message.obj).updateEntry();
+            }
+        };
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public AlbumSetEntry get(int slotIndex) {
+        if (!isActiveSlot(slotIndex)) {
+            Utils.fail("invalid slot: %s outsides (%s, %s)",
+                    slotIndex, mActiveStart, mActiveEnd);
+        }
+        return mData[slotIndex % mData.length];
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    public boolean isActiveSlot(int slotIndex) {
+        return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+    }
+
+    private void setContentWindow(int contentStart, int contentEnd) {
+        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+        if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            mSource.setActiveWindow(contentStart, contentEnd);
+            for (int i = contentStart; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        } else {
+            for (int i = mContentStart; i < contentStart; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            mSource.setActiveWindow(contentStart, contentEnd);
+            for (int i = contentStart, n = mContentStart; i < n; ++i) {
+                prepareSlotContent(i);
+            }
+            for (int i = mContentEnd; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        }
+
+        mContentStart = contentStart;
+        mContentEnd = contentEnd;
+    }
+
+    public void setActiveWindow(int start, int end) {
+        if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+            Utils.fail("start = %s, end = %s, length = %s, size = %s",
+                    start, end, mData.length, mSize);
+        }
+
+        AlbumSetEntry data[] = mData;
+        mActiveStart = start;
+        mActiveEnd = end;
+        int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+                0, Math.max(0, mSize - data.length));
+        int contentEnd = Math.min(contentStart + data.length, mSize);
+        setContentWindow(contentStart, contentEnd);
+
+        if (mIsActive) {
+            updateTextureUploadQueue();
+            updateAllImageRequests();
+        }
+    }
+
+    // We would like to request non active slots in the following order:
+    // Order:    8 6 4 2                   1 3 5 7
+    //         |---------|---------------|---------|
+    //                   |<-  active  ->|
+    //         |<-------- cached range ----------->|
+    private void requestNonactiveImages() {
+        int range = Math.max(
+                mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+        for (int i = 0 ;i < range; ++i) {
+            requestImagesInSlot(mActiveEnd + i);
+            requestImagesInSlot(mActiveStart - 1 - i);
+        }
+    }
+
+    private void cancelNonactiveImages() {
+        int range = Math.max(
+                mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+        for (int i = 0 ;i < range; ++i) {
+            cancelImagesInSlot(mActiveEnd + i);
+            cancelImagesInSlot(mActiveStart - 1 - i);
+        }
+    }
+
+    private void requestImagesInSlot(int slotIndex) {
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+        AlbumSetEntry entry = mData[slotIndex % mData.length];
+        if (entry.coverLoader != null) entry.coverLoader.startLoad();
+        if (entry.labelLoader != null) entry.labelLoader.startLoad();
+    }
+
+    private void cancelImagesInSlot(int slotIndex) {
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+        AlbumSetEntry entry = mData[slotIndex % mData.length];
+        if (entry.coverLoader != null) entry.coverLoader.cancelLoad();
+        if (entry.labelLoader != null) entry.labelLoader.cancelLoad();
+    }
+
+    private static long getDataVersion(MediaObject object) {
+        return object == null
+                ? MediaSet.INVALID_DATA_VERSION
+                : object.getDataVersion();
+    }
+
+    private void freeSlotContent(int slotIndex) {
+        AlbumSetEntry entry = mData[slotIndex % mData.length];
+        if (entry.coverLoader != null) entry.coverLoader.recycle();
+        if (entry.labelLoader != null) entry.labelLoader.recycle();
+        if (entry.labelTexture != null) entry.labelTexture.recycle();
+        if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+        mData[slotIndex % mData.length] = null;
+    }
+
+    private boolean isLabelChanged(
+            AlbumSetEntry entry, String title, int totalCount, int sourceType) {
+        return !Utils.equals(entry.title, title)
+                || entry.totalCount != totalCount
+                || entry.sourceType != sourceType;
+    }
+
+    private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) {
+        MediaSet album = mSource.getMediaSet(slotIndex);
+        MediaItem cover = mSource.getCoverItem(slotIndex);
+        int totalCount = mSource.getTotalCount(slotIndex);
+
+        entry.album = album;
+        entry.setDataVersion = getDataVersion(album);
+        entry.cacheFlag = identifyCacheFlag(album);
+        entry.cacheStatus = identifyCacheStatus(album);
+        entry.setPath = (album == null) ? null : album.getPath();
+
+        String title = (album == null) ? "" : Utils.ensureNotNull(album.getName());
+        int sourceType = DataSourceType.identifySourceType(album);
+        if (isLabelChanged(entry, title, totalCount, sourceType)) {
+            entry.title = title;
+            entry.totalCount = totalCount;
+            entry.sourceType = sourceType;
+            if (entry.labelLoader != null) {
+                entry.labelLoader.recycle();
+                entry.labelLoader = null;
+                entry.labelTexture = null;
+            }
+            if (album != null) {
+                entry.labelLoader = new AlbumLabelLoader(
+                        slotIndex, title, totalCount, sourceType);
+            }
+        }
+
+        entry.coverItem = cover;
+        if (getDataVersion(cover) != entry.coverDataVersion) {
+            entry.coverDataVersion = getDataVersion(cover);
+            entry.rotation = (cover == null) ? 0 : cover.getRotation();
+            if (entry.coverLoader != null) {
+                entry.coverLoader.recycle();
+                entry.coverLoader = null;
+                entry.bitmapTexture = null;
+                entry.content = null;
+            }
+            if (cover != null) {
+                entry.coverLoader = new AlbumCoverLoader(slotIndex, cover);
+            }
+        }
+    }
+
+    private void prepareSlotContent(int slotIndex) {
+        AlbumSetEntry entry = new AlbumSetEntry();
+        updateAlbumSetEntry(entry, slotIndex);
+        mData[slotIndex % mData.length] = entry;
+    }
+
+    private static boolean startLoadBitmap(BitmapLoader loader) {
+        if (loader == null) return false;
+        loader.startLoad();
+        return loader.isRequestInProgress();
+    }
+
+    private void uploadBackgroundTextureInSlot(int index) {
+        if (index < mContentStart || index >= mContentEnd) return;
+        AlbumSetEntry entry = mData[index % mData.length];
+        if (entry.bitmapTexture != null) {
+            mContentUploader.addTexture(entry.bitmapTexture);
+        }
+        if (entry.labelTexture != null) {
+            mLabelUploader.addBgTexture(entry.labelTexture);
+        }
+    }
+
+    private void updateTextureUploadQueue() {
+        if (!mIsActive) return;
+        mContentUploader.clear();
+        mLabelUploader.clear();
+
+        // Upload foreground texture
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            AlbumSetEntry entry = mData[i % mData.length];
+            if (entry.bitmapTexture != null) {
+                mContentUploader.addTexture(entry.bitmapTexture);
+            }
+            if (entry.labelTexture != null) {
+                mLabelUploader.addFgTexture(entry.labelTexture);
+            }
+        }
+
+        // add background textures
+        int range = Math.max(
+                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+        for (int i = 0; i < range; ++i) {
+            uploadBackgroundTextureInSlot(mActiveEnd + i);
+            uploadBackgroundTextureInSlot(mActiveStart - i - 1);
+        }
+    }
+
+    private void updateAllImageRequests() {
+        mActiveRequestCount = 0;
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            AlbumSetEntry entry = mData[i % mData.length];
+            if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount;
+            if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount;
+        }
+        if (mActiveRequestCount == 0) {
+            requestNonactiveImages();
+        } else {
+            cancelNonactiveImages();
+        }
+    }
+
+    @Override
+    public void onSizeChanged(int size) {
+        if (mIsActive && mSize != size) {
+            mSize = size;
+            if (mListener != null) mListener.onSizeChanged(mSize);
+            if (mContentEnd > mSize) mContentEnd = mSize;
+            if (mActiveEnd > mSize) mActiveEnd = mSize;
+        }
+    }
+
+    @Override
+    public void onContentChanged(int index) {
+        if (!mIsActive) {
+            // paused, ignore slot changed event
+            return;
+        }
+
+        // If the updated content is not cached, ignore it
+        if (index < mContentStart || index >= mContentEnd) {
+            Log.w(TAG, String.format(
+                    "invalid update: %s is outside (%s, %s)",
+                    index, mContentStart, mContentEnd) );
+            return;
+        }
+
+        AlbumSetEntry entry = mData[index % mData.length];
+        updateAlbumSetEntry(entry, index);
+        updateAllImageRequests();
+        updateTextureUploadQueue();
+        if (mListener != null && isActiveSlot(index)) {
+            mListener.onContentChanged();
+        }
+    }
+
+    public BitmapTexture getLoadingTexture() {
+        if (mLoadingLabel == null) {
+            Bitmap bitmap = mLabelMaker.requestLabel(
+                    mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED)
+                    .run(ThreadPool.JOB_CONTEXT_STUB);
+            mLoadingLabel = new BitmapTexture(bitmap);
+            mLoadingLabel.setOpaque(false);
+        }
+        return mLoadingLabel;
+    }
+
+    public void pause() {
+        mIsActive = false;
+        mLabelUploader.clear();
+        mContentUploader.clear();
+        TiledTexture.freeResources();
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            freeSlotContent(i);
+        }
+    }
+
+    public void resume() {
+        mIsActive = true;
+        TiledTexture.prepareResources();
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            prepareSlotContent(i);
+        }
+        updateAllImageRequests();
+    }
+
+    private static interface EntryUpdater {
+        public void updateEntry();
+    }
+
+    private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater {
+        private MediaItem mMediaItem;
+        private final int mSlotIndex;
+
+        public AlbumCoverLoader(int slotIndex, MediaItem item) {
+            mSlotIndex = slotIndex;
+            mMediaItem = item;
+        }
+
+        @Override
+        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+            return mThreadPool.submit(mMediaItem.requestImage(
+                    MediaItem.TYPE_MICROTHUMBNAIL), l);
+        }
+
+        @Override
+        protected void onLoadComplete(Bitmap bitmap) {
+            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+        }
+
+        @Override
+        public void updateEntry() {
+            Bitmap bitmap = getBitmap();
+            if (bitmap == null) return; // error or recycled
+
+            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+            TiledTexture texture = new TiledTexture(bitmap);
+            entry.bitmapTexture = texture;
+            entry.content = texture;
+
+            if (isActiveSlot(mSlotIndex)) {
+                mContentUploader.addTexture(texture);
+                --mActiveRequestCount;
+                if (mActiveRequestCount == 0) requestNonactiveImages();
+                if (mListener != null) mListener.onContentChanged();
+            } else {
+                mContentUploader.addTexture(texture);
+            }
+        }
+    }
+
+    private static int identifyCacheFlag(MediaSet set) {
+        if (set == null || (set.getSupportedOperations()
+                & MediaSet.SUPPORT_CACHE) == 0) {
+            return MediaSet.CACHE_FLAG_NO;
+        }
+
+        return set.getCacheFlag();
+    }
+
+    private static int identifyCacheStatus(MediaSet set) {
+        if (set == null || (set.getSupportedOperations()
+                & MediaSet.SUPPORT_CACHE) == 0) {
+            return MediaSet.CACHE_STATUS_NOT_CACHED;
+        }
+
+        return set.getCacheStatus();
+    }
+
+    private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater {
+        private final int mSlotIndex;
+        private final String mTitle;
+        private final int mTotalCount;
+        private final int mSourceType;
+
+        public AlbumLabelLoader(
+                int slotIndex, String title, int totalCount, int sourceType) {
+            mSlotIndex = slotIndex;
+            mTitle = title;
+            mTotalCount = totalCount;
+            mSourceType = sourceType;
+        }
+
+        @Override
+        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+            return mThreadPool.submit(mLabelMaker.requestLabel(
+                    mTitle, String.valueOf(mTotalCount), mSourceType), l);
+        }
+
+        @Override
+        protected void onLoadComplete(Bitmap bitmap) {
+            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+        }
+
+        @Override
+        public void updateEntry() {
+            Bitmap bitmap = getBitmap();
+            if (bitmap == null) return; // Error or recycled
+
+            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+            BitmapTexture texture = new BitmapTexture(bitmap);
+            texture.setOpaque(false);
+            entry.labelTexture = texture;
+
+            if (isActiveSlot(mSlotIndex)) {
+                mLabelUploader.addFgTexture(texture);
+                --mActiveRequestCount;
+                if (mActiveRequestCount == 0) requestNonactiveImages();
+                if (mListener != null) mListener.onContentChanged();
+            } else {
+                mLabelUploader.addBgTexture(texture);
+            }
+        }
+    }
+
+    public void onSlotSizeChanged(int width, int height) {
+        if (mSlotWidth == width) return;
+
+        mSlotWidth = width;
+        mLoadingLabel = null;
+        mLabelMaker.setLabelWidth(mSlotWidth);
+
+        if (!mIsActive) return;
+
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            AlbumSetEntry entry = mData[i % mData.length];
+            if (entry.labelLoader != null) {
+                entry.labelLoader.recycle();
+                entry.labelLoader = null;
+                entry.labelTexture = null;
+            }
+            if (entry.album != null) {
+                entry.labelLoader = new AlbumLabelLoader(i,
+                        entry.title, entry.totalCount, entry.sourceType);
+            }
+        }
+        updateAllImageRequests();
+        updateTextureUploadQueue();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
new file mode 100644
index 0000000..5332ef8
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumSetDataLoader;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.ColorTexture;
+import com.android.gallery3d.glrenderer.FadeInTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class AlbumSetSlotRenderer extends AbstractSlotRenderer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumSetView";
+    private static final int CACHE_SIZE = 96;
+    private final int mPlaceholderColor;
+
+    private final ColorTexture mWaitLoadingTexture;
+    private final ResourceTexture mCameraOverlay;
+    private final AbstractGalleryActivity mActivity;
+    private final SelectionManager mSelectionManager;
+    protected final LabelSpec mLabelSpec;
+
+    protected AlbumSetSlidingWindow mDataWindow;
+    private SlotView mSlotView;
+
+    private int mPressedIndex = -1;
+    private boolean mAnimatePressedUp;
+    private Path mHighlightItemPath = null;
+    private boolean mInSelectionMode;
+
+    public static class LabelSpec {
+        public int labelBackgroundHeight;
+        public int titleOffset;
+        public int countOffset;
+        public int titleFontSize;
+        public int countFontSize;
+        public int leftMargin;
+        public int iconSize;
+        public int titleRightMargin;
+        public int backgroundColor;
+        public int titleColor;
+        public int countColor;
+        public int borderSize;
+    }
+
+    public AlbumSetSlotRenderer(AbstractGalleryActivity activity,
+            SelectionManager selectionManager,
+            SlotView slotView, LabelSpec labelSpec, int placeholderColor) {
+        super (activity);
+        mActivity = activity;
+        mSelectionManager = selectionManager;
+        mSlotView = slotView;
+        mLabelSpec = labelSpec;
+        mPlaceholderColor = placeholderColor;
+
+        mWaitLoadingTexture = new ColorTexture(mPlaceholderColor);
+        mWaitLoadingTexture.setSize(1, 1);
+        mCameraOverlay = new ResourceTexture(activity,
+                R.drawable.ic_cameraalbum_overlay);
+    }
+
+    public void setPressedIndex(int index) {
+        if (mPressedIndex == index) return;
+        mPressedIndex = index;
+        mSlotView.invalidate();
+    }
+
+    public void setPressedUp() {
+        if (mPressedIndex == -1) return;
+        mAnimatePressedUp = true;
+        mSlotView.invalidate();
+    }
+
+    public void setHighlightItemPath(Path path) {
+        if (mHighlightItemPath == path) return;
+        mHighlightItemPath = path;
+        mSlotView.invalidate();
+    }
+
+    public void setModel(AlbumSetDataLoader model) {
+        if (mDataWindow != null) {
+            mDataWindow.setListener(null);
+            mDataWindow = null;
+            mSlotView.setSlotCount(0);
+        }
+        if (model != null) {
+            mDataWindow = new AlbumSetSlidingWindow(
+                    mActivity, model, mLabelSpec, CACHE_SIZE);
+            mDataWindow.setListener(new MyCacheListener());
+            mSlotView.setSlotCount(mDataWindow.size());
+        }
+    }
+
+    private static Texture checkLabelTexture(Texture texture) {
+        return ((texture instanceof UploadedTexture)
+                && ((UploadedTexture) texture).isUploading())
+                ? null
+                : texture;
+    }
+
+    private static Texture checkContentTexture(Texture texture) {
+        return ((texture instanceof TiledTexture)
+                && !((TiledTexture) texture).isReady())
+                ? null
+                : texture;
+    }
+
+    @Override
+    public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        AlbumSetEntry entry = mDataWindow.get(index);
+        int renderRequestFlags = 0;
+        renderRequestFlags |= renderContent(canvas, entry, width, height);
+        renderRequestFlags |= renderLabel(canvas, entry, width, height);
+        renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+        return renderRequestFlags;
+    }
+
+    protected int renderOverlay(
+            GLCanvas canvas, int index, AlbumSetEntry entry, int width, int height) {
+        int renderRequestFlags = 0;
+        if (entry.album != null && entry.album.isCameraRoll()) {
+            int uncoveredHeight = height - mLabelSpec.labelBackgroundHeight;
+            int dim = uncoveredHeight / 2;
+            mCameraOverlay.draw(canvas, (width - dim) / 2,
+                    (uncoveredHeight - dim) / 2, dim, dim);
+        }
+        if (mPressedIndex == index) {
+            if (mAnimatePressedUp) {
+                drawPressedUpFrame(canvas, width, height);
+                renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+                if (isPressedUpFrameFinished()) {
+                    mAnimatePressedUp = false;
+                    mPressedIndex = -1;
+                }
+            } else {
+                drawPressedFrame(canvas, width, height);
+            }
+        } else if ((mHighlightItemPath != null) && (mHighlightItemPath == entry.setPath)) {
+            drawSelectedFrame(canvas, width, height);
+        } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.setPath)) {
+            drawSelectedFrame(canvas, width, height);
+        }
+        return renderRequestFlags;
+    }
+
+    protected int renderContent(
+            GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+        int renderRequestFlags = 0;
+
+        Texture content = checkContentTexture(entry.content);
+        if (content == null) {
+            content = mWaitLoadingTexture;
+            entry.isWaitLoadingDisplayed = true;
+        } else if (entry.isWaitLoadingDisplayed) {
+            entry.isWaitLoadingDisplayed = false;
+            content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture);
+            entry.content = content;
+        }
+        drawContent(canvas, content, width, height, entry.rotation);
+        if ((content instanceof FadeInTexture) &&
+                ((FadeInTexture) content).isAnimating()) {
+            renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+        }
+
+        return renderRequestFlags;
+    }
+
+    protected int renderLabel(
+            GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+        Texture content = checkLabelTexture(entry.labelTexture);
+        if (content == null) {
+            content = mWaitLoadingTexture;
+        }
+        int b = AlbumLabelMaker.getBorderSize();
+        int h = mLabelSpec.labelBackgroundHeight;
+        content.draw(canvas, -b, height - h + b, width + b + b, h);
+
+        return 0;
+    }
+
+    @Override
+    public void prepareDrawing() {
+        mInSelectionMode = mSelectionManager.inSelectionMode();
+    }
+
+    private class MyCacheListener implements AlbumSetSlidingWindow.Listener {
+
+        @Override
+        public void onSizeChanged(int size) {
+            mSlotView.setSlotCount(size);
+        }
+
+        @Override
+        public void onContentChanged() {
+            mSlotView.invalidate();
+        }
+    }
+
+    public void pause() {
+        mDataWindow.pause();
+    }
+
+    public void resume() {
+        mDataWindow.resume();
+    }
+
+    @Override
+    public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+        if (mDataWindow != null) {
+            mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+        }
+    }
+
+    @Override
+    public void onSlotSizeChanged(int width, int height) {
+        if (mDataWindow != null) {
+            mDataWindow.onSlotSizeChanged(width, height);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
new file mode 100644
index 0000000..fec7d1e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.os.Message;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumDataLoader;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.JobLimiter;
+
+public class AlbumSlidingWindow implements AlbumDataLoader.DataListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumSlidingWindow";
+
+    private static final int MSG_UPDATE_ENTRY = 0;
+    private static final int JOB_LIMIT = 2;
+
+    public static interface Listener {
+        public void onSizeChanged(int size);
+        public void onContentChanged();
+    }
+
+    public static class AlbumEntry {
+        public MediaItem item;
+        public Path path;
+        public boolean isPanorama;
+        public int rotation;
+        public int mediaType;
+        public boolean isWaitDisplayed;
+        public TiledTexture bitmapTexture;
+        public Texture content;
+        private BitmapLoader contentLoader;
+        private PanoSupportListener mPanoSupportListener;
+    }
+
+    private final AlbumDataLoader mSource;
+    private final AlbumEntry mData[];
+    private final SynchronizedHandler mHandler;
+    private final JobLimiter mThreadPool;
+    private final TiledTexture.Uploader mTileUploader;
+
+    private int mSize;
+
+    private int mContentStart = 0;
+    private int mContentEnd = 0;
+
+    private int mActiveStart = 0;
+    private int mActiveEnd = 0;
+
+    private Listener mListener;
+
+    private int mActiveRequestCount = 0;
+    private boolean mIsActive = false;
+
+    private class PanoSupportListener implements PanoramaSupportCallback {
+        public final AlbumEntry mEntry;
+        public PanoSupportListener (AlbumEntry entry) {
+            mEntry = entry;
+        }
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            if (mEntry != null) mEntry.isPanorama = isPanorama;
+        }
+    }
+
+    public AlbumSlidingWindow(AbstractGalleryActivity activity,
+            AlbumDataLoader source, int cacheSize) {
+        source.setDataListener(this);
+        mSource = source;
+        mData = new AlbumEntry[cacheSize];
+        mSize = source.size();
+
+        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                Utils.assertTrue(message.what == MSG_UPDATE_ENTRY);
+                ((ThumbnailLoader) message.obj).updateEntry();
+            }
+        };
+
+        mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT);
+        mTileUploader = new TiledTexture.Uploader(activity.getGLRoot());
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public AlbumEntry get(int slotIndex) {
+        if (!isActiveSlot(slotIndex)) {
+            Utils.fail("invalid slot: %s outsides (%s, %s)",
+                    slotIndex, mActiveStart, mActiveEnd);
+        }
+        return mData[slotIndex % mData.length];
+    }
+
+    public boolean isActiveSlot(int slotIndex) {
+        return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+    }
+
+    private void setContentWindow(int contentStart, int contentEnd) {
+        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+        if (!mIsActive) {
+            mContentStart = contentStart;
+            mContentEnd = contentEnd;
+            mSource.setActiveWindow(contentStart, contentEnd);
+            return;
+        }
+
+        if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            mSource.setActiveWindow(contentStart, contentEnd);
+            for (int i = contentStart; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        } else {
+            for (int i = mContentStart; i < contentStart; ++i) {
+                freeSlotContent(i);
+            }
+            for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+                freeSlotContent(i);
+            }
+            mSource.setActiveWindow(contentStart, contentEnd);
+            for (int i = contentStart, n = mContentStart; i < n; ++i) {
+                prepareSlotContent(i);
+            }
+            for (int i = mContentEnd; i < contentEnd; ++i) {
+                prepareSlotContent(i);
+            }
+        }
+
+        mContentStart = contentStart;
+        mContentEnd = contentEnd;
+    }
+
+    public void setActiveWindow(int start, int end) {
+        if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+            Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize);
+        }
+        AlbumEntry data[] = mData;
+
+        mActiveStart = start;
+        mActiveEnd = end;
+
+        int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+                0, Math.max(0, mSize - data.length));
+        int contentEnd = Math.min(contentStart + data.length, mSize);
+        setContentWindow(contentStart, contentEnd);
+        updateTextureUploadQueue();
+        if (mIsActive) updateAllImageRequests();
+    }
+
+    private void uploadBgTextureInSlot(int index) {
+        if (index < mContentEnd && index >= mContentStart) {
+            AlbumEntry entry = mData[index % mData.length];
+            if (entry.bitmapTexture != null) {
+                mTileUploader.addTexture(entry.bitmapTexture);
+            }
+        }
+    }
+
+    private void updateTextureUploadQueue() {
+        if (!mIsActive) return;
+        mTileUploader.clear();
+
+        // add foreground textures
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            AlbumEntry entry = mData[i % mData.length];
+            if (entry.bitmapTexture != null) {
+                mTileUploader.addTexture(entry.bitmapTexture);
+            }
+        }
+
+        // add background textures
+        int range = Math.max(
+                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+        for (int i = 0; i < range; ++i) {
+            uploadBgTextureInSlot(mActiveEnd + i);
+            uploadBgTextureInSlot(mActiveStart - i - 1);
+        }
+    }
+
+    // We would like to request non active slots in the following order:
+    // Order:    8 6 4 2                   1 3 5 7
+    //         |---------|---------------|---------|
+    //                   |<-  active  ->|
+    //         |<-------- cached range ----------->|
+    private void requestNonactiveImages() {
+        int range = Math.max(
+                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+        for (int i = 0 ;i < range; ++i) {
+            requestSlotImage(mActiveEnd + i);
+            requestSlotImage(mActiveStart - 1 - i);
+        }
+    }
+
+    // return whether the request is in progress or not
+    private boolean requestSlotImage(int slotIndex) {
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false;
+        AlbumEntry entry = mData[slotIndex % mData.length];
+        if (entry.content != null || entry.item == null) return false;
+
+        // Set up the panorama callback
+        entry.mPanoSupportListener = new PanoSupportListener(entry);
+        entry.item.getPanoramaSupport(entry.mPanoSupportListener);
+
+        entry.contentLoader.startLoad();
+        return entry.contentLoader.isRequestInProgress();
+    }
+
+    private void cancelNonactiveImages() {
+        int range = Math.max(
+                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+        for (int i = 0 ;i < range; ++i) {
+            cancelSlotImage(mActiveEnd + i);
+            cancelSlotImage(mActiveStart - 1 - i);
+        }
+    }
+
+    private void cancelSlotImage(int slotIndex) {
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+        AlbumEntry item = mData[slotIndex % mData.length];
+        if (item.contentLoader != null) item.contentLoader.cancelLoad();
+    }
+
+    private void freeSlotContent(int slotIndex) {
+        AlbumEntry data[] = mData;
+        int index = slotIndex % data.length;
+        AlbumEntry entry = data[index];
+        if (entry.contentLoader != null) entry.contentLoader.recycle();
+        if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+        data[index] = null;
+    }
+
+    private void prepareSlotContent(int slotIndex) {
+        AlbumEntry entry = new AlbumEntry();
+        MediaItem item = mSource.get(slotIndex); // item could be null;
+        entry.item = item;
+        entry.mediaType = (item == null)
+                ? MediaItem.MEDIA_TYPE_UNKNOWN
+                : entry.item.getMediaType();
+        entry.path = (item == null) ? null : item.getPath();
+        entry.rotation = (item == null) ? 0 : item.getRotation();
+        entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item);
+        mData[slotIndex % mData.length] = entry;
+    }
+
+    private void updateAllImageRequests() {
+        mActiveRequestCount = 0;
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            if (requestSlotImage(i)) ++mActiveRequestCount;
+        }
+        if (mActiveRequestCount == 0) {
+            requestNonactiveImages();
+        } else {
+            cancelNonactiveImages();
+        }
+    }
+
+    private class ThumbnailLoader extends BitmapLoader  {
+        private final int mSlotIndex;
+        private final MediaItem mItem;
+
+        public ThumbnailLoader(int slotIndex, MediaItem item) {
+            mSlotIndex = slotIndex;
+            mItem = item;
+        }
+
+        @Override
+        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+            return mThreadPool.submit(
+                    mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this);
+        }
+
+        @Override
+        protected void onLoadComplete(Bitmap bitmap) {
+            mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget();
+        }
+
+        public void updateEntry() {
+            Bitmap bitmap = getBitmap();
+            if (bitmap == null) return; // error or recycled
+            AlbumEntry entry = mData[mSlotIndex % mData.length];
+            entry.bitmapTexture = new TiledTexture(bitmap);
+            entry.content = entry.bitmapTexture;
+
+            if (isActiveSlot(mSlotIndex)) {
+                mTileUploader.addTexture(entry.bitmapTexture);
+                --mActiveRequestCount;
+                if (mActiveRequestCount == 0) requestNonactiveImages();
+                if (mListener != null) mListener.onContentChanged();
+            } else {
+                mTileUploader.addTexture(entry.bitmapTexture);
+            }
+        }
+    }
+
+    @Override
+    public void onSizeChanged(int size) {
+        if (mSize != size) {
+            mSize = size;
+            if (mListener != null) mListener.onSizeChanged(mSize);
+            if (mContentEnd > mSize) mContentEnd = mSize;
+            if (mActiveEnd > mSize) mActiveEnd = mSize;
+        }
+    }
+
+    @Override
+    public void onContentChanged(int index) {
+        if (index >= mContentStart && index < mContentEnd && mIsActive) {
+            freeSlotContent(index);
+            prepareSlotContent(index);
+            updateAllImageRequests();
+            if (mListener != null && isActiveSlot(index)) {
+                mListener.onContentChanged();
+            }
+        }
+    }
+
+    public void resume() {
+        mIsActive = true;
+        TiledTexture.prepareResources();
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            prepareSlotContent(i);
+        }
+        updateAllImageRequests();
+    }
+
+    public void pause() {
+        mIsActive = false;
+        mTileUploader.clear();
+        TiledTexture.freeResources();
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            freeSlotContent(i);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
new file mode 100644
index 0000000..dc6c89b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumDataLoader;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.ColorTexture;
+import com.android.gallery3d.glrenderer.FadeInTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+
+public class AlbumSlotRenderer extends AbstractSlotRenderer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumView";
+
+    public interface SlotFilter {
+        public boolean acceptSlot(int index);
+    }
+
+    private final int mPlaceholderColor;
+    private static final int CACHE_SIZE = 96;
+
+    private AlbumSlidingWindow mDataWindow;
+    private final AbstractGalleryActivity mActivity;
+    private final ColorTexture mWaitLoadingTexture;
+    private final SlotView mSlotView;
+    private final SelectionManager mSelectionManager;
+
+    private int mPressedIndex = -1;
+    private boolean mAnimatePressedUp;
+    private Path mHighlightItemPath = null;
+    private boolean mInSelectionMode;
+
+    private SlotFilter mSlotFilter;
+
+    public AlbumSlotRenderer(AbstractGalleryActivity activity, SlotView slotView,
+            SelectionManager selectionManager, int placeholderColor) {
+        super(activity);
+        mActivity = activity;
+        mSlotView = slotView;
+        mSelectionManager = selectionManager;
+        mPlaceholderColor = placeholderColor;
+
+        mWaitLoadingTexture = new ColorTexture(mPlaceholderColor);
+        mWaitLoadingTexture.setSize(1, 1);
+    }
+
+    public void setPressedIndex(int index) {
+        if (mPressedIndex == index) return;
+        mPressedIndex = index;
+        mSlotView.invalidate();
+    }
+
+    public void setPressedUp() {
+        if (mPressedIndex == -1) return;
+        mAnimatePressedUp = true;
+        mSlotView.invalidate();
+    }
+
+    public void setHighlightItemPath(Path path) {
+        if (mHighlightItemPath == path) return;
+        mHighlightItemPath = path;
+        mSlotView.invalidate();
+    }
+
+    public void setModel(AlbumDataLoader model) {
+        if (mDataWindow != null) {
+            mDataWindow.setListener(null);
+            mSlotView.setSlotCount(0);
+            mDataWindow = null;
+        }
+        if (model != null) {
+            mDataWindow = new AlbumSlidingWindow(mActivity, model, CACHE_SIZE);
+            mDataWindow.setListener(new MyDataModelListener());
+            mSlotView.setSlotCount(model.size());
+        }
+    }
+
+    private static Texture checkTexture(Texture texture) {
+        return (texture instanceof TiledTexture)
+                && !((TiledTexture) texture).isReady()
+                ? null
+                : texture;
+    }
+
+    @Override
+    public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        if (mSlotFilter != null && !mSlotFilter.acceptSlot(index)) return 0;
+
+        AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index);
+
+        int renderRequestFlags = 0;
+
+        Texture content = checkTexture(entry.content);
+        if (content == null) {
+            content = mWaitLoadingTexture;
+            entry.isWaitDisplayed = true;
+        } else if (entry.isWaitDisplayed) {
+            entry.isWaitDisplayed = false;
+            content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture);
+            entry.content = content;
+        }
+        drawContent(canvas, content, width, height, entry.rotation);
+        if ((content instanceof FadeInTexture) &&
+                ((FadeInTexture) content).isAnimating()) {
+            renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+        }
+
+        if (entry.mediaType == MediaObject.MEDIA_TYPE_VIDEO) {
+            drawVideoOverlay(canvas, width, height);
+        }
+
+        if (entry.isPanorama) {
+            drawPanoramaIcon(canvas, width, height);
+        }
+
+        renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+
+        return renderRequestFlags;
+    }
+
+    private int renderOverlay(GLCanvas canvas, int index,
+            AlbumSlidingWindow.AlbumEntry entry, int width, int height) {
+        int renderRequestFlags = 0;
+        if (mPressedIndex == index) {
+            if (mAnimatePressedUp) {
+                drawPressedUpFrame(canvas, width, height);
+                renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+                if (isPressedUpFrameFinished()) {
+                    mAnimatePressedUp = false;
+                    mPressedIndex = -1;
+                }
+            } else {
+                drawPressedFrame(canvas, width, height);
+            }
+        } else if ((entry.path != null) && (mHighlightItemPath == entry.path)) {
+            drawSelectedFrame(canvas, width, height);
+        } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.path)) {
+            drawSelectedFrame(canvas, width, height);
+        }
+        return renderRequestFlags;
+    }
+
+    private class MyDataModelListener implements AlbumSlidingWindow.Listener {
+        @Override
+        public void onContentChanged() {
+            mSlotView.invalidate();
+        }
+
+        @Override
+        public void onSizeChanged(int size) {
+            mSlotView.setSlotCount(size);
+        }
+    }
+
+    public void resume() {
+        mDataWindow.resume();
+    }
+
+    public void pause() {
+        mDataWindow.pause();
+    }
+
+    @Override
+    public void prepareDrawing() {
+        mInSelectionMode = mSelectionManager.inSelectionMode();
+    }
+
+    @Override
+    public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+        if (mDataWindow != null) {
+            mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+        }
+    }
+
+    @Override
+    public void onSlotSizeChanged(int width, int height) {
+        // Do nothing
+    }
+
+    public void setSlotFilter(SlotFilter slotFilter) {
+        mSlotFilter = slotFilter;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AnimationTime.java b/src/com/android/gallery3d/ui/AnimationTime.java
new file mode 100644
index 0000000..0636774
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AnimationTime.java
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.os.SystemClock;
+
+//
+// The animation time should ideally be the vsync time the frame will be
+// displayed, but that is an unknown time in the future. So we use the system
+// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called)
+// as a approximation.
+//
+public class AnimationTime {
+    private static volatile long sTime;
+
+    // Sets current time as the animation time.
+    public static void update() {
+        sTime = SystemClock.uptimeMillis();
+    }
+
+    // Returns the animation time.
+    public static long get() {
+        return sTime;
+    }
+
+    public static long startTime() {
+        sTime = SystemClock.uptimeMillis();
+        return sTime;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapLoader.java b/src/com/android/gallery3d/ui/BitmapLoader.java
new file mode 100644
index 0000000..a708a90
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapLoader.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+// We use this class to
+//     1.) load bitmaps in background.
+//     2.) as a place holder for the loaded bitmap
+public abstract class BitmapLoader implements FutureListener<Bitmap> {
+    @SuppressWarnings("unused")
+    private static final String TAG = "BitmapLoader";
+
+    /* Transition Map:
+     *   INIT -> REQUESTED, RECYCLED
+     *   REQUESTED -> INIT (cancel), LOADED, ERROR, RECYCLED
+     *   LOADED, ERROR -> RECYCLED
+     */
+    private static final int STATE_INIT = 0;
+    private static final int STATE_REQUESTED = 1;
+    private static final int STATE_LOADED = 2;
+    private static final int STATE_ERROR = 3;
+    private static final int STATE_RECYCLED = 4;
+
+    private int mState = STATE_INIT;
+    // mTask is not null only when a task is on the way
+    private Future<Bitmap> mTask;
+    private Bitmap mBitmap;
+
+    @Override
+    public void onFutureDone(Future<Bitmap> future) {
+        synchronized (this) {
+            mTask = null;
+            mBitmap = future.get();
+            if (mState == STATE_RECYCLED) {
+                if (mBitmap != null) {
+                    GalleryBitmapPool.getInstance().put(mBitmap);
+                    mBitmap = null;
+                }
+                return; // don't call callback
+            }
+            if (future.isCancelled() && mBitmap == null) {
+                if (mState == STATE_REQUESTED) mTask = submitBitmapTask(this);
+                return; // don't call callback
+            } else {
+                mState = mBitmap == null ? STATE_ERROR : STATE_LOADED;
+            }
+        }
+        onLoadComplete(mBitmap);
+    }
+
+    public synchronized void startLoad() {
+        if (mState == STATE_INIT) {
+            mState = STATE_REQUESTED;
+            if (mTask == null) mTask = submitBitmapTask(this);
+        }
+    }
+
+    public synchronized void cancelLoad() {
+        if (mState == STATE_REQUESTED) {
+            mState = STATE_INIT;
+            if (mTask != null) mTask.cancel();
+        }
+    }
+
+    // Recycle the loader and the bitmap
+    public synchronized void recycle() {
+        mState = STATE_RECYCLED;
+        if (mBitmap != null) {
+            GalleryBitmapPool.getInstance().put(mBitmap);
+            mBitmap = null;
+        }
+        if (mTask != null) mTask.cancel();
+    }
+
+    public synchronized boolean isRequestInProgress() {
+        return mState == STATE_REQUESTED;
+    }
+
+    public synchronized boolean isRecycled() {
+        return mState == STATE_RECYCLED;
+    }
+
+    public synchronized Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    abstract protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l);
+    abstract protected void onLoadComplete(Bitmap bitmap);
+}
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
new file mode 100644
index 0000000..a3d4039
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.RectF;
+
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public class BitmapScreenNail implements ScreenNail {
+    private final BitmapTexture mBitmapTexture;
+
+    public BitmapScreenNail(Bitmap bitmap) {
+        mBitmapTexture = new BitmapTexture(bitmap);
+    }
+
+    @Override
+    public int getWidth() {
+        return mBitmapTexture.getWidth();
+    }
+
+    @Override
+    public int getHeight() {
+        return mBitmapTexture.getHeight();
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        mBitmapTexture.draw(canvas, x, y, width, height);
+    }
+
+    @Override
+    public void noDraw() {
+        // do nothing
+    }
+
+    @Override
+    public void recycle() {
+        mBitmapTexture.recycle();
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        canvas.drawTexture(mBitmapTexture, source, dest);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
new file mode 100644
index 0000000..e1a8b76
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.photos.data.GalleryBitmapPool;
+
+import java.util.ArrayList;
+
+public class BitmapTileProvider implements TileImageView.TileSource {
+    private final ScreenNail mScreenNail;
+    private final Bitmap[] mMipmaps;
+    private final Config mConfig;
+    private final int mImageWidth;
+    private final int mImageHeight;
+
+    private boolean mRecycled = false;
+
+    public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) {
+        mImageWidth = bitmap.getWidth();
+        mImageHeight = bitmap.getHeight();
+        ArrayList<Bitmap> list = new ArrayList<Bitmap>();
+        list.add(bitmap);
+        while (bitmap.getWidth() > maxBackupSize
+                || bitmap.getHeight() > maxBackupSize) {
+            bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false);
+            list.add(bitmap);
+        }
+
+        mScreenNail = new BitmapScreenNail(list.remove(list.size() - 1));
+        mMipmaps = list.toArray(new Bitmap[list.size()]);
+        mConfig = Config.ARGB_8888;
+    }
+
+    @Override
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
+    }
+
+    @Override
+    public int getImageHeight() {
+        return mImageHeight;
+    }
+
+    @Override
+    public int getImageWidth() {
+        return mImageWidth;
+    }
+
+    @Override
+    public int getLevelCount() {
+        return mMipmaps.length;
+    }
+
+    @Override
+    public Bitmap getTile(int level, int x, int y, int tileSize) {
+        x >>= level;
+        y >>= level;
+
+        Bitmap result = GalleryBitmapPool.getInstance().get(tileSize, tileSize);
+        if (result == null) {
+            result = Bitmap.createBitmap(tileSize, tileSize, mConfig);
+        } else {
+            result.eraseColor(0);
+        }
+
+        Bitmap mipmap = mMipmaps[level];
+        Canvas canvas = new Canvas(result);
+        int offsetX = -x;
+        int offsetY = -y;
+        canvas.drawBitmap(mipmap, offsetX, offsetY, null);
+        return result;
+    }
+
+    public void recycle() {
+        if (mRecycled) return;
+        mRecycled = true;
+        for (Bitmap bitmap : mMipmaps) {
+            BitmapUtils.recycleSilently(bitmap);
+        }
+        if (mScreenNail != null) {
+            mScreenNail.recycle();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
new file mode 100644
index 0000000..46f7a24
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.os.StatFs;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+
+public class CacheStorageUsageInfo {
+    @SuppressWarnings("unused")
+    private static final String TAG = "CacheStorageUsageInfo";
+
+    // number of bytes the storage has.
+    private long mTotalBytes;
+
+    // number of bytes already used.
+    private long mUsedBytes;
+
+    // number of bytes used for the cache (should be less then usedBytes).
+    private long mUsedCacheBytes;
+
+    // number of bytes used for the cache if all pending downloads (and removals) are completed.
+    private long mTargetCacheBytes;
+
+    private AbstractGalleryActivity mActivity;
+    private Context mContext;
+    private long mUserChangeDelta;
+
+    public CacheStorageUsageInfo(AbstractGalleryActivity activity) {
+        mActivity = activity;
+        mContext = activity.getAndroidContext();
+    }
+
+    public void increaseTargetCacheSize(long delta) {
+        mUserChangeDelta += delta;
+    }
+
+    public void loadStorageInfo(JobContext jc) {
+        File cacheDir = mContext.getExternalCacheDir();
+        if (cacheDir == null) {
+            cacheDir = mContext.getCacheDir();
+        }
+
+        String path = cacheDir.getAbsolutePath();
+        StatFs stat = new StatFs(path);
+        long blockSize = stat.getBlockSize();
+        long availableBlocks = stat.getAvailableBlocks();
+        long totalBlocks = stat.getBlockCount();
+
+        mTotalBytes = blockSize * totalBlocks;
+        mUsedBytes = blockSize * (totalBlocks - availableBlocks);
+        mUsedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize();
+        mTargetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize();
+    }
+
+    public long getTotalBytes() {
+        return mTotalBytes;
+    }
+
+    public long getExpectedUsedBytes() {
+        return mUsedBytes - mUsedCacheBytes + mTargetCacheBytes + mUserChangeDelta;
+    }
+
+    public long getUsedBytes() {
+        // Should it be usedBytes - usedCacheBytes + targetCacheBytes ?
+        return mUsedBytes;
+    }
+
+    public long getFreeBytes() {
+        return mTotalBytes - mUsedBytes;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/CaptureAnimation.java b/src/com/android/gallery3d/ui/CaptureAnimation.java
new file mode 100644
index 0000000..87c054a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CaptureAnimation.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+public class CaptureAnimation {
+    // The amount of change for zooming out.
+    private static final float ZOOM_DELTA = 0.2f;
+    // Pre-calculated value for convenience.
+    private static final float ZOOM_IN_BEGIN = 1f - ZOOM_DELTA;
+
+    private static final Interpolator sZoomOutInterpolator =
+            new DecelerateInterpolator();
+    private static final Interpolator sZoomInInterpolator =
+            new AccelerateInterpolator();
+    private static final Interpolator sSlideInterpolator =
+        new AccelerateDecelerateInterpolator();
+
+    // Calculate the slide factor based on the give time fraction.
+    public static float calculateSlide(float fraction) {
+        return sSlideInterpolator.getInterpolation(fraction);
+    }
+
+    // Calculate the scale factor based on the given time fraction.
+    public static float calculateScale(float fraction) {
+        float value;
+        if (fraction <= 0.5f) {
+            // Zoom in for the beginning.
+            value = 1f - ZOOM_DELTA *
+                    sZoomOutInterpolator.getInterpolation(fraction * 2);
+        } else {
+            // Zoom out for the ending.
+            value = ZOOM_IN_BEGIN + ZOOM_DELTA *
+                    sZoomInInterpolator.getInterpolation((fraction - 0.5f) * 2f);
+        }
+        return value;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsAddressResolver.java b/src/com/android/gallery3d/ui/DetailsAddressResolver.java
new file mode 100644
index 0000000..8de6677
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsAddressResolver.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.ui;
+
+import android.content.Context;
+import android.location.Address;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ReverseGeocoder;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class DetailsAddressResolver {
+    private AddressResolvingListener mListener;
+    private final AbstractGalleryActivity mContext;
+    private Future<Address> mAddressLookupJob;
+    private final Handler mHandler;
+
+    private class AddressLookupJob implements Job<Address> {
+        private double[] mLatlng;
+
+        protected AddressLookupJob(double[] latlng) {
+            mLatlng = latlng;
+        }
+
+        @Override
+        public Address run(JobContext jc) {
+            ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext());
+            return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true);
+        }
+    }
+
+    public interface AddressResolvingListener {
+        public void onAddressAvailable(String address);
+    }
+
+    public DetailsAddressResolver(AbstractGalleryActivity context) {
+        mContext = context;
+        mHandler = new Handler(Looper.getMainLooper());
+    }
+
+    public String resolveAddress(double[] latlng, AddressResolvingListener listener) {
+        mListener = listener;
+        mAddressLookupJob = mContext.getThreadPool().submit(
+                new AddressLookupJob(latlng),
+                new FutureListener<Address>() {
+                    @Override
+                    public void onFutureDone(final Future<Address> future) {
+                        mAddressLookupJob = null;
+                        if (!future.isCancelled()) {
+                            mHandler.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    updateLocation(future.get());
+                                }
+                            });
+                        }
+                    }
+                });
+        return GalleryUtils.formatLatitudeLongitude("(%f,%f)", latlng[0], latlng[1]);
+    }
+
+    private void updateLocation(Address address) {
+        if (address != null) {
+            Context context = mContext.getAndroidContext();
+            String parts[] = {
+                address.getAdminArea(),
+                address.getSubAdminArea(),
+                address.getLocality(),
+                address.getSubLocality(),
+                address.getThoroughfare(),
+                address.getSubThoroughfare(),
+                address.getPremises(),
+                address.getPostalCode(),
+                address.getCountryName()
+            };
+
+            String addressText = "";
+            for (int i = 0; i < parts.length; i++) {
+                if (parts[i] == null || parts[i].isEmpty()) continue;
+                if (!addressText.isEmpty()) {
+                    addressText += ", ";
+                }
+                addressText += parts[i];
+            }
+            String text = String.format("%s : %s", DetailsHelper.getDetailsName(
+                    context, MediaDetails.INDEX_LOCATION), addressText);
+            mListener.onAddressAvailable(text);
+        }
+    }
+
+    public void cancel() {
+        if (mAddressLookupJob != null) {
+            mAddressLookupJob.cancel();
+            mAddressLookupJob = null;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsHelper.java b/src/com/android/gallery3d/ui/DetailsHelper.java
new file mode 100644
index 0000000..47296f6
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsHelper.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.view.View.MeasureSpec;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener;
+
+public class DetailsHelper {
+    private static DetailsAddressResolver sAddressResolver;
+    private DetailsViewContainer mContainer;
+
+    public interface DetailsSource {
+        public int size();
+        public int setIndex();
+        public MediaDetails getDetails();
+    }
+
+    public interface CloseListener {
+        public void onClose();
+    }
+
+    public interface DetailsViewContainer {
+        public void reloadDetails();
+        public void setCloseListener(CloseListener listener);
+        public void show();
+        public void hide();
+    }
+
+    public interface ResolutionResolvingListener {
+        public void onResolutionAvailable(int width, int height);
+    }
+
+    public DetailsHelper(AbstractGalleryActivity activity, GLView rootPane, DetailsSource source) {
+        mContainer = new DialogDetailsView(activity, source);
+    }
+
+    public void layout(int left, int top, int right, int bottom) {
+        if (mContainer instanceof GLView) {
+            GLView view = (GLView) mContainer;
+            view.measure(MeasureSpec.UNSPECIFIED,
+                    MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.AT_MOST));
+            view.layout(0, top, view.getMeasuredWidth(), top + view.getMeasuredHeight());
+        }
+    }
+
+    public void reloadDetails() {
+        mContainer.reloadDetails();
+    }
+
+    public void setCloseListener(CloseListener listener) {
+        mContainer.setCloseListener(listener);
+    }
+
+    public static String resolveAddress(AbstractGalleryActivity activity, double[] latlng,
+            AddressResolvingListener listener) {
+        if (sAddressResolver == null) {
+            sAddressResolver = new DetailsAddressResolver(activity);
+        } else {
+            sAddressResolver.cancel();
+        }
+        return sAddressResolver.resolveAddress(latlng, listener);
+    }
+
+    public static void resolveResolution(String path, ResolutionResolvingListener listener) {
+        Bitmap bitmap = BitmapFactory.decodeFile(path);
+        if (bitmap == null) return;
+        listener.onResolutionAvailable(bitmap.getWidth(), bitmap.getHeight());
+    }
+
+    public static void pause() {
+        if (sAddressResolver != null) sAddressResolver.cancel();
+    }
+
+    public void show() {
+        mContainer.show();
+    }
+
+    public void hide() {
+        mContainer.hide();
+    }
+
+    public static String getDetailsName(Context context, int key) {
+        switch (key) {
+            case MediaDetails.INDEX_TITLE:
+                return context.getString(R.string.title);
+            case MediaDetails.INDEX_DESCRIPTION:
+                return context.getString(R.string.description);
+            case MediaDetails.INDEX_DATETIME:
+                return context.getString(R.string.time);
+            case MediaDetails.INDEX_LOCATION:
+                return context.getString(R.string.location);
+            case MediaDetails.INDEX_PATH:
+                return context.getString(R.string.path);
+            case MediaDetails.INDEX_WIDTH:
+                return context.getString(R.string.width);
+            case MediaDetails.INDEX_HEIGHT:
+                return context.getString(R.string.height);
+            case MediaDetails.INDEX_ORIENTATION:
+                return context.getString(R.string.orientation);
+            case MediaDetails.INDEX_DURATION:
+                return context.getString(R.string.duration);
+            case MediaDetails.INDEX_MIMETYPE:
+                return context.getString(R.string.mimetype);
+            case MediaDetails.INDEX_SIZE:
+                return context.getString(R.string.file_size);
+            case MediaDetails.INDEX_MAKE:
+                return context.getString(R.string.maker);
+            case MediaDetails.INDEX_MODEL:
+                return context.getString(R.string.model);
+            case MediaDetails.INDEX_FLASH:
+                return context.getString(R.string.flash);
+            case MediaDetails.INDEX_APERTURE:
+                return context.getString(R.string.aperture);
+            case MediaDetails.INDEX_FOCAL_LENGTH:
+                return context.getString(R.string.focal_length);
+            case MediaDetails.INDEX_WHITE_BALANCE:
+                return context.getString(R.string.white_balance);
+            case MediaDetails.INDEX_EXPOSURE_TIME:
+                return context.getString(R.string.exposure_time);
+            case MediaDetails.INDEX_ISO:
+                return context.getString(R.string.iso);
+            default:
+                return "Unknown key" + key;
+        }
+    }
+}
+
+
diff --git a/src/com/android/gallery3d/ui/DialogDetailsView.java b/src/com/android/gallery3d/ui/DialogDetailsView.java
new file mode 100644
index 0000000..058c036
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DialogDetailsView.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.ui;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
+import com.android.gallery3d.ui.DetailsHelper.DetailsViewContainer;
+import com.android.gallery3d.ui.DetailsHelper.ResolutionResolvingListener;
+
+import java.util.ArrayList;
+import java.util.Map.Entry;
+
+public class DialogDetailsView implements DetailsViewContainer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "DialogDetailsView";
+
+    private final AbstractGalleryActivity mActivity;
+    private DetailsAdapter mAdapter;
+    private MediaDetails mDetails;
+    private final DetailsSource mSource;
+    private int mIndex;
+    private Dialog mDialog;
+    private CloseListener mListener;
+
+    public DialogDetailsView(AbstractGalleryActivity activity, DetailsSource source) {
+        mActivity = activity;
+        mSource = source;
+    }
+
+    @Override
+    public void show() {
+        reloadDetails();
+        mDialog.show();
+    }
+
+    @Override
+    public void hide() {
+        mDialog.hide();
+    }
+
+    @Override
+    public void reloadDetails() {
+        int index = mSource.setIndex();
+        if (index == -1) return;
+        MediaDetails details = mSource.getDetails();
+        if (details != null) {
+            if (mIndex == index && mDetails == details) return;
+            mIndex = index;
+            mDetails = details;
+            setDetails(details);
+        }
+    }
+
+    private void setDetails(MediaDetails details) {
+        mAdapter = new DetailsAdapter(details);
+        String title = String.format(
+                mActivity.getAndroidContext().getString(R.string.details_title),
+                mIndex + 1, mSource.size());
+        ListView detailsList = (ListView) LayoutInflater.from(mActivity.getAndroidContext()).inflate(
+                R.layout.details_list, null, false);
+        detailsList.setAdapter(mAdapter);
+        mDialog = new AlertDialog.Builder(mActivity)
+            .setView(detailsList)
+            .setTitle(title)
+            .setPositiveButton(R.string.close, new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int whichButton) {
+                    mDialog.dismiss();
+                }
+            })
+            .create();
+
+        mDialog.setOnDismissListener(new OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                if (mListener != null) {
+                    mListener.onClose();
+                }
+            }
+        });
+    }
+
+
+    private class DetailsAdapter extends BaseAdapter
+        implements AddressResolvingListener, ResolutionResolvingListener {
+        private final ArrayList<String> mItems;
+        private int mLocationIndex;
+        private int mWidthIndex = -1;
+        private int mHeightIndex = -1;
+
+        public DetailsAdapter(MediaDetails details) {
+            Context context = mActivity.getAndroidContext();
+            mItems = new ArrayList<String>(details.size());
+            mLocationIndex = -1;
+            setDetails(context, details);
+        }
+
+        private void setDetails(Context context, MediaDetails details) {
+            boolean resolutionIsValid = true;
+            String path = null;
+            for (Entry<Integer, Object> detail : details) {
+                String value;
+                switch (detail.getKey()) {
+                    case MediaDetails.INDEX_LOCATION: {
+                        double[] latlng = (double[]) detail.getValue();
+                        mLocationIndex = mItems.size();
+                        value = DetailsHelper.resolveAddress(mActivity, latlng, this);
+                        break;
+                    }
+                    case MediaDetails.INDEX_SIZE: {
+                        value = Formatter.formatFileSize(
+                                context, (Long) detail.getValue());
+                        break;
+                    }
+                    case MediaDetails.INDEX_WHITE_BALANCE: {
+                        value = "1".equals(detail.getValue())
+                                ? context.getString(R.string.manual)
+                                : context.getString(R.string.auto);
+                        break;
+                    }
+                    case MediaDetails.INDEX_FLASH: {
+                        MediaDetails.FlashState flash =
+                                (MediaDetails.FlashState) detail.getValue();
+                        // TODO: camera doesn't fill in the complete values, show more information
+                        // when it is fixed.
+                        if (flash.isFlashFired()) {
+                            value = context.getString(R.string.flash_on);
+                        } else {
+                            value = context.getString(R.string.flash_off);
+                        }
+                        break;
+                    }
+                    case MediaDetails.INDEX_EXPOSURE_TIME: {
+                        value = (String) detail.getValue();
+                        double time = Double.valueOf(value);
+                        if (time < 1.0f) {
+                            value = String.format("1/%d", (int) (0.5f + 1 / time));
+                        } else {
+                            int integer = (int) time;
+                            time -= integer;
+                            value = String.valueOf(integer) + "''";
+                            if (time > 0.0001) {
+                                value += String.format(" 1/%d", (int) (0.5f + 1 / time));
+                            }
+                        }
+                        break;
+                    }
+                    case MediaDetails.INDEX_WIDTH:
+                        mWidthIndex = mItems.size();
+                        value = detail.getValue().toString();
+                        if (value.equalsIgnoreCase("0")) {
+                            value = context.getString(R.string.unknown);
+                            resolutionIsValid = false;
+                        }
+                        break;
+                    case MediaDetails.INDEX_HEIGHT: {
+                        mHeightIndex = mItems.size();
+                        value = detail.getValue().toString();
+                        if (value.equalsIgnoreCase("0")) {
+                            value = context.getString(R.string.unknown);
+                            resolutionIsValid = false;
+                        }
+                        break;
+                    }
+                    case MediaDetails.INDEX_PATH:
+                        // Get the path and then fall through to the default case
+                        path = detail.getValue().toString();
+                    default: {
+                        Object valueObj = detail.getValue();
+                        // This shouldn't happen, log its key to help us diagnose the problem.
+                        if (valueObj == null) {
+                            Utils.fail("%s's value is Null",
+                                    DetailsHelper.getDetailsName(context, detail.getKey()));
+                        }
+                        value = valueObj.toString();
+                    }
+                }
+                int key = detail.getKey();
+                if (details.hasUnit(key)) {
+                    value = String.format("%s: %s %s", DetailsHelper.getDetailsName(
+                            context, key), value, context.getString(details.getUnit(key)));
+                } else {
+                    value = String.format("%s: %s", DetailsHelper.getDetailsName(
+                            context, key), value);
+                }
+                mItems.add(value);
+                if (!resolutionIsValid) {
+                    DetailsHelper.resolveResolution(path, this);
+                }
+            }
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return false;
+        }
+
+        @Override
+        public int getCount() {
+            return mItems.size();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mDetails.getDetail(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            TextView tv;
+            if (convertView == null) {
+                tv = (TextView) LayoutInflater.from(mActivity.getAndroidContext()).inflate(
+                        R.layout.details, parent, false);
+            } else {
+                tv = (TextView) convertView;
+            }
+            tv.setText(mItems.get(position));
+            return tv;
+        }
+
+        @Override
+        public void onAddressAvailable(String address) {
+            mItems.set(mLocationIndex, address);
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onResolutionAvailable(int width, int height) {
+            if (width == 0 || height == 0) return;
+            // Update the resolution with the new width and height
+            Context context = mActivity.getAndroidContext();
+            String widthString = String.format("%s: %d", DetailsHelper.getDetailsName(
+                    context, MediaDetails.INDEX_WIDTH), width);
+            String heightString = String.format("%s: %d", DetailsHelper.getDetailsName(
+                    context, MediaDetails.INDEX_HEIGHT), height);
+            mItems.set(mWidthIndex, String.valueOf(widthString));
+            mItems.set(mHeightIndex, String.valueOf(heightString));
+            notifyDataSetChanged();
+        }
+    }
+
+    @Override
+    public void setCloseListener(CloseListener listener) {
+        mListener = listener;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java
new file mode 100644
index 0000000..19db772
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DownUpDetector.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.view.MotionEvent;
+
+public class DownUpDetector {
+    public interface DownUpListener {
+        void onDown(MotionEvent e);
+        void onUp(MotionEvent e);
+    }
+
+    private boolean mStillDown;
+    private DownUpListener mListener;
+
+    public DownUpDetector(DownUpListener listener) {
+        mListener = listener;
+    }
+
+    private void setState(boolean down, MotionEvent e) {
+        if (down == mStillDown) return;
+        mStillDown = down;
+        if (down) {
+            mListener.onDown(e);
+        } else {
+            mListener.onUp(e);
+        }
+    }
+
+    public void onTouchEvent(MotionEvent ev) {
+        switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+        case MotionEvent.ACTION_DOWN:
+            setState(true, ev);
+            break;
+
+        case MotionEvent.ACTION_UP:
+        case MotionEvent.ACTION_CANCEL:
+        case MotionEvent.ACTION_POINTER_DOWN:  // Multitouch event - abort.
+            setState(false, ev);
+            break;
+        }
+    }
+
+    public boolean isDown() {
+        return mStillDown;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java
new file mode 100644
index 0000000..87ff0c5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeEffect.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+
+// This is copied from android.widget.EdgeEffect with some small modifications:
+// (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
+// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
+// (3) Use a private Drawable class (which inherits from ResourceTexture)
+//     instead of android.graphics.drawable.Drawable to hold the images.
+//     The private Drawable class is used to translate original Canvas calls to
+//     corresponding GLCanvas calls.
+
+/**
+ * This class performs the graphical effect used at the edges of scrollable widgets
+ * when the user scrolls beyond the content bounds in 2D space.
+ *
+ * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
+ * instance for each edge that should show the effect, feed it input data using
+ * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
+ * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
+ * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
+ * false after drawing, the edge effect's animation is not yet complete and the widget
+ * should schedule another drawing pass to continue the animation.</p>
+ *
+ * <p>When drawing, widgets should draw their main content and child views first,
+ * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
+ * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
+ * The edge effect may then be drawn on top of the view's content using the
+ * {@link #draw(Canvas)} method.</p>
+ */
+public class EdgeEffect {
+    @SuppressWarnings("unused")
+    private static final String TAG = "EdgeEffect";
+
+    // Time it will take the effect to fully recede in ms
+    private static final int RECEDE_TIME = 1000;
+
+    // Time it will take before a pulled glow begins receding in ms
+    private static final int PULL_TIME = 167;
+
+    // Time it will take in ms for a pulled glow to decay to partial strength before release
+    private static final int PULL_DECAY_TIME = 1000;
+
+    private static final float MAX_ALPHA = 0.8f;
+    private static final float HELD_EDGE_ALPHA = 0.7f;
+    private static final float HELD_EDGE_SCALE_Y = 0.5f;
+    private static final float HELD_GLOW_ALPHA = 0.5f;
+    private static final float HELD_GLOW_SCALE_Y = 0.5f;
+
+    private static final float MAX_GLOW_HEIGHT = 4.f;
+
+    private static final float PULL_GLOW_BEGIN = 1.f;
+    private static final float PULL_EDGE_BEGIN = 0.6f;
+
+    // Minimum velocity that will be absorbed
+    private static final int MIN_VELOCITY = 100;
+
+    private static final float EPSILON = 0.001f;
+
+    private final Drawable mEdge;
+    private final Drawable mGlow;
+    private int mWidth;
+    private int mHeight;
+    private final int MIN_WIDTH = 300;
+    private final int mMinWidth;
+
+    private float mEdgeAlpha;
+    private float mEdgeScaleY;
+    private float mGlowAlpha;
+    private float mGlowScaleY;
+
+    private float mEdgeAlphaStart;
+    private float mEdgeAlphaFinish;
+    private float mEdgeScaleYStart;
+    private float mEdgeScaleYFinish;
+    private float mGlowAlphaStart;
+    private float mGlowAlphaFinish;
+    private float mGlowScaleYStart;
+    private float mGlowScaleYFinish;
+
+    private long mStartTime;
+    private float mDuration;
+
+    private final Interpolator mInterpolator;
+
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PULL = 1;
+    private static final int STATE_ABSORB = 2;
+    private static final int STATE_RECEDE = 3;
+    private static final int STATE_PULL_DECAY = 4;
+
+    // How much dragging should effect the height of the edge image.
+    // Number determined by user testing.
+    private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
+
+    // How much dragging should effect the height of the glow image.
+    // Number determined by user testing.
+    private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
+    private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
+
+    private static final int VELOCITY_EDGE_FACTOR = 8;
+    private static final int VELOCITY_GLOW_FACTOR = 16;
+
+    private int mState = STATE_IDLE;
+
+    private float mPullDistance;
+
+    /**
+     * Construct a new EdgeEffect with a theme appropriate for the provided context.
+     * @param context Context used to provide theming and resource information for the EdgeEffect
+     */
+    public EdgeEffect(Context context) {
+        mEdge = new Drawable(context, R.drawable.overscroll_edge);
+        mGlow = new Drawable(context, R.drawable.overscroll_glow);
+
+        mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f);
+        mInterpolator = new DecelerateInterpolator();
+    }
+
+    /**
+     * Set the size of this edge effect in pixels.
+     *
+     * @param width Effect width in pixels
+     * @param height Effect height in pixels
+     */
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+    }
+
+    /**
+     * Reports if this EdgeEffect's animation is finished. If this method returns false
+     * after a call to {@link #draw(Canvas)} the host widget should schedule another
+     * drawing pass to continue the animation.
+     *
+     * @return true if animation is finished, false if drawing should continue on the next frame.
+     */
+    public boolean isFinished() {
+        return mState == STATE_IDLE;
+    }
+
+    /**
+     * Immediately finish the current animation.
+     * After this call {@link #isFinished()} will return true.
+     */
+    public void finish() {
+        mState = STATE_IDLE;
+    }
+
+    /**
+     * A view should call this when content is pulled away from an edge by the user.
+     * This will update the state of the current visual effect and its associated animation.
+     * The host view should always {@link android.view.View#invalidate()} after this
+     * and draw the results accordingly.
+     *
+     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+     *                      1.f (full length of the view) or negative values to express change
+     *                      back toward the edge reached to initiate the effect.
+     */
+    public void onPull(float deltaDistance) {
+        final long now = AnimationTime.get();
+        if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
+            return;
+        }
+        if (mState != STATE_PULL) {
+            mGlowScaleY = PULL_GLOW_BEGIN;
+        }
+        mState = STATE_PULL;
+
+        mStartTime = now;
+        mDuration = PULL_TIME;
+
+        mPullDistance += deltaDistance;
+        float distance = Math.abs(mPullDistance);
+
+        mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
+        mEdgeScaleY = mEdgeScaleYStart = Math.max(
+                HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
+
+        mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
+                mGlowAlpha +
+                (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
+
+        float glowChange = Math.abs(deltaDistance);
+        if (deltaDistance > 0 && mPullDistance < 0) {
+            glowChange = -glowChange;
+        }
+        if (mPullDistance == 0) {
+            mGlowScaleY = 0;
+        }
+
+        // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
+        mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
+                0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
+
+        mEdgeAlphaFinish = mEdgeAlpha;
+        mEdgeScaleYFinish = mEdgeScaleY;
+        mGlowAlphaFinish = mGlowAlpha;
+        mGlowScaleYFinish = mGlowScaleY;
+    }
+
+    /**
+     * Call when the object is released after being pulled.
+     * This will begin the "decay" phase of the effect. After calling this method
+     * the host view should {@link android.view.View#invalidate()} and thereby
+     * draw the results accordingly.
+     */
+    public void onRelease() {
+        mPullDistance = 0;
+
+        if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
+            return;
+        }
+
+        mState = STATE_RECEDE;
+        mEdgeAlphaStart = mEdgeAlpha;
+        mEdgeScaleYStart = mEdgeScaleY;
+        mGlowAlphaStart = mGlowAlpha;
+        mGlowScaleYStart = mGlowScaleY;
+
+        mEdgeAlphaFinish = 0.f;
+        mEdgeScaleYFinish = 0.f;
+        mGlowAlphaFinish = 0.f;
+        mGlowScaleYFinish = 0.f;
+
+        mStartTime = AnimationTime.get();
+        mDuration = RECEDE_TIME;
+    }
+
+    /**
+     * Call when the effect absorbs an impact at the given velocity.
+     * Used when a fling reaches the scroll boundary.
+     *
+     * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+     * the method <code>getCurrVelocity</code> will provide a reasonable approximation
+     * to use here.</p>
+     *
+     * @param velocity Velocity at impact in pixels per second.
+     */
+    public void onAbsorb(int velocity) {
+        mState = STATE_ABSORB;
+        velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
+
+        mStartTime = AnimationTime.get();
+        mDuration = 0.1f + (velocity * 0.03f);
+
+        // The edge should always be at least partially visible, regardless
+        // of velocity.
+        mEdgeAlphaStart = 0.f;
+        mEdgeScaleY = mEdgeScaleYStart = 0.f;
+        // The glow depends more on the velocity, and therefore starts out
+        // nearly invisible.
+        mGlowAlphaStart = 0.5f;
+        mGlowScaleYStart = 0.f;
+
+        // Factor the velocity by 8. Testing on device shows this works best to
+        // reflect the strength of the user's scrolling.
+        mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
+        // Edge should never get larger than the size of its asset.
+        mEdgeScaleYFinish = Math.max(
+                HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
+
+        // Growth for the size of the glow should be quadratic to properly
+        // respond
+        // to a user's scrolling speed. The faster the scrolling speed, the more
+        // intense the effect should be for both the size and the saturation.
+        mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
+        // Alpha should change for the glow as well as size.
+        mGlowAlphaFinish = Math.max(
+                mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
+    }
+
+
+    /**
+     * Draw into the provided canvas. Assumes that the canvas has been rotated
+     * accordingly and the size has been set. The effect will be drawn the full
+     * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+     * 1.f of height.
+     *
+     * @param canvas Canvas to draw into
+     * @return true if drawing should continue beyond this frame to continue the
+     *         animation
+     */
+    public boolean draw(GLCanvas canvas) {
+        update();
+
+        final int edgeHeight = mEdge.getIntrinsicHeight();
+        final int edgeWidth = mEdge.getIntrinsicWidth();
+        final int glowHeight = mGlow.getIntrinsicHeight();
+        final int glowWidth = mGlow.getIntrinsicWidth();
+
+        mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
+
+        int glowBottom = (int) Math.min(
+                glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f,
+                glowHeight * MAX_GLOW_HEIGHT);
+        if (mWidth < mMinWidth) {
+            // Center the glow and clip it.
+            int glowLeft = (mWidth - mMinWidth)/2;
+            mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
+        } else {
+            // Stretch the glow to fit.
+            mGlow.setBounds(0, 0, mWidth, glowBottom);
+        }
+
+        mGlow.draw(canvas);
+
+        mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
+
+        int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
+        if (mWidth < mMinWidth) {
+            // Center the edge and clip it.
+            int edgeLeft = (mWidth - mMinWidth)/2;
+            mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
+        } else {
+            // Stretch the edge to fit.
+            mEdge.setBounds(0, 0, mWidth, edgeBottom);
+        }
+        mEdge.draw(canvas);
+
+        return mState != STATE_IDLE;
+    }
+
+    private void update() {
+        final long time = AnimationTime.get();
+        final float t = Math.min((time - mStartTime) / mDuration, 1.f);
+
+        final float interp = mInterpolator.getInterpolation(t);
+
+        mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
+        mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
+        mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
+        mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
+
+        if (t >= 1.f - EPSILON) {
+            switch (mState) {
+                case STATE_ABSORB:
+                    mState = STATE_RECEDE;
+                    mStartTime = AnimationTime.get();
+                    mDuration = RECEDE_TIME;
+
+                    mEdgeAlphaStart = mEdgeAlpha;
+                    mEdgeScaleYStart = mEdgeScaleY;
+                    mGlowAlphaStart = mGlowAlpha;
+                    mGlowScaleYStart = mGlowScaleY;
+
+                    // After absorb, the glow and edge should fade to nothing.
+                    mEdgeAlphaFinish = 0.f;
+                    mEdgeScaleYFinish = 0.f;
+                    mGlowAlphaFinish = 0.f;
+                    mGlowScaleYFinish = 0.f;
+                    break;
+                case STATE_PULL:
+                    mState = STATE_PULL_DECAY;
+                    mStartTime = AnimationTime.get();
+                    mDuration = PULL_DECAY_TIME;
+
+                    mEdgeAlphaStart = mEdgeAlpha;
+                    mEdgeScaleYStart = mEdgeScaleY;
+                    mGlowAlphaStart = mGlowAlpha;
+                    mGlowScaleYStart = mGlowScaleY;
+
+                    // After pull, the glow and edge should fade to nothing.
+                    mEdgeAlphaFinish = 0.f;
+                    mEdgeScaleYFinish = 0.f;
+                    mGlowAlphaFinish = 0.f;
+                    mGlowScaleYFinish = 0.f;
+                    break;
+                case STATE_PULL_DECAY:
+                    // When receding, we want edge to decrease more slowly
+                    // than the glow.
+                    float factor = mGlowScaleYFinish != 0 ? 1
+                            / (mGlowScaleYFinish * mGlowScaleYFinish)
+                            : Float.MAX_VALUE;
+                    mEdgeScaleY = mEdgeScaleYStart +
+                        (mEdgeScaleYFinish - mEdgeScaleYStart) *
+                            interp * factor;
+                    mState = STATE_RECEDE;
+                    break;
+                case STATE_RECEDE:
+                    mState = STATE_IDLE;
+                    break;
+            }
+        }
+    }
+
+    private static class Drawable extends ResourceTexture {
+        private Rect mBounds = new Rect();
+        private int mAlpha = 255;
+
+        public Drawable(Context context, int resId) {
+            super(context, resId);
+        }
+
+        public int getIntrinsicWidth() {
+            return getWidth();
+        }
+
+        public int getIntrinsicHeight() {
+            return getHeight();
+        }
+
+        public void setBounds(int left, int top, int right, int bottom) {
+            mBounds.set(left, top, right, bottom);
+        }
+
+        public void setAlpha(int alpha) {
+            mAlpha = alpha;
+        }
+
+        public void draw(GLCanvas canvas) {
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(mAlpha / 255.0f);
+            Rect b = mBounds;
+            draw(canvas, b.left, b.top, b.width(), b.height());
+            canvas.restore();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java
new file mode 100644
index 0000000..051de18
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeView.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.ui;
+
+import android.content.Context;
+import android.opengl.Matrix;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+// EdgeView draws EdgeEffect (blue glow) at four sides of the view.
+public class EdgeView extends GLView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "EdgeView";
+
+    public static final int INVALID_DIRECTION = -1;
+    public static final int TOP = 0;
+    public static final int LEFT = 1;
+    public static final int BOTTOM = 2;
+    public static final int RIGHT = 3;
+
+    // Each edge effect has a transform matrix, and each matrix has 16 elements.
+    // We put all the elements in one array. These constants specify the
+    // starting index of each matrix.
+    private static final int TOP_M = TOP * 16;
+    private static final int LEFT_M = LEFT * 16;
+    private static final int BOTTOM_M = BOTTOM * 16;
+    private static final int RIGHT_M = RIGHT * 16;
+
+    private EdgeEffect[] mEffect = new EdgeEffect[4];
+    private float[] mMatrix = new float[4 * 16];
+
+    public EdgeView(Context context) {
+        for (int i = 0; i < 4; i++) {
+            mEffect[i] = new EdgeEffect(context);
+        }
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changeSize, int left, int top, int right, int bottom) {
+        if (!changeSize) return;
+
+        int w = right - left;
+        int h = bottom - top;
+        for (int i = 0; i < 4; i++) {
+            if ((i & 1) == 0) {  // top or bottom
+                mEffect[i].setSize(w, h);
+            } else {  // left or right
+                mEffect[i].setSize(h, w);
+            }
+        }
+
+        // Set up transforms for the four edges. Without transforms an
+        // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y
+        // is some factor < 1. For other edges we need to move, rotate, and
+        // flip the effects into proper places.
+        Matrix.setIdentityM(mMatrix, TOP_M);
+        Matrix.setIdentityM(mMatrix, LEFT_M);
+        Matrix.setIdentityM(mMatrix, BOTTOM_M);
+        Matrix.setIdentityM(mMatrix, RIGHT_M);
+
+        Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1);
+        Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1);
+
+        Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0);
+        Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1);
+
+        Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0);
+        Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1);
+    }
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        super.render(canvas);
+        boolean more = false;
+        for (int i = 0; i < 4; i++) {
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            canvas.multiplyMatrix(mMatrix, i * 16);
+            more |= mEffect[i].draw(canvas);
+            canvas.restore();
+        }
+        if (more) {
+            invalidate();
+        }
+    }
+
+    // Called when the content is pulled away from the edge.
+    // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+    public void onPull(int offset, int direction) {
+        int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight();
+        mEffect[direction].onPull((float)offset / fullLength);
+        if (!mEffect[direction].isFinished()) {
+            invalidate();
+        }
+    }
+
+    // Call when the object is released after being pulled.
+    public void onRelease() {
+        boolean more = false;
+        for (int i = 0; i < 4; i++) {
+            mEffect[i].onRelease();
+            more |= !mEffect[i].isFinished();
+        }
+        if (more) {
+            invalidate();
+        }
+    }
+
+    // Call when the effect absorbs an impact at the given velocity.
+    // Used when a fling reaches the scroll boundary. velocity is in pixels
+    // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+    public void onAbsorb(int velocity, int direction) {
+        mEffect[direction].onAbsorb(velocity);
+        if (!mEffect[direction].isFinished()) {
+            invalidate();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java
new file mode 100644
index 0000000..6f98c64
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FlingScroller.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.ui;
+
+
+// This is a customized version of Scroller, with a interface similar to
+// android.widget.Scroller. It does fling only, not scroll.
+//
+// The differences between the this Scroller and the system one are:
+//
+// (1) The velocity does not change because of min/max limit.
+// (2) The duration is different.
+// (3) The deceleration curve is different.
+class FlingScroller {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FlingController";
+
+    // The fling duration (in milliseconds) when velocity is 1 pixel/second
+    private static final float FLING_DURATION_PARAM = 50f;
+    private static final int DECELERATED_FACTOR = 4;
+
+    private int mStartX, mStartY;
+    private int mMinX, mMinY, mMaxX, mMaxY;
+    private double mSinAngle;
+    private double mCosAngle;
+    private int mDuration;
+    private int mDistance;
+    private int mFinalX, mFinalY;
+
+    private int mCurrX, mCurrY;
+    private double mCurrV;
+
+    public int getFinalX() {
+        return mFinalX;
+    }
+
+    public int getFinalY() {
+        return mFinalY;
+    }
+
+    public int getDuration() {
+        return mDuration;
+    }
+
+    public int getCurrX() {
+        return mCurrX;
+
+    }
+
+    public int getCurrY() {
+        return mCurrY;
+    }
+
+    public int getCurrVelocityX() {
+        return (int)Math.round(mCurrV * mCosAngle);
+    }
+
+    public int getCurrVelocityY() {
+        return (int)Math.round(mCurrV * mSinAngle);
+    }
+
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        mStartX = startX;
+        mStartY = startY;
+        mMinX = minX;
+        mMinY = minY;
+        mMaxX = maxX;
+        mMaxY = maxY;
+
+        double velocity = Math.hypot(velocityX, velocityY);
+        mSinAngle = velocityY / velocity;
+        mCosAngle = velocityX / velocity;
+        //
+        // The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d)
+        //     velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+        // Thus,
+        //     v0 = d * (e - s) / T => (e - s) = v0 * T / d
+        //
+
+        // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second;
+        mDuration = (int)Math.round(FLING_DURATION_PARAM
+                * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1)));
+
+        // (e - s) = v0 * T / d
+        mDistance = (int)Math.round(
+                velocity * mDuration / DECELERATED_FACTOR / 1000);
+
+        mFinalX = getX(1.0f);
+        mFinalY = getY(1.0f);
+    }
+
+    public void computeScrollOffset(float progress) {
+        progress = Math.min(progress, 1);
+        float f = 1 - progress;
+        f = 1 - (float) Math.pow(f, DECELERATED_FACTOR);
+        mCurrX = getX(f);
+        mCurrY = getY(f);
+        mCurrV = getV(progress);
+    }
+
+    private int getX(float f) {
+        int r = (int) Math.round(mStartX + f * mDistance * mCosAngle);
+        if (mCosAngle > 0 && mStartX <= mMaxX) {
+            r = Math.min(r, mMaxX);
+        } else if (mCosAngle < 0 && mStartX >= mMinX) {
+            r = Math.max(r, mMinX);
+        }
+        return r;
+    }
+
+    private int getY(float f) {
+        int r = (int) Math.round(mStartY + f * mDistance * mSinAngle);
+        if (mSinAngle > 0 && mStartY <= mMaxY) {
+            r = Math.min(r, mMaxY);
+        } else if (mSinAngle < 0 && mStartY >= mMinY) {
+            r = Math.max(r, mMinY);
+        }
+        return r;
+    }
+
+    private double getV(float progress) {
+        // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+        return DECELERATED_FACTOR * mDistance * 1000 *
+                Math.pow(1 - progress, DECELERATED_FACTOR - 1) / mDuration;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java
new file mode 100644
index 0000000..33a82ea
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRoot.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Matrix;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public interface GLRoot {
+
+    // Listener will be called when GL is idle AND before each frame.
+    // Mainly used for uploading textures.
+    public static interface OnGLIdleListener {
+        public boolean onGLIdle(
+                GLCanvas canvas, boolean renderRequested);
+    }
+
+    public void addOnGLIdleListener(OnGLIdleListener listener);
+    public void registerLaunchedAnimation(CanvasAnimation animation);
+    public void requestRenderForced();
+    public void requestRender();
+    public void requestLayoutContentPane();
+
+    public void lockRenderThread();
+    public void unlockRenderThread();
+
+    public void setContentPane(GLView content);
+    public void setOrientationSource(OrientationSource source);
+    public int getDisplayRotation();
+    public int getCompensation();
+    public Matrix getCompensationMatrix();
+    public void freeze();
+    public void unfreeze();
+    public void setLightsOutMode(boolean enabled);
+
+    public Context getContext();
+}
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
new file mode 100644
index 0000000..dc898d8
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.GLES11Canvas;
+import com.android.gallery3d.glrenderer.GLES20Canvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MotionEventHelper;
+import com.android.gallery3d.util.Profile;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+// The root component of all <code>GLView</code>s. The rendering is done in GL
+// thread while the event handling is done in the main thread.  To synchronize
+// the two threads, the entry points of this package need to synchronize on the
+// <code>GLRootView</code> instance unless it can be proved that the rendering
+// thread won't access the same thing as the method. The entry points include:
+// (1) The public methods of HeadUpDisplay
+// (2) The public methods of CameraHeadUpDisplay
+// (3) The overridden methods in GLRootView.
+public class GLRootView extends GLSurfaceView
+        implements GLSurfaceView.Renderer, GLRoot {
+    private static final String TAG = "GLRootView";
+
+    private static final boolean DEBUG_FPS = false;
+    private int mFrameCount = 0;
+    private long mFrameCountingStart = 0;
+
+    private static final boolean DEBUG_INVALIDATE = false;
+    private int mInvalidateColor = 0;
+
+    private static final boolean DEBUG_DRAWING_STAT = false;
+
+    private static final boolean DEBUG_PROFILE = false;
+    private static final boolean DEBUG_PROFILE_SLOW_ONLY = false;
+
+    private static final int FLAG_INITIALIZED = 1;
+    private static final int FLAG_NEED_LAYOUT = 2;
+
+    private GL11 mGL;
+    private GLCanvas mCanvas;
+    private GLView mContentView;
+
+    private OrientationSource mOrientationSource;
+    // mCompensation is the difference between the UI orientation on GLCanvas
+    // and the framework orientation. See OrientationManager for details.
+    private int mCompensation;
+    // mCompensationMatrix maps the coordinates of touch events. It is kept sync
+    // with mCompensation.
+    private Matrix mCompensationMatrix = new Matrix();
+    private int mDisplayRotation;
+
+    private int mFlags = FLAG_NEED_LAYOUT;
+    private volatile boolean mRenderRequested = false;
+
+    private final ArrayList<CanvasAnimation> mAnimations =
+            new ArrayList<CanvasAnimation>();
+
+    private final ArrayDeque<OnGLIdleListener> mIdleListeners =
+            new ArrayDeque<OnGLIdleListener>();
+
+    private final IdleRunner mIdleRunner = new IdleRunner();
+
+    private final ReentrantLock mRenderLock = new ReentrantLock();
+    private final Condition mFreezeCondition =
+            mRenderLock.newCondition();
+    private boolean mFreeze;
+
+    private long mLastDrawFinishTime;
+    private boolean mInDownState = false;
+    private boolean mFirstDraw = true;
+
+    public GLRootView(Context context) {
+        this(context, null);
+    }
+
+    public GLRootView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mFlags |= FLAG_INITIALIZED;
+        setBackgroundDrawable(null);
+        setEGLContextClientVersion(ApiHelper.HAS_GLES20_REQUIRED ? 2 : 1);
+        if (ApiHelper.USE_888_PIXEL_FORMAT) {
+            setEGLConfigChooser(8, 8, 8, 0, 0, 0);
+        } else {
+            setEGLConfigChooser(5, 6, 5, 0, 0, 0);
+        }
+        setRenderer(this);
+        if (ApiHelper.USE_888_PIXEL_FORMAT) {
+            getHolder().setFormat(PixelFormat.RGB_888);
+        } else {
+            getHolder().setFormat(PixelFormat.RGB_565);
+        }
+
+        // Uncomment this to enable gl error check.
+        // setDebugFlags(DEBUG_CHECK_GL_ERROR);
+    }
+
+    @Override
+    public void registerLaunchedAnimation(CanvasAnimation animation) {
+        // Register the newly launched animation so that we can set the start
+        // time more precisely. (Usually, it takes much longer for first
+        // rendering, so we set the animation start time as the time we
+        // complete rendering)
+        mAnimations.add(animation);
+    }
+
+    @Override
+    public void addOnGLIdleListener(OnGLIdleListener listener) {
+        synchronized (mIdleListeners) {
+            mIdleListeners.addLast(listener);
+            mIdleRunner.enable();
+        }
+    }
+
+    @Override
+    public void setContentPane(GLView content) {
+        if (mContentView == content) return;
+        if (mContentView != null) {
+            if (mInDownState) {
+                long now = SystemClock.uptimeMillis();
+                MotionEvent cancelEvent = MotionEvent.obtain(
+                        now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+                mContentView.dispatchTouchEvent(cancelEvent);
+                cancelEvent.recycle();
+                mInDownState = false;
+            }
+            mContentView.detachFromRoot();
+            BasicTexture.yieldAllTextures();
+        }
+        mContentView = content;
+        if (content != null) {
+            content.attachToRoot(this);
+            requestLayoutContentPane();
+        }
+    }
+
+    @Override
+    public void requestRenderForced() {
+        superRequestRender();
+    }
+
+    @Override
+    public void requestRender() {
+        if (DEBUG_INVALIDATE) {
+            StackTraceElement e = Thread.currentThread().getStackTrace()[4];
+            String caller = e.getFileName() + ":" + e.getLineNumber() + " ";
+            Log.d(TAG, "invalidate: " + caller);
+        }
+        if (mRenderRequested) return;
+        mRenderRequested = true;
+        if (ApiHelper.HAS_POST_ON_ANIMATION) {
+            postOnAnimation(mRequestRenderOnAnimationFrame);
+        } else {
+            super.requestRender();
+        }
+    }
+
+    private Runnable mRequestRenderOnAnimationFrame = new Runnable() {
+        @Override
+        public void run() {
+            superRequestRender();
+        }
+    };
+
+    private void superRequestRender() {
+        super.requestRender();
+    }
+
+    @Override
+    public void requestLayoutContentPane() {
+        mRenderLock.lock();
+        try {
+            if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return;
+
+            // "View" system will invoke onLayout() for initialization(bug ?), we
+            // have to ignore it since the GLThread is not ready yet.
+            if ((mFlags & FLAG_INITIALIZED) == 0) return;
+
+            mFlags |= FLAG_NEED_LAYOUT;
+            requestRender();
+        } finally {
+            mRenderLock.unlock();
+        }
+    }
+
+    private void layoutContentPane() {
+        mFlags &= ~FLAG_NEED_LAYOUT;
+
+        int w = getWidth();
+        int h = getHeight();
+        int displayRotation = 0;
+        int compensation = 0;
+
+        // Get the new orientation values
+        if (mOrientationSource != null) {
+            displayRotation = mOrientationSource.getDisplayRotation();
+            compensation = mOrientationSource.getCompensation();
+        } else {
+            displayRotation = 0;
+            compensation = 0;
+        }
+
+        if (mCompensation != compensation) {
+            mCompensation = compensation;
+            if (mCompensation % 180 != 0) {
+                mCompensationMatrix.setRotate(mCompensation);
+                // move center to origin before rotation
+                mCompensationMatrix.preTranslate(-w / 2, -h / 2);
+                // align with the new origin after rotation
+                mCompensationMatrix.postTranslate(h / 2, w / 2);
+            } else {
+                mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2);
+            }
+        }
+        mDisplayRotation = displayRotation;
+
+        // Do the actual layout.
+        if (mCompensation % 180 != 0) {
+            int tmp = w;
+            w = h;
+            h = tmp;
+        }
+        Log.i(TAG, "layout content pane " + w + "x" + h
+                + " (compensation " + mCompensation + ")");
+        if (mContentView != null && w != 0 && h != 0) {
+            mContentView.layout(0, 0, w, h);
+        }
+        // Uncomment this to dump the view hierarchy.
+        //mContentView.dumpTree("");
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changed, int left, int top, int right, int bottom) {
+        if (changed) requestLayoutContentPane();
+    }
+
+    /**
+     * Called when the context is created, possibly after automatic destruction.
+     */
+    // This is a GLSurfaceView.Renderer callback
+    @Override
+    public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
+        GL11 gl = (GL11) gl1;
+        if (mGL != null) {
+            // The GL Object has changed
+            Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
+        }
+        mRenderLock.lock();
+        try {
+            mGL = gl;
+            mCanvas = ApiHelper.HAS_GLES20_REQUIRED ? new GLES20Canvas() : new GLES11Canvas(gl);
+            BasicTexture.invalidateAllTextures();
+        } finally {
+            mRenderLock.unlock();
+        }
+
+        if (DEBUG_FPS || DEBUG_PROFILE) {
+            setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+        } else {
+            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+        }
+    }
+
+    /**
+     * Called when the OpenGL surface is recreated without destroying the
+     * context.
+     */
+    // This is a GLSurfaceView.Renderer callback
+    @Override
+    public void onSurfaceChanged(GL10 gl1, int width, int height) {
+        Log.i(TAG, "onSurfaceChanged: " + width + "x" + height
+                + ", gl10: " + gl1.toString());
+        Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
+        GalleryUtils.setRenderThread();
+        if (DEBUG_PROFILE) {
+            Log.d(TAG, "Start profiling");
+            Profile.enable(20);  // take a sample every 20ms
+        }
+        GL11 gl = (GL11) gl1;
+        Utils.assertTrue(mGL == gl);
+
+        mCanvas.setSize(width, height);
+    }
+
+    private void outputFps() {
+        long now = System.nanoTime();
+        if (mFrameCountingStart == 0) {
+            mFrameCountingStart = now;
+        } else if ((now - mFrameCountingStart) > 1000000000) {
+            Log.d(TAG, "fps: " + (double) mFrameCount
+                    * 1000000000 / (now - mFrameCountingStart));
+            mFrameCountingStart = now;
+            mFrameCount = 0;
+        }
+        ++mFrameCount;
+    }
+
+    @Override
+    public void onDrawFrame(GL10 gl) {
+        AnimationTime.update();
+        long t0;
+        if (DEBUG_PROFILE_SLOW_ONLY) {
+            Profile.hold();
+            t0 = System.nanoTime();
+        }
+        mRenderLock.lock();
+
+        while (mFreeze) {
+            mFreezeCondition.awaitUninterruptibly();
+        }
+
+        try {
+            onDrawFrameLocked(gl);
+        } finally {
+            mRenderLock.unlock();
+        }
+
+        // We put a black cover View in front of the SurfaceView and hide it
+        // after the first draw. This prevents the SurfaceView being transparent
+        // before the first draw.
+        if (mFirstDraw) {
+            mFirstDraw = false;
+            post(new Runnable() {
+                    @Override
+                    public void run() {
+                        View root = getRootView();
+                        View cover = root.findViewById(R.id.gl_root_cover);
+                        cover.setVisibility(GONE);
+                    }
+                });
+        }
+
+        if (DEBUG_PROFILE_SLOW_ONLY) {
+            long t = System.nanoTime();
+            long durationInMs = (t - mLastDrawFinishTime) / 1000000;
+            long durationDrawInMs = (t - t0) / 1000000;
+            mLastDrawFinishTime = t;
+
+            if (durationInMs > 34) {  // 34ms -> we skipped at least 2 frames
+                Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" +
+                        durationInMs + ") -----");
+                Profile.commit();
+            } else {
+                Profile.drop();
+            }
+        }
+    }
+
+    private void onDrawFrameLocked(GL10 gl) {
+        if (DEBUG_FPS) outputFps();
+
+        // release the unbound textures and deleted buffers.
+        mCanvas.deleteRecycledResources();
+
+        // reset texture upload limit
+        UploadedTexture.resetUploadLimit();
+
+        mRenderRequested = false;
+
+        if ((mOrientationSource != null
+                && mDisplayRotation != mOrientationSource.getDisplayRotation())
+                || (mFlags & FLAG_NEED_LAYOUT) != 0) {
+            layoutContentPane();
+        }
+
+        mCanvas.save(GLCanvas.SAVE_FLAG_ALL);
+        rotateCanvas(-mCompensation);
+        if (mContentView != null) {
+           mContentView.render(mCanvas);
+        } else {
+            // Make sure we always draw something to prevent displaying garbage
+            mCanvas.clearBuffer();
+        }
+        mCanvas.restore();
+
+        if (!mAnimations.isEmpty()) {
+            long now = AnimationTime.get();
+            for (int i = 0, n = mAnimations.size(); i < n; i++) {
+                mAnimations.get(i).setStartTime(now);
+            }
+            mAnimations.clear();
+        }
+
+        if (UploadedTexture.uploadLimitReached()) {
+            requestRender();
+        }
+
+        synchronized (mIdleListeners) {
+            if (!mIdleListeners.isEmpty()) mIdleRunner.enable();
+        }
+
+        if (DEBUG_INVALIDATE) {
+            mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);
+            mInvalidateColor = ~mInvalidateColor;
+        }
+
+        if (DEBUG_DRAWING_STAT) {
+            mCanvas.dumpStatisticsAndClear();
+        }
+    }
+
+    private void rotateCanvas(int degrees) {
+        if (degrees == 0) return;
+        int w = getWidth();
+        int h = getHeight();
+        int cx = w / 2;
+        int cy = h / 2;
+        mCanvas.translate(cx, cy);
+        mCanvas.rotate(degrees, 0, 0, 1);
+        if (degrees % 180 != 0) {
+            mCanvas.translate(-cy, -cx);
+        } else {
+            mCanvas.translate(-cx, -cy);
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (!isEnabled()) return false;
+
+        int action = event.getAction();
+        if (action == MotionEvent.ACTION_CANCEL
+                || action == MotionEvent.ACTION_UP) {
+            mInDownState = false;
+        } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
+            return false;
+        }
+
+        if (mCompensation != 0) {
+            event = MotionEventHelper.transformEvent(event, mCompensationMatrix);
+        }
+
+        mRenderLock.lock();
+        try {
+            // If this has been detached from root, we don't need to handle event
+            boolean handled = mContentView != null
+                    && mContentView.dispatchTouchEvent(event);
+            if (action == MotionEvent.ACTION_DOWN && handled) {
+                mInDownState = true;
+            }
+            return handled;
+        } finally {
+            mRenderLock.unlock();
+        }
+    }
+
+    private class IdleRunner implements Runnable {
+        // true if the idle runner is in the queue
+        private boolean mActive = false;
+
+        @Override
+        public void run() {
+            OnGLIdleListener listener;
+            synchronized (mIdleListeners) {
+                mActive = false;
+                if (mIdleListeners.isEmpty()) return;
+                listener = mIdleListeners.removeFirst();
+            }
+            mRenderLock.lock();
+            boolean keepInQueue;
+            try {
+                keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested);
+            } finally {
+                mRenderLock.unlock();
+            }
+            synchronized (mIdleListeners) {
+                if (keepInQueue) mIdleListeners.addLast(listener);
+                if (!mRenderRequested && !mIdleListeners.isEmpty()) enable();
+            }
+        }
+
+        public void enable() {
+            // Who gets the flag can add it to the queue
+            if (mActive) return;
+            mActive = true;
+            queueEvent(this);
+        }
+    }
+
+    @Override
+    public void lockRenderThread() {
+        mRenderLock.lock();
+    }
+
+    @Override
+    public void unlockRenderThread() {
+        mRenderLock.unlock();
+    }
+
+    @Override
+    public void onPause() {
+        unfreeze();
+        super.onPause();
+        if (DEBUG_PROFILE) {
+            Log.d(TAG, "Stop profiling");
+            Profile.disableAll();
+            Profile.dumpToFile("/sdcard/gallery.prof");
+            Profile.reset();
+        }
+    }
+
+    @Override
+    public void setOrientationSource(OrientationSource source) {
+        mOrientationSource = source;
+    }
+
+    @Override
+    public int getDisplayRotation() {
+        return mDisplayRotation;
+    }
+
+    @Override
+    public int getCompensation() {
+        return mCompensation;
+    }
+
+    @Override
+    public Matrix getCompensationMatrix() {
+        return mCompensationMatrix;
+    }
+
+    @Override
+    public void freeze() {
+        mRenderLock.lock();
+        mFreeze = true;
+        mRenderLock.unlock();
+    }
+
+    @Override
+    public void unfreeze() {
+        mRenderLock.lock();
+        mFreeze = false;
+        mFreezeCondition.signalAll();
+        mRenderLock.unlock();
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    public void setLightsOutMode(boolean enabled) {
+        if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return;
+
+        int flags = 0;
+        if (enabled) {
+            flags = STATUS_BAR_HIDDEN;
+            if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
+                flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE);
+            }
+        }
+        setSystemUiVisibility(flags);
+    }
+
+    // We need to unfreeze in the following methods and in onPause().
+    // These methods will wait on GLThread. If we have freezed the GLRootView,
+    // the GLThread will wait on main thread to call unfreeze and cause dead
+    // lock.
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+        unfreeze();
+        super.surfaceChanged(holder, format, w, h);
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        unfreeze();
+        super.surfaceCreated(holder);
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        unfreeze();
+        super.surfaceDestroyed(holder);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        unfreeze();
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            unfreeze();
+        } finally {
+            super.finalize();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
new file mode 100644
index 0000000..83de19f
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+import java.util.ArrayList;
+
+// GLView is a UI component. It can render to a GLCanvas and accept touch
+// events. A GLView may have zero or more child GLView and they form a tree
+// structure. The rendering and event handling will pass through the tree
+// structure.
+//
+// A GLView tree should be attached to a GLRoot before event dispatching and
+// rendering happens. GLView asks GLRoot to re-render or re-layout the
+// GLView hierarchy using requestRender() and requestLayoutContentPane().
+//
+// The render() method is called in a separate thread. Before calling
+// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the
+// rendering thread running at the same time. If there are other entry points
+// from main thread (like a Handler) in your GLView, you need to call
+// lockRendering() if the rendering thread should not run at the same time.
+//
+public class GLView {
+    private static final String TAG = "GLView";
+
+    public static final int VISIBLE = 0;
+    public static final int INVISIBLE = 1;
+
+    private static final int FLAG_INVISIBLE = 1;
+    private static final int FLAG_SET_MEASURED_SIZE = 2;
+    private static final int FLAG_LAYOUT_REQUESTED = 4;
+
+    public interface OnClickListener {
+        void onClick(GLView v);
+    }
+
+    protected final Rect mBounds = new Rect();
+    protected final Rect mPaddings = new Rect();
+
+    private GLRoot mRoot;
+    protected GLView mParent;
+    private ArrayList<GLView> mComponents;
+    private GLView mMotionTarget;
+
+    private CanvasAnimation mAnimation;
+
+    private int mViewFlags = 0;
+
+    protected int mMeasuredWidth = 0;
+    protected int mMeasuredHeight = 0;
+
+    private int mLastWidthSpec = -1;
+    private int mLastHeightSpec = -1;
+
+    protected int mScrollY = 0;
+    protected int mScrollX = 0;
+    protected int mScrollHeight = 0;
+    protected int mScrollWidth = 0;
+
+    private float [] mBackgroundColor;
+    private StateTransitionAnimation mTransition;
+
+    public void startAnimation(CanvasAnimation animation) {
+        GLRoot root = getGLRoot();
+        if (root == null) throw new IllegalStateException();
+        mAnimation = animation;
+        if (mAnimation != null) {
+            mAnimation.start();
+            root.registerLaunchedAnimation(mAnimation);
+        }
+        invalidate();
+    }
+
+    // Sets the visiblity of this GLView (either GLView.VISIBLE or
+    // GLView.INVISIBLE).
+    public void setVisibility(int visibility) {
+        if (visibility == getVisibility()) return;
+        if (visibility == VISIBLE) {
+            mViewFlags &= ~FLAG_INVISIBLE;
+        } else {
+            mViewFlags |= FLAG_INVISIBLE;
+        }
+        onVisibilityChanged(visibility);
+        invalidate();
+    }
+
+    // Returns GLView.VISIBLE or GLView.INVISIBLE
+    public int getVisibility() {
+        return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
+    }
+
+    // This should only be called on the content pane (the topmost GLView).
+    public void attachToRoot(GLRoot root) {
+        Utils.assertTrue(mParent == null && mRoot == null);
+        onAttachToRoot(root);
+    }
+
+    // This should only be called on the content pane (the topmost GLView).
+    public void detachFromRoot() {
+        Utils.assertTrue(mParent == null && mRoot != null);
+        onDetachFromRoot();
+    }
+
+    // Returns the number of children of the GLView.
+    public int getComponentCount() {
+        return mComponents == null ? 0 : mComponents.size();
+    }
+
+    // Returns the children for the given index.
+    public GLView getComponent(int index) {
+        if (mComponents == null) {
+            throw new ArrayIndexOutOfBoundsException(index);
+        }
+        return mComponents.get(index);
+    }
+
+    // Adds a child to this GLView.
+    public void addComponent(GLView component) {
+        // Make sure the component doesn't have a parent currently.
+        if (component.mParent != null) throw new IllegalStateException();
+
+        // Build parent-child links
+        if (mComponents == null) {
+            mComponents = new ArrayList<GLView>();
+        }
+        mComponents.add(component);
+        component.mParent = this;
+
+        // If this is added after we have a root, tell the component.
+        if (mRoot != null) {
+            component.onAttachToRoot(mRoot);
+        }
+    }
+
+    // Removes a child from this GLView.
+    public boolean removeComponent(GLView component) {
+        if (mComponents == null) return false;
+        if (mComponents.remove(component)) {
+            removeOneComponent(component);
+            return true;
+        }
+        return false;
+    }
+
+    // Removes all children of this GLView.
+    public void removeAllComponents() {
+        for (int i = 0, n = mComponents.size(); i < n; ++i) {
+            removeOneComponent(mComponents.get(i));
+        }
+        mComponents.clear();
+    }
+
+    private void removeOneComponent(GLView component) {
+        if (mMotionTarget == component) {
+            long now = SystemClock.uptimeMillis();
+            MotionEvent cancelEvent = MotionEvent.obtain(
+                    now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+            dispatchTouchEvent(cancelEvent);
+            cancelEvent.recycle();
+        }
+        component.onDetachFromRoot();
+        component.mParent = null;
+    }
+
+    public Rect bounds() {
+        return mBounds;
+    }
+
+    public int getWidth() {
+        return mBounds.right - mBounds.left;
+    }
+
+    public int getHeight() {
+        return mBounds.bottom - mBounds.top;
+    }
+
+    public GLRoot getGLRoot() {
+        return mRoot;
+    }
+
+    // Request re-rendering of the view hierarchy.
+    // This is used for animation or when the contents changed.
+    public void invalidate() {
+        GLRoot root = getGLRoot();
+        if (root != null) root.requestRender();
+    }
+
+    // Request re-layout of the view hierarchy.
+    public void requestLayout() {
+        mViewFlags |= FLAG_LAYOUT_REQUESTED;
+        mLastHeightSpec = -1;
+        mLastWidthSpec = -1;
+        if (mParent != null) {
+            mParent.requestLayout();
+        } else {
+            // Is this a content pane ?
+            GLRoot root = getGLRoot();
+            if (root != null) root.requestLayoutContentPane();
+        }
+    }
+
+    protected void render(GLCanvas canvas) {
+        boolean transitionActive = false;
+        if (mTransition != null && mTransition.calculate(AnimationTime.get())) {
+            invalidate();
+            transitionActive = mTransition.isActive();
+        }
+        renderBackground(canvas);
+        canvas.save();
+        if (transitionActive) {
+            mTransition.applyContentTransform(this, canvas);
+        }
+        for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            renderChild(canvas, getComponent(i));
+        }
+        canvas.restore();
+        if (transitionActive) {
+            mTransition.applyOverlay(this, canvas);
+        }
+    }
+
+    public void setIntroAnimation(StateTransitionAnimation intro) {
+        mTransition = intro;
+        if (mTransition != null) mTransition.start();
+    }
+
+    public float [] getBackgroundColor() {
+        return mBackgroundColor;
+    }
+
+    public void setBackgroundColor(float [] color) {
+        mBackgroundColor = color;
+    }
+
+    protected void renderBackground(GLCanvas view) {
+        if (mBackgroundColor != null) {
+            view.clearBuffer(mBackgroundColor);
+        }
+        if (mTransition != null && mTransition.isActive()) {
+            mTransition.applyBackground(this, view);
+            return;
+        }
+    }
+
+    protected void renderChild(GLCanvas canvas, GLView component) {
+        if (component.getVisibility() != GLView.VISIBLE
+                && component.mAnimation == null) return;
+
+        int xoffset = component.mBounds.left - mScrollX;
+        int yoffset = component.mBounds.top - mScrollY;
+
+        canvas.translate(xoffset, yoffset);
+
+        CanvasAnimation anim = component.mAnimation;
+        if (anim != null) {
+            canvas.save(anim.getCanvasSaveFlags());
+            if (anim.calculate(AnimationTime.get())) {
+                invalidate();
+            } else {
+                component.mAnimation = null;
+            }
+            anim.apply(canvas);
+        }
+        component.render(canvas);
+        if (anim != null) canvas.restore();
+        canvas.translate(-xoffset, -yoffset);
+    }
+
+    protected boolean onTouch(MotionEvent event) {
+        return false;
+    }
+
+    protected boolean dispatchTouchEvent(MotionEvent event,
+            int x, int y, GLView component, boolean checkBounds) {
+        Rect rect = component.mBounds;
+        int left = rect.left;
+        int top = rect.top;
+        if (!checkBounds || rect.contains(x, y)) {
+            event.offsetLocation(-left, -top);
+            if (component.dispatchTouchEvent(event)) {
+                event.offsetLocation(left, top);
+                return true;
+            }
+            event.offsetLocation(left, top);
+        }
+        return false;
+    }
+
+    protected boolean dispatchTouchEvent(MotionEvent event) {
+        int x = (int) event.getX();
+        int y = (int) event.getY();
+        int action = event.getAction();
+        if (mMotionTarget != null) {
+            if (action == MotionEvent.ACTION_DOWN) {
+                MotionEvent cancel = MotionEvent.obtain(event);
+                cancel.setAction(MotionEvent.ACTION_CANCEL);
+                dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
+                mMotionTarget = null;
+            } else {
+                dispatchTouchEvent(event, x, y, mMotionTarget, false);
+                if (action == MotionEvent.ACTION_CANCEL
+                        || action == MotionEvent.ACTION_UP) {
+                    mMotionTarget = null;
+                }
+                return true;
+            }
+        }
+        if (action == MotionEvent.ACTION_DOWN) {
+            // in the reverse rendering order
+            for (int i = getComponentCount() - 1; i >= 0; --i) {
+                GLView component = getComponent(i);
+                if (component.getVisibility() != GLView.VISIBLE) continue;
+                if (dispatchTouchEvent(event, x, y, component, true)) {
+                    mMotionTarget = component;
+                    return true;
+                }
+            }
+        }
+        return onTouch(event);
+    }
+
+    public Rect getPaddings() {
+        return mPaddings;
+    }
+
+    public void layout(int left, int top, int right, int bottom) {
+        boolean sizeChanged = setBounds(left, top, right, bottom);
+        mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+        // We call onLayout no matter sizeChanged is true or not because the
+        // orientation may change without changing the size of the View (for
+        // example, rotate the device by 180 degrees), and we want to handle
+        // orientation change in onLayout.
+        onLayout(sizeChanged, left, top, right, bottom);
+    }
+
+    private boolean setBounds(int left, int top, int right, int bottom) {
+        boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left)
+                || (bottom - top) != (mBounds.bottom - mBounds.top);
+        mBounds.set(left, top, right, bottom);
+        return sizeChanged;
+    }
+
+    public void measure(int widthSpec, int heightSpec) {
+        if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec
+                && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) {
+            return;
+        }
+
+        mLastWidthSpec = widthSpec;
+        mLastHeightSpec = heightSpec;
+
+        mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
+        onMeasure(widthSpec, heightSpec);
+        if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
+            throw new IllegalStateException(getClass().getName()
+                    + " should call setMeasuredSize() in onMeasure()");
+        }
+    }
+
+    protected void onMeasure(int widthSpec, int heightSpec) {
+    }
+
+    protected void setMeasuredSize(int width, int height) {
+        mViewFlags |= FLAG_SET_MEASURED_SIZE;
+        mMeasuredWidth = width;
+        mMeasuredHeight = height;
+    }
+
+    public int getMeasuredWidth() {
+        return mMeasuredWidth;
+    }
+
+    public int getMeasuredHeight() {
+        return mMeasuredHeight;
+    }
+
+    protected void onLayout(
+            boolean changeSize, int left, int top, int right, int bottom) {
+    }
+
+    /**
+     * Gets the bounds of the given descendant that relative to this view.
+     */
+    public boolean getBoundsOf(GLView descendant, Rect out) {
+        int xoffset = 0;
+        int yoffset = 0;
+        GLView view = descendant;
+        while (view != this) {
+            if (view == null) return false;
+            Rect bounds = view.mBounds;
+            xoffset += bounds.left;
+            yoffset += bounds.top;
+            view = view.mParent;
+        }
+        out.set(xoffset, yoffset, xoffset + descendant.getWidth(),
+                yoffset + descendant.getHeight());
+        return true;
+    }
+
+    protected void onVisibilityChanged(int visibility) {
+        for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            GLView child = getComponent(i);
+            if (child.getVisibility() == GLView.VISIBLE) {
+                child.onVisibilityChanged(visibility);
+            }
+        }
+    }
+
+    protected void onAttachToRoot(GLRoot root) {
+        mRoot = root;
+        for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            getComponent(i).onAttachToRoot(root);
+        }
+    }
+
+    protected void onDetachFromRoot() {
+        for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            getComponent(i).onDetachFromRoot();
+        }
+        mRoot = null;
+    }
+
+    public void lockRendering() {
+        if (mRoot != null) {
+            mRoot.lockRenderThread();
+        }
+    }
+
+    public void unlockRendering() {
+        if (mRoot != null) {
+            mRoot.unlockRenderThread();
+        }
+    }
+
+    // This is for debugging only.
+    // Dump the view hierarchy into log.
+    void dumpTree(String prefix) {
+        Log.d(TAG, prefix + getClass().getSimpleName());
+        for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            getComponent(i).dumpTree(prefix + "....");
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java
new file mode 100644
index 0000000..1e5250b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GestureRecognizer.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector, and DownUpDetector.
+public class GestureRecognizer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "GestureRecognizer";
+
+    public interface Listener {
+        boolean onSingleTapUp(float x, float y);
+        boolean onDoubleTap(float x, float y);
+        boolean onScroll(float dx, float dy, float totalX, float totalY);
+        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
+        boolean onScaleBegin(float focusX, float focusY);
+        boolean onScale(float focusX, float focusY, float scale);
+        void onScaleEnd();
+        void onDown(float x, float y);
+        void onUp();
+    }
+
+    private final GestureDetector mGestureDetector;
+    private final ScaleGestureDetector mScaleDetector;
+    private final DownUpDetector mDownUpDetector;
+    private final Listener mListener;
+
+    public GestureRecognizer(Context context, Listener listener) {
+        mListener = listener;
+        mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+                null, true /* ignoreMultitouch */);
+        mScaleDetector = new ScaleGestureDetector(
+                context, new MyScaleListener());
+        mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+    }
+
+    public void onTouchEvent(MotionEvent event) {
+        mGestureDetector.onTouchEvent(event);
+        mScaleDetector.onTouchEvent(event);
+        mDownUpDetector.onTouchEvent(event);
+    }
+
+    public boolean isDown() {
+        return mDownUpDetector.isDown();
+    }
+
+    public void cancelScale() {
+        long now = SystemClock.uptimeMillis();
+        MotionEvent cancelEvent = MotionEvent.obtain(
+                now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+        mScaleDetector.onTouchEvent(cancelEvent);
+        cancelEvent.recycle();
+    }
+
+    private class MyGestureListener
+                extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            return mListener.onSingleTapUp(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            return mListener.onDoubleTap(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onScroll(
+                MotionEvent e1, MotionEvent e2, float dx, float dy) {
+            return mListener.onScroll(
+                    dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY());
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                float velocityY) {
+            return mListener.onFling(e1, e2, velocityX, velocityY);
+        }
+    }
+
+    private class MyScaleListener
+            extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            return mListener.onScaleBegin(
+                    detector.getFocusX(), detector.getFocusY());
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            return mListener.onScale(detector.getFocusX(),
+                    detector.getFocusY(), detector.getScaleFactor());
+        }
+
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            mListener.onScaleEnd();
+        }
+    }
+
+    private class MyDownUpListener implements DownUpDetector.DownUpListener {
+        @Override
+        public void onDown(MotionEvent e) {
+            mListener.onDown(e.getX(), e.getY());
+        }
+
+        @Override
+        public void onUp(MotionEvent e) {
+            mListener.onUp();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java
new file mode 100644
index 0000000..5570763
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Log.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+// TODO: Delete this
+public class Log {
+    public static int v(String tag, String msg) {
+        return android.util.Log.v(tag, msg);
+    }
+    public static int v(String tag, String msg, Throwable tr) {
+        return android.util.Log.v(tag, msg, tr);
+    }
+    public static int d(String tag, String msg) {
+        return android.util.Log.d(tag, msg);
+    }
+    public static int d(String tag, String msg, Throwable tr) {
+        return android.util.Log.d(tag, msg, tr);
+    }
+    public static int i(String tag, String msg) {
+        return android.util.Log.i(tag, msg);
+    }
+    public static int i(String tag, String msg, Throwable tr) {
+        return android.util.Log.i(tag, msg, tr);
+    }
+    public static int w(String tag, String msg) {
+        return android.util.Log.w(tag, msg);
+    }
+    public static int w(String tag, String msg, Throwable tr) {
+        return android.util.Log.w(tag, msg, tr);
+    }
+    public static int w(String tag, Throwable tr) {
+        return android.util.Log.w(tag, tr);
+    }
+    public static int e(String tag, String msg) {
+        return android.util.Log.e(tag, msg);
+    }
+    public static int e(String tag, String msg, Throwable tr) {
+        return android.util.Log.e(tag, msg, tr);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
new file mode 100644
index 0000000..d210bd1
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class ManageCacheDrawer extends AlbumSetSlotRenderer {
+    private final ResourceTexture mCheckedItem;
+    private final ResourceTexture mUnCheckedItem;
+    private final SelectionManager mSelectionManager;
+
+    private final ResourceTexture mLocalAlbumIcon;
+    private final StringTexture mCachingText;
+
+    private final int mCachePinSize;
+    private final int mCachePinMargin;
+
+    public ManageCacheDrawer(AbstractGalleryActivity activity, SelectionManager selectionManager,
+            SlotView slotView, LabelSpec labelSpec, int cachePinSize, int cachePinMargin) {
+        super(activity, selectionManager, slotView, labelSpec,
+                activity.getResources().getColor(R.color.cache_placeholder));
+        Context context = activity;
+        mCheckedItem = new ResourceTexture(
+                context, R.drawable.btn_make_offline_normal_on_holo_dark);
+        mUnCheckedItem = new ResourceTexture(
+                context, R.drawable.btn_make_offline_normal_off_holo_dark);
+        mLocalAlbumIcon = new ResourceTexture(
+                context, R.drawable.btn_make_offline_disabled_on_holo_dark);
+        String cachingLabel = context.getString(R.string.caching_label);
+        mCachingText = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
+        mSelectionManager = selectionManager;
+        mCachePinSize = cachePinSize;
+        mCachePinMargin = cachePinMargin;
+    }
+
+    private static boolean isLocal(int dataSourceType) {
+        return dataSourceType != DataSourceType.TYPE_PICASA;
+    }
+
+    @Override
+    public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        AlbumSetEntry entry = mDataWindow.get(index);
+
+        boolean wantCache = entry.cacheFlag == MediaSet.CACHE_FLAG_FULL;
+        boolean isCaching = wantCache && (
+                entry.cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL);
+        boolean selected = mSelectionManager.isItemSelected(entry.setPath);
+        boolean chooseToCache = wantCache ^ selected;
+        boolean available = isLocal(entry.sourceType) || chooseToCache;
+
+        int renderRequestFlags = 0;
+
+        if (!available) {
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(0.6f);
+        }
+        renderRequestFlags |= renderContent(canvas, entry, width, height);
+        if (!available) canvas.restore();
+
+        renderRequestFlags |= renderLabel(canvas, entry, width, height);
+
+        drawCachingPin(canvas, entry.setPath,
+                entry.sourceType, isCaching, chooseToCache, width, height);
+
+        renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+        return renderRequestFlags;
+    }
+
+    private void drawCachingPin(GLCanvas canvas, Path path, int dataSourceType,
+            boolean isCaching, boolean chooseToCache, int width, int height) {
+        ResourceTexture icon;
+        if (isLocal(dataSourceType)) {
+            icon = mLocalAlbumIcon;
+        } else if (chooseToCache) {
+            icon = mCheckedItem;
+        } else {
+            icon = mUnCheckedItem;
+        }
+
+        // show the icon in right bottom
+        int s = mCachePinSize;
+        int m = mCachePinMargin;
+        icon.draw(canvas, width - m - s, height - s, s, s);
+
+        if (isCaching) {
+            int w = mCachingText.getWidth();
+            int h = mCachingText.getHeight();
+            // Show the caching text in bottom center
+            mCachingText.draw(canvas, (width - w) / 2, height - h);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java
new file mode 100644
index 0000000..f65dc10
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MeasureHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.view.View.MeasureSpec;
+
+class MeasureHelper {
+
+    private static MeasureHelper sInstance = new MeasureHelper(null);
+
+    private GLView mComponent;
+    private int mPreferredWidth;
+    private int mPreferredHeight;
+
+    private MeasureHelper(GLView component) {
+        mComponent = component;
+    }
+
+    public static MeasureHelper getInstance(GLView component) {
+        sInstance.mComponent = component;
+        return sInstance;
+    }
+
+    public MeasureHelper setPreferredContentSize(int width, int height) {
+        mPreferredWidth = width;
+        mPreferredHeight = height;
+        return this;
+    }
+
+    public void measure(int widthSpec, int heightSpec) {
+        Rect p = mComponent.getPaddings();
+        setMeasuredSize(
+                getLength(widthSpec, mPreferredWidth + p.left + p.right),
+                getLength(heightSpec, mPreferredHeight + p.top + p.bottom));
+    }
+
+    private static int getLength(int measureSpec, int prefered) {
+        int specLength = MeasureSpec.getSize(measureSpec);
+        switch(MeasureSpec.getMode(measureSpec)) {
+            case MeasureSpec.EXACTLY: return specLength;
+            case MeasureSpec.AT_MOST: return Math.min(prefered, specLength);
+            default: return prefered;
+        }
+    }
+
+    protected void setMeasuredSize(int width, int height) {
+        mComponent.setMeasuredSize(width, height);
+    }
+
+}
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
new file mode 100644
index 0000000..29def05
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+
+public class MenuExecutor {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MenuExecutor";
+
+    private static final int MSG_TASK_COMPLETE = 1;
+    private static final int MSG_TASK_UPDATE = 2;
+    private static final int MSG_TASK_START = 3;
+    private static final int MSG_DO_SHARE = 4;
+
+    public static final int EXECUTION_RESULT_SUCCESS = 1;
+    public static final int EXECUTION_RESULT_FAIL = 2;
+    public static final int EXECUTION_RESULT_CANCEL = 3;
+
+    private ProgressDialog mDialog;
+    private Future<?> mTask;
+    // wait the operation to finish when we want to stop it.
+    private boolean mWaitOnStop;
+    private boolean mPaused;
+
+    private final AbstractGalleryActivity mActivity;
+    private final SelectionManager mSelectionManager;
+    private final Handler mHandler;
+
+    private static ProgressDialog createProgressDialog(
+            Context context, int titleId, int progressMax) {
+        ProgressDialog dialog = new ProgressDialog(context);
+        dialog.setTitle(titleId);
+        dialog.setMax(progressMax);
+        dialog.setCancelable(false);
+        dialog.setIndeterminate(false);
+        if (progressMax > 1) {
+            dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+        }
+        return dialog;
+    }
+
+    public interface ProgressListener {
+        public void onConfirmDialogShown();
+        public void onConfirmDialogDismissed(boolean confirmed);
+        public void onProgressStart();
+        public void onProgressUpdate(int index);
+        public void onProgressComplete(int result);
+    }
+
+    public MenuExecutor(
+            AbstractGalleryActivity activity, SelectionManager selectionManager) {
+        mActivity = Utils.checkNotNull(activity);
+        mSelectionManager = Utils.checkNotNull(selectionManager);
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_TASK_START: {
+                        if (message.obj != null) {
+                            ProgressListener listener = (ProgressListener) message.obj;
+                            listener.onProgressStart();
+                        }
+                        break;
+                    }
+                    case MSG_TASK_COMPLETE: {
+                        stopTaskAndDismissDialog();
+                        if (message.obj != null) {
+                            ProgressListener listener = (ProgressListener) message.obj;
+                            listener.onProgressComplete(message.arg1);
+                        }
+                        mSelectionManager.leaveSelectionMode();
+                        break;
+                    }
+                    case MSG_TASK_UPDATE: {
+                        if (mDialog != null && !mPaused) mDialog.setProgress(message.arg1);
+                        if (message.obj != null) {
+                            ProgressListener listener = (ProgressListener) message.obj;
+                            listener.onProgressUpdate(message.arg1);
+                        }
+                        break;
+                    }
+                    case MSG_DO_SHARE: {
+                        ((Activity) mActivity).startActivity((Intent) message.obj);
+                        break;
+                    }
+                }
+            }
+        };
+    }
+
+    private void stopTaskAndDismissDialog() {
+        if (mTask != null) {
+            if (!mWaitOnStop) mTask.cancel();
+            if (mDialog != null && mDialog.isShowing()) mDialog.dismiss();
+            mDialog = null;
+            mTask = null;
+        }
+    }
+
+    public void resume() {
+        mPaused = false;
+        if (mDialog != null) mDialog.show();
+    }
+
+    public void pause() {
+        mPaused = true;
+        if (mDialog != null && mDialog.isShowing()) mDialog.hide();
+    }
+
+    public void destroy() {
+        stopTaskAndDismissDialog();
+    }
+
+    private void onProgressUpdate(int index, ProgressListener listener) {
+        mHandler.sendMessage(
+                mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener));
+    }
+
+    private void onProgressStart(ProgressListener listener) {
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_START, listener));
+    }
+
+    private void onProgressComplete(int result, ProgressListener listener) {
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener));
+    }
+
+    public static void updateMenuOperation(Menu menu, int supported) {
+        boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0;
+        boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0;
+        boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0;
+        boolean supportTrim = (supported & MediaObject.SUPPORT_TRIM) != 0;
+        boolean supportMute = (supported & MediaObject.SUPPORT_MUTE) != 0;
+        boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0;
+        boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0;
+        boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0;
+        boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0;
+        boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0;
+        boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0;
+
+        setMenuItemVisible(menu, R.id.action_delete, supportDelete);
+        setMenuItemVisible(menu, R.id.action_rotate_ccw, supportRotate);
+        setMenuItemVisible(menu, R.id.action_rotate_cw, supportRotate);
+        setMenuItemVisible(menu, R.id.action_crop, supportCrop);
+        setMenuItemVisible(menu, R.id.action_trim, supportTrim);
+        setMenuItemVisible(menu, R.id.action_mute, supportMute);
+        // Hide panorama until call to updateMenuForPanorama corrects it
+        setMenuItemVisible(menu, R.id.action_share_panorama, false);
+        setMenuItemVisible(menu, R.id.action_share, supportShare);
+        setMenuItemVisible(menu, R.id.action_setas, supportSetAs);
+        setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap);
+        setMenuItemVisible(menu, R.id.action_edit, supportEdit);
+        setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit);
+        setMenuItemVisible(menu, R.id.action_details, supportInfo);
+    }
+
+    public static void updateMenuForPanorama(Menu menu, boolean shareAsPanorama360,
+            boolean disablePanorama360Options) {
+        setMenuItemVisible(menu, R.id.action_share_panorama, shareAsPanorama360);
+        if (disablePanorama360Options) {
+            setMenuItemVisible(menu, R.id.action_rotate_ccw, false);
+            setMenuItemVisible(menu, R.id.action_rotate_cw, false);
+        }
+    }
+
+    private static void setMenuItemVisible(Menu menu, int itemId, boolean visible) {
+        MenuItem item = menu.findItem(itemId);
+        if (item != null) item.setVisible(visible);
+    }
+
+    private Path getSingleSelectedPath() {
+        ArrayList<Path> ids = mSelectionManager.getSelected(true);
+        Utils.assertTrue(ids.size() == 1);
+        return ids.get(0);
+    }
+
+    private Intent getIntentBySingleSelectedPath(String action) {
+        DataManager manager = mActivity.getDataManager();
+        Path path = getSingleSelectedPath();
+        String mimeType = getMimeType(manager.getMediaType(path));
+        return new Intent(action).setDataAndType(manager.getContentUri(path), mimeType);
+    }
+
+    private void onMenuClicked(int action, ProgressListener listener) {
+        onMenuClicked(action, listener, false, true);
+    }
+
+    public void onMenuClicked(int action, ProgressListener listener,
+            boolean waitOnStop, boolean showDialog) {
+        int title;
+        switch (action) {
+            case R.id.action_select_all:
+                if (mSelectionManager.inSelectAllMode()) {
+                    mSelectionManager.deSelectAll();
+                } else {
+                    mSelectionManager.selectAll();
+                }
+                return;
+            case R.id.action_crop: {
+                Intent intent = getIntentBySingleSelectedPath(CropActivity.CROP_ACTION);
+                ((Activity) mActivity).startActivity(intent);
+                return;
+            }
+            case R.id.action_edit: {
+                Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_EDIT)
+                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                ((Activity) mActivity).startActivity(Intent.createChooser(intent, null));
+                return;
+            }
+            case R.id.action_setas: {
+                Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_ATTACH_DATA)
+                        .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                intent.putExtra("mimeType", intent.getType());
+                Activity activity = mActivity;
+                activity.startActivity(Intent.createChooser(
+                        intent, activity.getString(R.string.set_as)));
+                return;
+            }
+            case R.id.action_delete:
+                title = R.string.delete;
+                break;
+            case R.id.action_rotate_cw:
+                title = R.string.rotate_right;
+                break;
+            case R.id.action_rotate_ccw:
+                title = R.string.rotate_left;
+                break;
+            case R.id.action_show_on_map:
+                title = R.string.show_on_map;
+                break;
+            default:
+                return;
+        }
+        startAction(action, title, listener, waitOnStop, showDialog);
+    }
+
+    private class ConfirmDialogListener implements OnClickListener, OnCancelListener {
+        private final int mActionId;
+        private final ProgressListener mListener;
+
+        public ConfirmDialogListener(int actionId, ProgressListener listener) {
+            mActionId = actionId;
+            mListener = listener;
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == DialogInterface.BUTTON_POSITIVE) {
+                if (mListener != null) {
+                    mListener.onConfirmDialogDismissed(true);
+                }
+                onMenuClicked(mActionId, mListener);
+            } else {
+                if (mListener != null) {
+                    mListener.onConfirmDialogDismissed(false);
+                }
+            }
+        }
+
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            if (mListener != null) {
+                mListener.onConfirmDialogDismissed(false);
+            }
+        }
+    }
+
+    public void onMenuClicked(MenuItem menuItem, String confirmMsg,
+            final ProgressListener listener) {
+        final int action = menuItem.getItemId();
+
+        if (confirmMsg != null) {
+            if (listener != null) listener.onConfirmDialogShown();
+            ConfirmDialogListener cdl = new ConfirmDialogListener(action, listener);
+            new AlertDialog.Builder(mActivity.getAndroidContext())
+                    .setMessage(confirmMsg)
+                    .setOnCancelListener(cdl)
+                    .setPositiveButton(R.string.ok, cdl)
+                    .setNegativeButton(R.string.cancel, cdl)
+                    .create().show();
+        } else {
+            onMenuClicked(action, listener);
+        }
+    }
+
+    public void startAction(int action, int title, ProgressListener listener) {
+        startAction(action, title, listener, false, true);
+    }
+
+    public void startAction(int action, int title, ProgressListener listener,
+            boolean waitOnStop, boolean showDialog) {
+        ArrayList<Path> ids = mSelectionManager.getSelected(false);
+        stopTaskAndDismissDialog();
+
+        Activity activity = mActivity;
+        if (showDialog) {
+            mDialog = createProgressDialog(activity, title, ids.size());
+            mDialog.show();
+        } else {
+            mDialog = null;
+        }
+        MediaOperation operation = new MediaOperation(action, ids, listener);
+        mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null);
+        mWaitOnStop = waitOnStop;
+    }
+
+    public void startSingleItemAction(int action, Path targetPath) {
+        ArrayList<Path> ids = new ArrayList<Path>(1);
+        ids.add(targetPath);
+        mDialog = null;
+        MediaOperation operation = new MediaOperation(action, ids, null);
+        mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null);
+        mWaitOnStop = false;
+    }
+
+    public static String getMimeType(int type) {
+        switch (type) {
+            case MediaObject.MEDIA_TYPE_IMAGE :
+                return GalleryUtils.MIME_TYPE_IMAGE;
+            case MediaObject.MEDIA_TYPE_VIDEO :
+                return GalleryUtils.MIME_TYPE_VIDEO;
+            default: return GalleryUtils.MIME_TYPE_ALL;
+        }
+    }
+
+    private boolean execute(
+            DataManager manager, JobContext jc, int cmd, Path path) {
+        boolean result = true;
+        Log.v(TAG, "Execute cmd: " + cmd + " for " + path);
+        long startTime = System.currentTimeMillis();
+
+        switch (cmd) {
+            case R.id.action_delete:
+                manager.delete(path);
+                break;
+            case R.id.action_rotate_cw:
+                manager.rotate(path, 90);
+                break;
+            case R.id.action_rotate_ccw:
+                manager.rotate(path, -90);
+                break;
+            case R.id.action_toggle_full_caching: {
+                MediaObject obj = manager.getMediaObject(path);
+                int cacheFlag = obj.getCacheFlag();
+                if (cacheFlag == MediaObject.CACHE_FLAG_FULL) {
+                    cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL;
+                } else {
+                    cacheFlag = MediaObject.CACHE_FLAG_FULL;
+                }
+                obj.cache(cacheFlag);
+                break;
+            }
+            case R.id.action_show_on_map: {
+                MediaItem item = (MediaItem) manager.getMediaObject(path);
+                double latlng[] = new double[2];
+                item.getLatLong(latlng);
+                if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) {
+                    GalleryUtils.showOnMap(mActivity, latlng[0], latlng[1]);
+                }
+                break;
+            }
+            default:
+                throw new AssertionError();
+        }
+        Log.v(TAG, "It takes " + (System.currentTimeMillis() - startTime) +
+                " ms to execute cmd for " + path);
+        return result;
+    }
+
+    private class MediaOperation implements Job<Void> {
+        private final ArrayList<Path> mItems;
+        private final int mOperation;
+        private final ProgressListener mListener;
+
+        public MediaOperation(int operation, ArrayList<Path> items,
+                ProgressListener listener) {
+            mOperation = operation;
+            mItems = items;
+            mListener = listener;
+        }
+
+        @Override
+        public Void run(JobContext jc) {
+            int index = 0;
+            DataManager manager = mActivity.getDataManager();
+            int result = EXECUTION_RESULT_SUCCESS;
+            try {
+                onProgressStart(mListener);
+                for (Path id : mItems) {
+                    if (jc.isCancelled()) {
+                        result = EXECUTION_RESULT_CANCEL;
+                        break;
+                    }
+                    if (!execute(manager, jc, mOperation, id)) {
+                        result = EXECUTION_RESULT_FAIL;
+                    }
+                    onProgressUpdate(index++, mListener);
+                }
+            } catch (Throwable th) {
+                Log.e(TAG, "failed to execute operation " + mOperation
+                        + " : " + th);
+            } finally {
+               onProgressComplete(result, mListener);
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/OrientationSource.java b/src/com/android/gallery3d/ui/OrientationSource.java
new file mode 100644
index 0000000..e13ce1c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/OrientationSource.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+public interface OrientationSource {
+    public int getDisplayRotation();
+    public int getCompensation();
+}
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
new file mode 100644
index 0000000..b36f5c3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.opengl.Matrix;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.common.Utils;
+
+// This class does the overscroll effect.
+class Paper {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Paper";
+    private static final int ROTATE_FACTOR = 4;
+    private EdgeAnimation mAnimationLeft = new EdgeAnimation();
+    private EdgeAnimation mAnimationRight = new EdgeAnimation();
+    private int mWidth;
+    private float[] mMatrix = new float[16];
+
+    public void overScroll(float distance) {
+        distance /= mWidth;  // make it relative to width
+        if (distance < 0) {
+            mAnimationLeft.onPull(-distance);
+        } else {
+            mAnimationRight.onPull(distance);
+        }
+    }
+
+    public void edgeReached(float velocity) {
+        velocity /= mWidth;  // make it relative to width
+        if (velocity < 0) {
+            mAnimationRight.onAbsorb(-velocity);
+        } else {
+            mAnimationLeft.onAbsorb(velocity);
+        }
+    }
+
+    public void onRelease() {
+        mAnimationLeft.onRelease();
+        mAnimationRight.onRelease();
+    }
+
+    public boolean advanceAnimation() {
+        // Note that we use "|" because we want both animations get updated.
+        return mAnimationLeft.update() | mAnimationRight.update();
+    }
+
+    public void setSize(int width, int height) {
+        mWidth = width;
+    }
+
+    public float[] getTransform(Rect rect, float scrollX) {
+        float left = mAnimationLeft.getValue();
+        float right = mAnimationRight.getValue();
+        float screenX = rect.centerX() - scrollX;
+        // We linearly interpolate the value [left, right] for the screenX
+        // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside
+        // the screen, we still get some transform.
+        float x = screenX + mWidth / 4;
+        int range = 3 * mWidth / 2;
+        float t = ((range - x) * left - x * right) / range;
+        // compress t to the range (-1, 1) by the function
+        // f(t) = (1 / (1 + e^-t) - 0.5) * 2
+        // then multiply by 90 to make the range (-45, 45)
+        float degrees =
+                (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45;
+        Matrix.setIdentityM(mMatrix, 0);
+        Matrix.translateM(mMatrix, 0, mMatrix, 0, rect.centerX(), rect.centerY(), 0);
+        Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0);
+        Matrix.translateM(mMatrix, 0, mMatrix, 0, -rect.width() / 2, -rect.height() / 2, 0);
+        return mMatrix;
+    }
+}
+
+// This class follows the structure of frameworks's EdgeEffect class.
+class EdgeAnimation {
+    @SuppressWarnings("unused")
+    private static final String TAG = "EdgeAnimation";
+
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PULL = 1;
+    private static final int STATE_ABSORB = 2;
+    private static final int STATE_RELEASE = 3;
+
+    // Time it will take the effect to fully done in ms
+    private static final int ABSORB_TIME = 200;
+    private static final int RELEASE_TIME = 500;
+
+    private static final float VELOCITY_FACTOR = 0.1f;
+
+    private final Interpolator mInterpolator;
+
+    private int mState;
+    private float mValue;
+
+    private float mValueStart;
+    private float mValueFinish;
+    private long mStartTime;
+    private long mDuration;
+
+    public EdgeAnimation() {
+        mInterpolator = new DecelerateInterpolator();
+        mState = STATE_IDLE;
+    }
+
+    private void startAnimation(float start, float finish, long duration,
+            int newState) {
+        mValueStart = start;
+        mValueFinish = finish;
+        mDuration = duration;
+        mStartTime = now();
+        mState = newState;
+    }
+
+    // The deltaDistance's magnitude is in the range of -1 (no change) to 1.
+    // The value 1 is the full length of the view. Negative values means the
+    // movement is in the opposite direction.
+    public void onPull(float deltaDistance) {
+        if (mState == STATE_ABSORB) return;
+        mValue = Utils.clamp(mValue + deltaDistance, -1.0f, 1.0f);
+        mState = STATE_PULL;
+    }
+
+    public void onRelease() {
+        if (mState == STATE_IDLE || mState == STATE_ABSORB) return;
+        startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+    }
+
+    public void onAbsorb(float velocity) {
+        float finish = Utils.clamp(mValue + velocity * VELOCITY_FACTOR,
+                -1.0f, 1.0f);
+        startAnimation(mValue, finish, ABSORB_TIME, STATE_ABSORB);
+    }
+
+    public boolean update() {
+        if (mState == STATE_IDLE) return false;
+        if (mState == STATE_PULL) return true;
+
+        float t = Utils.clamp((float)(now() - mStartTime) / mDuration, 0.0f, 1.0f);
+        /* Use linear interpolation for absorb, quadratic for others */
+        float interp = (mState == STATE_ABSORB)
+                ? t : mInterpolator.getInterpolation(t);
+
+        mValue = mValueStart + (mValueFinish - mValueStart) * interp;
+
+        if (t >= 1.0f) {
+            switch (mState) {
+                case STATE_ABSORB:
+                    startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+                    break;
+                case STATE_RELEASE:
+                    mState = STATE_IDLE;
+                    break;
+            }
+        }
+
+        return true;
+    }
+
+    public float getValue() {
+        return mValue;
+    }
+
+    private long now() {
+        return AnimationTime.get();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoFallbackEffect.java b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
new file mode 100644
index 0000000..4603285
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.AlbumSlotRenderer.SlotFilter;
+
+import java.util.ArrayList;
+
+public class PhotoFallbackEffect extends Animation implements SlotFilter {
+
+    private static final int ANIM_DURATION = 300;
+    private static final Interpolator ANIM_INTERPOLATE = new DecelerateInterpolator(1.5f);
+
+    public static class Entry {
+        public int index;
+        public Path path;
+        public Rect source;
+        public Rect dest;
+        public RawTexture texture;
+
+        public Entry(Path path, Rect source, RawTexture texture) {
+            this.path = path;
+            this.source = source;
+            this.texture = texture;
+        }
+    }
+
+    public interface PositionProvider {
+        public Rect getPosition(int index);
+        public int getItemIndex(Path path);
+    }
+
+    private RectF mSource = new RectF();
+    private RectF mTarget = new RectF();
+    private float mProgress;
+    private PositionProvider mPositionProvider;
+
+    private ArrayList<Entry> mList = new ArrayList<Entry>();
+
+    public PhotoFallbackEffect() {
+        setDuration(ANIM_DURATION);
+        setInterpolator(ANIM_INTERPOLATE);
+    }
+
+    public void addEntry(Path path, Rect rect, RawTexture texture) {
+        mList.add(new Entry(path, rect, texture));
+    }
+
+    public Entry getEntry(Path path) {
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.path == path) return entry;
+        }
+        return null;
+    }
+
+    public boolean draw(GLCanvas canvas) {
+        boolean more = calculate(AnimationTime.get());
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.index < 0) continue;
+            entry.dest = mPositionProvider.getPosition(entry.index);
+            drawEntry(canvas, entry);
+        }
+        return more;
+    }
+
+    private void drawEntry(GLCanvas canvas, Entry entry) {
+        if (!entry.texture.isLoaded()) return;
+
+        int w = entry.texture.getWidth();
+        int h = entry.texture.getHeight();
+
+        Rect s = entry.source;
+        Rect d = entry.dest;
+
+        // the following calculation is based on d.width() == d.height()
+
+        float p = mProgress;
+
+        float fullScale = (float) d.height() / Math.min(s.width(), s.height());
+        float scale = fullScale * p + 1 * (1 - p);
+
+        float cx = d.centerX() * p + s.centerX() * (1 - p);
+        float cy = d.centerY() * p + s.centerY() * (1 - p);
+
+        float ch = s.height() * scale;
+        float cw = s.width() * scale;
+
+        if (w > h) {
+            // draw the center part
+            mTarget.set(cx - ch / 2, cy - ch / 2, cx + ch / 2, cy + ch / 2);
+            mSource.set((w - h) / 2, 0, (w + h) / 2, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(1 - p);
+
+            // draw the left part
+            mTarget.set(cx - cw / 2, cy - ch / 2, cx - ch / 2, cy + ch / 2);
+            mSource.set(0, 0, (w - h) / 2, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            // draw the right part
+            mTarget.set(cx + ch / 2, cy - ch / 2, cx + cw / 2, cy + ch / 2);
+            mSource.set((w + h) / 2, 0, w, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.restore();
+        } else {
+            // draw the center part
+            mTarget.set(cx - cw / 2, cy - cw / 2, cx + cw / 2, cy + cw / 2);
+            mSource.set(0, (h - w) / 2, w, (h + w) / 2);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(1 - p);
+
+            // draw the upper part
+            mTarget.set(cx - cw / 2, cy - ch / 2, cx + cw / 2, cy - cw / 2);
+            mSource.set(0, 0, w, (h - w) / 2);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            // draw the bottom part
+            mTarget.set(cx - cw / 2, cy + cw / 2, cx + cw / 2, cy + ch / 2);
+            mSource.set(0, (w + h) / 2, w, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.restore();
+        }
+    }
+
+    @Override
+    protected void onCalculate(float progress) {
+        mProgress = progress;
+    }
+
+    public void setPositionProvider(PositionProvider provider) {
+        mPositionProvider = provider;
+        if (mPositionProvider != null) {
+            for (int i = 0, n = mList.size(); i < n; ++i) {
+                Entry entry = mList.get(i);
+                entry.index = mPositionProvider.getItemIndex(entry.path);
+            }
+        }
+    }
+
+    @Override
+    public boolean acceptSlot(int index) {
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.index == index) return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
new file mode 100644
index 0000000..7afa203
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -0,0 +1,1858 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Message;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+import android.view.animation.AccelerateInterpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.UsageStatistics;
+
+public class PhotoView extends GLView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "PhotoView";
+    private final int mPlaceholderColor;
+
+    public static final int INVALID_SIZE = -1;
+    public static final long INVALID_DATA_VERSION =
+            MediaObject.INVALID_DATA_VERSION;
+
+    public static class Size {
+        public int width;
+        public int height;
+    }
+
+    public interface Model extends TileImageView.TileSource {
+        public int getCurrentIndex();
+        public void moveTo(int index);
+
+        // Returns the size for the specified picture. If the size information is
+        // not avaiable, width = height = 0.
+        public void getImageSize(int offset, Size size);
+
+        // Returns the media item for the specified picture.
+        public MediaItem getMediaItem(int offset);
+
+        // Returns the rotation for the specified picture.
+        public int getImageRotation(int offset);
+
+        // This amends the getScreenNail() method of TileImageView.Model to get
+        // ScreenNail at previous (negative offset) or next (positive offset)
+        // positions. Returns null if the specified ScreenNail is unavailable.
+        public ScreenNail getScreenNail(int offset);
+
+        // Set this to true if we need the model to provide full images.
+        public void setNeedFullImage(boolean enabled);
+
+        // Returns true if the item is the Camera preview.
+        public boolean isCamera(int offset);
+
+        // Returns true if the item is the Panorama.
+        public boolean isPanorama(int offset);
+
+        // Returns true if the item is a static image that represents camera
+        // preview.
+        public boolean isStaticCamera(int offset);
+
+        // Returns true if the item is a Video.
+        public boolean isVideo(int offset);
+
+        // Returns true if the item can be deleted.
+        public boolean isDeletable(int offset);
+
+        public static final int LOADING_INIT = 0;
+        public static final int LOADING_COMPLETE = 1;
+        public static final int LOADING_FAIL = 2;
+
+        public int getLoadingState(int offset);
+
+        // When data change happens, we need to decide which MediaItem to focus
+        // on.
+        //
+        // 1. If focus hint path != null, we try to focus on it if we can find
+        // it.  This is used for undo a deletion, so we can focus on the
+        // undeleted item.
+        //
+        // 2. Otherwise try to focus on the MediaItem that is currently focused,
+        // if we can find it.
+        //
+        // 3. Otherwise try to focus on the previous MediaItem or the next
+        // MediaItem, depending on the value of focus hint direction.
+        public static final int FOCUS_HINT_NEXT = 0;
+        public static final int FOCUS_HINT_PREVIOUS = 1;
+        public void setFocusHintDirection(int direction);
+        public void setFocusHintPath(Path path);
+    }
+
+    public interface Listener {
+        public void onSingleTapUp(int x, int y);
+        public void onFullScreenChanged(boolean full);
+        public void onActionBarAllowed(boolean allowed);
+        public void onActionBarWanted();
+        public void onCurrentImageUpdated();
+        public void onDeleteImage(Path path, int offset);
+        public void onUndoDeleteImage();
+        public void onCommitDeleteImage();
+        public void onFilmModeChanged(boolean enabled);
+        public void onPictureCenter(boolean isCamera);
+        public void onUndoBarVisibilityChanged(boolean visible);
+    }
+
+    // The rules about orientation locking:
+    //
+    // (1) We need to lock the orientation if we are in page mode camera
+    // preview, so there is no (unwanted) rotation animation when the user
+    // rotates the device.
+    //
+    // (2) We need to unlock the orientation if we want to show the action bar
+    // because the action bar follows the system orientation.
+    //
+    // The rules about action bar:
+    //
+    // (1) If we are in film mode, we don't show action bar.
+    //
+    // (2) If we go from camera to gallery with capture animation, we show
+    // action bar.
+    private static final int MSG_CANCEL_EXTRA_SCALING = 2;
+    private static final int MSG_SWITCH_FOCUS = 3;
+    private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
+    private static final int MSG_DELETE_ANIMATION_DONE = 5;
+    private static final int MSG_DELETE_DONE = 6;
+    private static final int MSG_UNDO_BAR_TIMEOUT = 7;
+    private static final int MSG_UNDO_BAR_FULL_CAMERA = 8;
+
+    private static final float SWIPE_THRESHOLD = 300f;
+
+    private static final float DEFAULT_TEXT_SIZE = 20;
+    private static float TRANSITION_SCALE_FACTOR = 0.74f;
+    private static final int ICON_RATIO = 6;
+
+    // whether we want to apply card deck effect in page mode.
+    private static final boolean CARD_EFFECT = true;
+
+    // whether we want to apply offset effect in film mode.
+    private static final boolean OFFSET_EFFECT = true;
+
+    // Used to calculate the scaling factor for the card deck effect.
+    private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
+
+    // Used to calculate the alpha factor for the fading animation.
+    private AccelerateInterpolator mAlphaInterpolator =
+            new AccelerateInterpolator(0.9f);
+
+    // We keep this many previous ScreenNails. (also this many next ScreenNails)
+    public static final int SCREEN_NAIL_MAX = 3;
+
+    // These are constants for the delete gesture.
+    private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec
+    private static final int MAX_DISMISS_VELOCITY = 2500; // dp/sec
+    private static final int SWIPE_ESCAPE_DISTANCE = 150; // dp
+
+    // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
+    // SCREEN_NAIL_MAX.
+    private final RangeArray<Picture> mPictures =
+            new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
+    private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1];
+
+    private final MyGestureListener mGestureListener;
+    private final GestureRecognizer mGestureRecognizer;
+    private final PositionController mPositionController;
+
+    private Listener mListener;
+    private Model mModel;
+    private StringTexture mNoThumbnailText;
+    private TileImageView mTileView;
+    private EdgeView mEdgeView;
+    private UndoBarView mUndoBar;
+    private Texture mVideoPlayIcon;
+
+    private SynchronizedHandler mHandler;
+
+    private boolean mCancelExtraScalingPending;
+    private boolean mFilmMode = false;
+    private boolean mWantPictureCenterCallbacks = false;
+    private int mDisplayRotation = 0;
+    private int mCompensation = 0;
+    private boolean mFullScreenCamera;
+    private Rect mCameraRelativeFrame = new Rect();
+    private Rect mCameraRect = new Rect();
+    private boolean mFirst = true;
+
+    // [mPrevBound, mNextBound] is the range of index for all pictures in the
+    // model, if we assume the index of current focused picture is 0.  So if
+    // there are some previous pictures, mPrevBound < 0, and if there are some
+    // next pictures, mNextBound > 0.
+    private int mPrevBound;
+    private int mNextBound;
+
+    // This variable prevents us doing snapback until its values goes to 0. This
+    // happens if the user gesture is still in progress or we are in a capture
+    // animation.
+    private int mHolding;
+    private static final int HOLD_TOUCH_DOWN = 1;
+    private static final int HOLD_CAPTURE_ANIMATION = 2;
+    private static final int HOLD_DELETE = 4;
+
+    // mTouchBoxIndex is the index of the box that is touched by the down
+    // gesture in film mode. The value Integer.MAX_VALUE means no box was
+    // touched.
+    private int mTouchBoxIndex = Integer.MAX_VALUE;
+    // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful
+    // if mTouchBoxIndex is not Integer.MAX_VALUE.
+    private boolean mTouchBoxDeletable;
+    // This is the index of the last deleted item. This is only used as a hint
+    // to hide the undo button when we are too far away from the deleted
+    // item. The value Integer.MAX_VALUE means there is no such hint.
+    private int mUndoIndexHint = Integer.MAX_VALUE;
+
+    private Context mContext;
+
+    public PhotoView(AbstractGalleryActivity activity) {
+        mTileView = new TileImageView(activity);
+        addComponent(mTileView);
+        mContext = activity.getAndroidContext();
+        mPlaceholderColor = mContext.getResources().getColor(
+                R.color.photo_placeholder);
+        mEdgeView = new EdgeView(mContext);
+        addComponent(mEdgeView);
+        mUndoBar = new UndoBarView(mContext);
+        addComponent(mUndoBar);
+        mUndoBar.setVisibility(GLView.INVISIBLE);
+        mUndoBar.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(GLView v) {
+                    mListener.onUndoDeleteImage();
+                    hideUndoBar();
+                }
+            });
+        mNoThumbnailText = StringTexture.newInstance(
+                mContext.getString(R.string.no_thumbnail),
+                DEFAULT_TEXT_SIZE, Color.WHITE);
+
+        mHandler = new MyHandler(activity.getGLRoot());
+
+        mGestureListener = new MyGestureListener();
+        mGestureRecognizer = new GestureRecognizer(mContext, mGestureListener);
+
+        mPositionController = new PositionController(mContext,
+                new PositionController.Listener() {
+
+            @Override
+            public void invalidate() {
+                PhotoView.this.invalidate();
+            }
+
+            @Override
+            public boolean isHoldingDown() {
+                return (mHolding & HOLD_TOUCH_DOWN) != 0;
+            }
+
+            @Override
+            public boolean isHoldingDelete() {
+                return (mHolding & HOLD_DELETE) != 0;
+            }
+
+            @Override
+            public void onPull(int offset, int direction) {
+                mEdgeView.onPull(offset, direction);
+            }
+
+            @Override
+            public void onRelease() {
+                mEdgeView.onRelease();
+            }
+
+            @Override
+            public void onAbsorb(int velocity, int direction) {
+                mEdgeView.onAbsorb(velocity, direction);
+            }
+        });
+        mVideoPlayIcon = new ResourceTexture(mContext, R.drawable.ic_control_play);
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            if (i == 0) {
+                mPictures.put(i, new FullPicture());
+            } else {
+                mPictures.put(i, new ScreenNailPicture(i));
+            }
+        }
+    }
+
+    public void stopScrolling() {
+        mPositionController.stopScrolling();
+    }
+
+    public void setModel(Model model) {
+        mModel = model;
+        mTileView.setModel(mModel);
+    }
+
+    class MyHandler extends SynchronizedHandler {
+        public MyHandler(GLRoot root) {
+            super(root);
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_CANCEL_EXTRA_SCALING: {
+                    mGestureRecognizer.cancelScale();
+                    mPositionController.setExtraScalingRange(false);
+                    mCancelExtraScalingPending = false;
+                    break;
+                }
+                case MSG_SWITCH_FOCUS: {
+                    switchFocus();
+                    break;
+                }
+                case MSG_CAPTURE_ANIMATION_DONE: {
+                    // message.arg1 is the offset parameter passed to
+                    // switchWithCaptureAnimation().
+                    captureAnimationDone(message.arg1);
+                    break;
+                }
+                case MSG_DELETE_ANIMATION_DONE: {
+                    // message.obj is the Path of the MediaItem which should be
+                    // deleted. message.arg1 is the offset of the image.
+                    mListener.onDeleteImage((Path) message.obj, message.arg1);
+                    // Normally a box which finishes delete animation will hold
+                    // position until the underlying MediaItem is actually
+                    // deleted, and HOLD_DELETE will be cancelled that time. In
+                    // case the MediaItem didn't actually get deleted in 2
+                    // seconds, we will cancel HOLD_DELETE and make it bounce
+                    // back.
+
+                    // We make sure there is at most one MSG_DELETE_DONE
+                    // in the handler.
+                    mHandler.removeMessages(MSG_DELETE_DONE);
+                    Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+                    mHandler.sendMessageDelayed(m, 2000);
+
+                    int numberOfPictures = mNextBound - mPrevBound + 1;
+                    if (numberOfPictures == 2) {
+                        if (mModel.isCamera(mNextBound)
+                                || mModel.isCamera(mPrevBound)) {
+                            numberOfPictures--;
+                        }
+                    }
+                    showUndoBar(numberOfPictures <= 1);
+                    break;
+                }
+                case MSG_DELETE_DONE: {
+                    if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) {
+                        mHolding &= ~HOLD_DELETE;
+                        snapback();
+                    }
+                    break;
+                }
+                case MSG_UNDO_BAR_TIMEOUT: {
+                    checkHideUndoBar(UNDO_BAR_TIMEOUT);
+                    break;
+                }
+                case MSG_UNDO_BAR_FULL_CAMERA: {
+                    checkHideUndoBar(UNDO_BAR_FULL_CAMERA);
+                    break;
+                }
+                default: throw new AssertionError(message.what);
+            }
+        }
+    }
+
+    public void setWantPictureCenterCallbacks(boolean wanted) {
+        mWantPictureCenterCallbacks = wanted;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Data/Image change notifications
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
+        mPrevBound = prevBound;
+        mNextBound = nextBound;
+
+        // Update mTouchBoxIndex
+        if (mTouchBoxIndex != Integer.MAX_VALUE) {
+            int k = mTouchBoxIndex;
+            mTouchBoxIndex = Integer.MAX_VALUE;
+            for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) {
+                if (fromIndex[i] == k) {
+                    mTouchBoxIndex = i - SCREEN_NAIL_MAX;
+                    break;
+                }
+            }
+        }
+
+        // Hide undo button if we are too far away
+        if (mUndoIndexHint != Integer.MAX_VALUE) {
+            if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) {
+                hideUndoBar();
+            }
+        }
+
+        // Update the ScreenNails.
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            Picture p =  mPictures.get(i);
+            p.reload();
+            mSizes[i + SCREEN_NAIL_MAX] = p.getSize();
+        }
+
+        boolean wasDeleting = mPositionController.hasDeletingBox();
+
+        // Move the boxes
+        mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
+                mModel.isCamera(0), mSizes);
+
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            setPictureSize(i);
+        }
+
+        boolean isDeleting = mPositionController.hasDeletingBox();
+
+        // If the deletion is done, make HOLD_DELETE persist for only the time
+        // needed for a snapback animation.
+        if (wasDeleting && !isDeleting) {
+            mHandler.removeMessages(MSG_DELETE_DONE);
+            Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+            mHandler.sendMessageDelayed(
+                    m, PositionController.SNAPBACK_ANIMATION_TIME);
+        }
+
+        invalidate();
+    }
+
+    public boolean isDeleting() {
+        return (mHolding & HOLD_DELETE) != 0
+                && mPositionController.hasDeletingBox();
+    }
+
+    public void notifyImageChange(int index) {
+        if (index == 0) {
+            mListener.onCurrentImageUpdated();
+        }
+        mPictures.get(index).reload();
+        setPictureSize(index);
+        invalidate();
+    }
+
+    private void setPictureSize(int index) {
+        Picture p = mPictures.get(index);
+        mPositionController.setImageSize(index, p.getSize(),
+                index == 0 && p.isCamera() ? mCameraRect : null);
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changeSize, int left, int top, int right, int bottom) {
+        int w = right - left;
+        int h = bottom - top;
+        mTileView.layout(0, 0, w, h);
+        mEdgeView.layout(0, 0, w, h);
+        mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h);
+
+        GLRoot root = getGLRoot();
+        int displayRotation = root.getDisplayRotation();
+        int compensation = root.getCompensation();
+        if (mDisplayRotation != displayRotation
+                || mCompensation != compensation) {
+            mDisplayRotation = displayRotation;
+            mCompensation = compensation;
+
+            // We need to change the size and rotation of the Camera ScreenNail,
+            // but we don't want it to animate because the size doen't actually
+            // change in the eye of the user.
+            for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+                Picture p = mPictures.get(i);
+                if (p.isCamera()) {
+                    p.forceSize();
+                }
+            }
+        }
+
+        updateCameraRect();
+        mPositionController.setConstrainedFrame(mCameraRect);
+        if (changeSize) {
+            mPositionController.setViewSize(getWidth(), getHeight());
+        }
+    }
+
+    // Update the camera rectangle due to layout change or camera relative frame
+    // change.
+    private void updateCameraRect() {
+        // Get the width and height in framework orientation because the given
+        // mCameraRelativeFrame is in that coordinates.
+        int w = getWidth();
+        int h = getHeight();
+        if (mCompensation % 180 != 0) {
+            int tmp = w;
+            w = h;
+            h = tmp;
+        }
+        int l = mCameraRelativeFrame.left;
+        int t = mCameraRelativeFrame.top;
+        int r = mCameraRelativeFrame.right;
+        int b = mCameraRelativeFrame.bottom;
+
+        // Now convert it to the coordinates we are using.
+        switch (mCompensation) {
+            case 0: mCameraRect.set(l, t, r, b); break;
+            case 90: mCameraRect.set(h - b, l, h - t, r); break;
+            case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
+            case 270: mCameraRect.set(t, w - r, b, w - l); break;
+        }
+
+        Log.d(TAG, "compensation = " + mCompensation
+                + ", CameraRelativeFrame = " + mCameraRelativeFrame
+                + ", mCameraRect = " + mCameraRect);
+    }
+
+    public void setCameraRelativeFrame(Rect frame) {
+        mCameraRelativeFrame.set(frame);
+        updateCameraRect();
+        // Originally we do
+        //     mPositionController.setConstrainedFrame(mCameraRect);
+        // here, but it is moved to a parameter of the setImageSize() call, so
+        // it can be updated atomically with the CameraScreenNail's size change.
+    }
+
+    // Returns the rotation we need to do to the camera texture before drawing
+    // it to the canvas, assuming the camera texture is correct when the device
+    // is in its natural orientation.
+    private int getCameraRotation() {
+        return (mCompensation - mDisplayRotation + 360) % 360;
+    }
+
+    private int getPanoramaRotation() {
+        // This function is magic
+        // The issue here is that Pano makes bad assumptions about rotation and
+        // orientation. The first is it assumes only two rotations are possible,
+        // 0 and 90. Thus, if display rotation is >= 180, we invert the output.
+        // The second is that it assumes landscape is a 90 rotation from portrait,
+        // however on landscape devices this is not true. Thus, if we are in portrait
+        // on a landscape device, we need to invert the output
+        int orientation = mContext.getResources().getConfiguration().orientation;
+        boolean invertPortrait = (orientation == Configuration.ORIENTATION_PORTRAIT
+                && (mDisplayRotation == 90 || mDisplayRotation == 270));
+        boolean invert = (mDisplayRotation >= 180);
+        if (invert != invertPortrait) {
+            return (mCompensation + 180) % 360;
+        }
+        return mCompensation;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Pictures
+    ////////////////////////////////////////////////////////////////////////////
+
+    private interface Picture {
+        void reload();
+        void draw(GLCanvas canvas, Rect r);
+        void setScreenNail(ScreenNail s);
+        boolean isCamera();  // whether the picture is a camera preview
+        boolean isDeletable();  // whether the picture can be deleted
+        void forceSize();  // called when mCompensation changes
+        Size getSize();
+    }
+
+    class FullPicture implements Picture {
+        private int mRotation;
+        private boolean mIsCamera;
+        private boolean mIsPanorama;
+        private boolean mIsStaticCamera;
+        private boolean mIsVideo;
+        private boolean mIsDeletable;
+        private int mLoadingState = Model.LOADING_INIT;
+        private Size mSize = new Size();
+
+        @Override
+        public void reload() {
+            // mImageWidth and mImageHeight will get updated
+            mTileView.notifyModelInvalidated();
+
+            mIsCamera = mModel.isCamera(0);
+            mIsPanorama = mModel.isPanorama(0);
+            mIsStaticCamera = mModel.isStaticCamera(0);
+            mIsVideo = mModel.isVideo(0);
+            mIsDeletable = mModel.isDeletable(0);
+            mLoadingState = mModel.getLoadingState(0);
+            setScreenNail(mModel.getScreenNail(0));
+            updateSize();
+        }
+
+        @Override
+        public Size getSize() {
+            return mSize;
+        }
+
+        @Override
+        public void forceSize() {
+            updateSize();
+            mPositionController.forceImageSize(0, mSize);
+        }
+
+        private void updateSize() {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera && !mIsStaticCamera) {
+                mRotation = getCameraRotation();
+            } else {
+                mRotation = mModel.getImageRotation(0);
+            }
+
+            int w = mTileView.mImageWidth;
+            int h = mTileView.mImageHeight;
+            mSize.width = getRotated(mRotation, w, h);
+            mSize.height = getRotated(mRotation, h, w);
+        }
+
+        @Override
+        public void draw(GLCanvas canvas, Rect r) {
+            drawTileView(canvas, r);
+
+            // We want to have the following transitions:
+            // (1) Move camera preview out of its place: switch to film mode
+            // (2) Move camera preview into its place: switch to page mode
+            // The extra mWasCenter check makes sure (1) does not apply if in
+            // page mode, we move _to_ the camera preview from another picture.
+
+            // Holdings except touch-down prevent the transitions.
+            if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
+
+            if (mWantPictureCenterCallbacks && mPositionController.isCenter()) {
+                mListener.onPictureCenter(mIsCamera);
+            }
+        }
+
+        @Override
+        public void setScreenNail(ScreenNail s) {
+            mTileView.setScreenNail(s);
+        }
+
+        @Override
+        public boolean isCamera() {
+            return mIsCamera;
+        }
+
+        @Override
+        public boolean isDeletable() {
+            return mIsDeletable;
+        }
+
+        private void drawTileView(GLCanvas canvas, Rect r) {
+            float imageScale = mPositionController.getImageScale();
+            int viewW = getWidth();
+            int viewH = getHeight();
+            float cx = r.exactCenterX();
+            float cy = r.exactCenterY();
+            float scale = 1f;  // the scaling factor due to card effect
+
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+            float filmRatio = mPositionController.getFilmRatio();
+            boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
+                    && filmRatio != 1f && !mPictures.get(-1).isCamera()
+                    && !mPositionController.inOpeningAnimation();
+            boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+                    && filmRatio == 1f && r.centerY() != viewH / 2;
+            if (wantsCardEffect) {
+                // Calculate the move-out progress value.
+                int left = r.left;
+                int right = r.right;
+                float progress = calculateMoveOutProgress(left, right, viewW);
+                progress = Utils.clamp(progress, -1f, 1f);
+
+                // We only want to apply the fading animation if the scrolling
+                // movement is to the right.
+                if (progress < 0) {
+                    scale = getScrollScale(progress);
+                    float alpha = getScrollAlpha(progress);
+                    scale = interpolate(filmRatio, scale, 1f);
+                    alpha = interpolate(filmRatio, alpha, 1f);
+
+                    imageScale *= scale;
+                    canvas.multiplyAlpha(alpha);
+
+                    float cxPage;  // the cx value in page mode
+                    if (right - left <= viewW) {
+                        // If the picture is narrower than the view, keep it at
+                        // the center of the view.
+                        cxPage = viewW / 2f;
+                    } else {
+                        // If the picture is wider than the view (it's
+                        // zoomed-in), keep the left edge of the object align
+                        // the the left edge of the view.
+                        cxPage = (right - left) * scale / 2f;
+                    }
+                    cx = interpolate(filmRatio, cxPage, cx);
+                }
+            } else if (wantsOffsetEffect) {
+                float offset = (float) (r.centerY() - viewH / 2) / viewH;
+                float alpha = getOffsetAlpha(offset);
+                canvas.multiplyAlpha(alpha);
+            }
+
+            // Draw the tile view.
+            setTileViewPosition(cx, cy, viewW, viewH, imageScale);
+            renderChild(canvas, mTileView);
+
+            // Draw the play video icon and the message.
+            canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
+            int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
+            }
+
+            // Draw a debug indicator showing which picture has focus (index ==
+            // 0).
+            //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
+
+            canvas.restore();
+        }
+
+        // Set the position of the tile view
+        private void setTileViewPosition(float cx, float cy,
+                int viewW, int viewH, float scale) {
+            // Find out the bitmap coordinates of the center of the view
+            int imageW = mPositionController.getImageWidth();
+            int imageH = mPositionController.getImageHeight();
+            int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
+            int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
+
+            int inverseX = imageW - centerX;
+            int inverseY = imageH - centerY;
+            int x, y;
+            switch (mRotation) {
+                case 0: x = centerX; y = centerY; break;
+                case 90: x = centerY; y = inverseX; break;
+                case 180: x = inverseX; y = inverseY; break;
+                case 270: x = inverseY; y = centerX; break;
+                default:
+                    throw new RuntimeException(String.valueOf(mRotation));
+            }
+            mTileView.setPosition(x, y, scale, mRotation);
+        }
+    }
+
+    private class ScreenNailPicture implements Picture {
+        private int mIndex;
+        private int mRotation;
+        private ScreenNail mScreenNail;
+        private boolean mIsCamera;
+        private boolean mIsPanorama;
+        private boolean mIsStaticCamera;
+        private boolean mIsVideo;
+        private boolean mIsDeletable;
+        private int mLoadingState = Model.LOADING_INIT;
+        private Size mSize = new Size();
+
+        public ScreenNailPicture(int index) {
+            mIndex = index;
+        }
+
+        @Override
+        public void reload() {
+            mIsCamera = mModel.isCamera(mIndex);
+            mIsPanorama = mModel.isPanorama(mIndex);
+            mIsStaticCamera = mModel.isStaticCamera(mIndex);
+            mIsVideo = mModel.isVideo(mIndex);
+            mIsDeletable = mModel.isDeletable(mIndex);
+            mLoadingState = mModel.getLoadingState(mIndex);
+            setScreenNail(mModel.getScreenNail(mIndex));
+            updateSize();
+        }
+
+        @Override
+        public Size getSize() {
+            return mSize;
+        }
+
+        @Override
+        public void draw(GLCanvas canvas, Rect r) {
+            if (mScreenNail == null) {
+                // Draw a placeholder rectange if there should be a picture in
+                // this position (but somehow there isn't).
+                if (mIndex >= mPrevBound && mIndex <= mNextBound) {
+                    drawPlaceHolder(canvas, r);
+                }
+                return;
+            }
+            int w = getWidth();
+            int h = getHeight();
+            if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) {
+                mScreenNail.noDraw();
+                return;
+            }
+
+            float filmRatio = mPositionController.getFilmRatio();
+            boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
+                    && filmRatio != 1f && !mPictures.get(0).isCamera();
+            boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+                    && filmRatio == 1f && r.centerY() != h / 2;
+            int cx = wantsCardEffect
+                    ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
+                    : r.centerX();
+            int cy = r.centerY();
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.translate(cx, cy);
+            if (wantsCardEffect) {
+                float progress = (float) (w / 2 - r.centerX()) / w;
+                progress = Utils.clamp(progress, -1, 1);
+                float alpha = getScrollAlpha(progress);
+                float scale = getScrollScale(progress);
+                alpha = interpolate(filmRatio, alpha, 1f);
+                scale = interpolate(filmRatio, scale, 1f);
+                canvas.multiplyAlpha(alpha);
+                canvas.scale(scale, scale, 1);
+            } else if (wantsOffsetEffect) {
+                float offset = (float) (r.centerY() - h / 2) / h;
+                float alpha = getOffsetAlpha(offset);
+                canvas.multiplyAlpha(alpha);
+            }
+            if (mRotation != 0) {
+                canvas.rotate(mRotation, 0, 0, 1);
+            }
+            int drawW = getRotated(mRotation, r.width(), r.height());
+            int drawH = getRotated(mRotation, r.height(), r.width());
+            mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
+            if (isScreenNailAnimating()) {
+                invalidate();
+            }
+            int s = Math.min(drawW, drawH);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
+            }
+            canvas.restore();
+        }
+
+        private boolean isScreenNailAnimating() {
+            return (mScreenNail instanceof TiledScreenNail)
+                    && ((TiledScreenNail) mScreenNail).isAnimating();
+        }
+
+        @Override
+        public void setScreenNail(ScreenNail s) {
+            mScreenNail = s;
+        }
+
+        @Override
+        public void forceSize() {
+            updateSize();
+            mPositionController.forceImageSize(mIndex, mSize);
+        }
+
+        private void updateSize() {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera && !mIsStaticCamera) {
+                mRotation = getCameraRotation();
+            } else {
+                mRotation = mModel.getImageRotation(mIndex);
+            }
+
+            if (mScreenNail != null) {
+                mSize.width = mScreenNail.getWidth();
+                mSize.height = mScreenNail.getHeight();
+            } else {
+                // If we don't have ScreenNail available, we can still try to
+                // get the size information of it.
+                mModel.getImageSize(mIndex, mSize);
+            }
+
+            int w = mSize.width;
+            int h = mSize.height;
+            mSize.width = getRotated(mRotation, w, h);
+            mSize.height = getRotated(mRotation, h, w);
+        }
+
+        @Override
+        public boolean isCamera() {
+            return mIsCamera;
+        }
+
+        @Override
+        public boolean isDeletable() {
+            return mIsDeletable;
+        }
+    }
+
+    // Draw a gray placeholder in the specified rectangle.
+    private void drawPlaceHolder(GLCanvas canvas, Rect r) {
+        canvas.fillRect(r.left, r.top, r.width(), r.height(), mPlaceholderColor);
+    }
+
+    // Draw the video play icon (in the place where the spinner was)
+    private void drawVideoPlayIcon(GLCanvas canvas, int side) {
+        int s = side / ICON_RATIO;
+        // Draw the video play icon at the center
+        mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
+    }
+
+    // Draw the "no thumbnail" message
+    private void drawLoadingFailMessage(GLCanvas canvas) {
+        StringTexture m = mNoThumbnailText;
+        m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
+    }
+
+    private static int getRotated(int degree, int original, int theother) {
+        return (degree % 180 == 0) ? original : theother;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Gestures Handling
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        mGestureRecognizer.onTouchEvent(event);
+        return true;
+    }
+
+    private class MyGestureListener implements GestureRecognizer.Listener {
+        private boolean mIgnoreUpEvent = false;
+        // If we can change mode for this scale gesture.
+        private boolean mCanChangeMode;
+        // If we have changed the film mode in this scaling gesture.
+        private boolean mModeChanged;
+        // If this scaling gesture should be ignored.
+        private boolean mIgnoreScalingGesture;
+        // whether the down action happened while the view is scrolling.
+        private boolean mDownInScrolling;
+        // If we should ignore all gestures other than onSingleTapUp.
+        private boolean mIgnoreSwipingGesture;
+        // If a scrolling has happened after a down gesture.
+        private boolean mScrolledAfterDown;
+        // If the first scrolling move is in X direction. In the film mode, X
+        // direction scrolling is normal scrolling. but Y direction scrolling is
+        // a delete gesture.
+        private boolean mFirstScrollX;
+        // The accumulated Y delta that has been sent to mPositionController.
+        private int mDeltaY;
+        // The accumulated scaling change from a scaling gesture.
+        private float mAccScale;
+        // If an onFling happened after the last onDown
+        private boolean mHadFling;
+
+        @Override
+        public boolean onSingleTapUp(float x, float y) {
+            // On crespo running Android 2.3.6 (gingerbread), a pinch out gesture results in the
+            // following call sequence: onDown(), onUp() and then onSingleTapUp(). The correct
+            // sequence for a single-tap-up gesture should be: onDown(), onSingleTapUp() and onUp().
+            // The call sequence for a pinch out gesture in JB is: onDown(), then onUp() and there's
+            // no onSingleTapUp(). Base on these observations, the following condition is added to
+            // filter out the false alarm where onSingleTapUp() is called within a pinch out
+            // gesture. The framework fix went into ICS. Refer to b/4588114.
+            if (Build.VERSION.SDK_INT < ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) {
+                if ((mHolding & HOLD_TOUCH_DOWN) == 0) {
+                    return true;
+                }
+            }
+
+            // We do this in addition to onUp() because we want the snapback of
+            // setFilmMode to happen.
+            mHolding &= ~HOLD_TOUCH_DOWN;
+
+            if (mFilmMode && !mDownInScrolling) {
+                switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
+
+                // If this is a lock screen photo, let the listener handle the
+                // event. Tapping on lock screen photo should take the user
+                // directly to the lock screen.
+                MediaItem item = mModel.getMediaItem(0);
+                int supported = 0;
+                if (item != null) supported = item.getSupportedOperations();
+                if ((supported & MediaItem.SUPPORT_ACTION) == 0) {
+                    setFilmMode(false);
+                    mIgnoreUpEvent = true;
+                    return true;
+                }
+            }
+
+            if (mListener != null) {
+                // Do the inverse transform of the touch coordinates.
+                Matrix m = getGLRoot().getCompensationMatrix();
+                Matrix inv = new Matrix();
+                m.invert(inv);
+                float[] pts = new float[] {x, y};
+                inv.mapPoints(pts);
+                mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onDoubleTap(float x, float y) {
+            if (mIgnoreSwipingGesture) return true;
+            if (mPictures.get(0).isCamera()) return false;
+            PositionController controller = mPositionController;
+            float scale = controller.getImageScale();
+            // onDoubleTap happened on the second ACTION_DOWN.
+            // We need to ignore the next UP event.
+            mIgnoreUpEvent = true;
+            if (scale <= .75f || controller.isAtMinimalScale()) {
+                controller.zoomIn(x, y, Math.max(1.0f, scale * 1.5f));
+            } else {
+                controller.resetToFullView();
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onScroll(float dx, float dy, float totalX, float totalY) {
+            if (mIgnoreSwipingGesture) return true;
+            if (!mScrolledAfterDown) {
+                mScrolledAfterDown = true;
+                mFirstScrollX = (Math.abs(dx) > Math.abs(dy));
+            }
+
+            int dxi = (int) (-dx + 0.5f);
+            int dyi = (int) (-dy + 0.5f);
+            if (mFilmMode) {
+                if (mFirstScrollX) {
+                    mPositionController.scrollFilmX(dxi);
+                } else {
+                    if (mTouchBoxIndex == Integer.MAX_VALUE) return true;
+                    int newDeltaY = calculateDeltaY(totalY);
+                    int d = newDeltaY - mDeltaY;
+                    if (d != 0) {
+                        mPositionController.scrollFilmY(mTouchBoxIndex, d);
+                        mDeltaY = newDeltaY;
+                    }
+                }
+            } else {
+                mPositionController.scrollPage(dxi, dyi);
+            }
+            return true;
+        }
+
+        private int calculateDeltaY(float delta) {
+            if (mTouchBoxDeletable) return (int) (delta + 0.5f);
+
+            // don't let items that can't be deleted be dragged more than
+            // maxScrollDistance, and make it harder and harder to drag.
+            int size = getHeight();
+            float maxScrollDistance = 0.15f * size;
+            if (Math.abs(delta) >= size) {
+                delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
+            } else {
+                delta = maxScrollDistance *
+                        FloatMath.sin((delta / size) * (float) (Math.PI / 2));
+            }
+            return (int) (delta + 0.5f);
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+            if (mIgnoreSwipingGesture) return true;
+            if (mModeChanged) return true;
+            if (swipeImages(velocityX, velocityY)) {
+                mIgnoreUpEvent = true;
+            } else {
+                flingImages(velocityX, velocityY, Math.abs(e2.getY() - e1.getY()));
+            }
+            mHadFling = true;
+            return true;
+        }
+
+        private boolean flingImages(float velocityX, float velocityY, float dY) {
+            int vx = (int) (velocityX + 0.5f);
+            int vy = (int) (velocityY + 0.5f);
+            if (!mFilmMode) {
+                return mPositionController.flingPage(vx, vy);
+            }
+            if (Math.abs(velocityX) > Math.abs(velocityY)) {
+                return mPositionController.flingFilmX(vx);
+            }
+            // If we scrolled in Y direction fast enough, treat it as a delete
+            // gesture.
+            if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE
+                    || !mTouchBoxDeletable) {
+                return false;
+            }
+            int maxVelocity = GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY);
+            int escapeVelocity = GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY);
+            int escapeDistance = GalleryUtils.dpToPixel(SWIPE_ESCAPE_DISTANCE);
+            int centerY = mPositionController.getPosition(mTouchBoxIndex)
+                    .centerY();
+            boolean fastEnough = (Math.abs(vy) > escapeVelocity)
+                    && (Math.abs(vy) > Math.abs(vx))
+                    && ((vy > 0) == (centerY > getHeight() / 2))
+                    && dY >= escapeDistance;
+            if (fastEnough) {
+                vy = Math.min(vy, maxVelocity);
+                int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy);
+                if (duration >= 0) {
+                    mPositionController.setPopFromTop(vy < 0);
+                    deleteAfterAnimation(duration);
+                    // We reset mTouchBoxIndex, so up() won't check if Y
+                    // scrolled far enough to be a delete gesture.
+                    mTouchBoxIndex = Integer.MAX_VALUE;
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void deleteAfterAnimation(int duration) {
+            MediaItem item = mModel.getMediaItem(mTouchBoxIndex);
+            if (item == null) return;
+            mListener.onCommitDeleteImage();
+            mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex;
+            mHolding |= HOLD_DELETE;
+            Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE);
+            m.obj = item.getPath();
+            m.arg1 = mTouchBoxIndex;
+            mHandler.sendMessageDelayed(m, duration);
+        }
+
+        @Override
+        public boolean onScaleBegin(float focusX, float focusY) {
+            if (mIgnoreSwipingGesture) return true;
+            // We ignore the scaling gesture if it is a camera preview.
+            mIgnoreScalingGesture = mPictures.get(0).isCamera();
+            if (mIgnoreScalingGesture) {
+                return true;
+            }
+            mPositionController.beginScale(focusX, focusY);
+            // We can change mode if we are in film mode, or we are in page
+            // mode and at minimal scale.
+            mCanChangeMode = mFilmMode
+                    || mPositionController.isAtMinimalScale();
+            mAccScale = 1f;
+            return true;
+        }
+
+        @Override
+        public boolean onScale(float focusX, float focusY, float scale) {
+            if (mIgnoreSwipingGesture) return true;
+            if (mIgnoreScalingGesture) return true;
+            if (mModeChanged) return true;
+            if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
+
+            int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
+
+            // We wait for a large enough scale change before changing mode.
+            // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out
+            // or vice versa.
+            mAccScale *= scale;
+            boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f);
+
+            // If mode changes, we treat this scaling gesture has ended.
+            if (mCanChangeMode && largeEnough) {
+                if ((outOfRange < 0 && !mFilmMode) ||
+                        (outOfRange > 0 && mFilmMode)) {
+                    stopExtraScalingIfNeeded();
+
+                    // Removing the touch down flag allows snapback to happen
+                    // for film mode change.
+                    mHolding &= ~HOLD_TOUCH_DOWN;
+                    if (mFilmMode) {
+                        UsageStatistics.setPendingTransitionCause(
+                                UsageStatistics.TRANSITION_PINCH_OUT);
+                    } else {
+                        UsageStatistics.setPendingTransitionCause(
+                                UsageStatistics.TRANSITION_PINCH_IN);
+                    }
+                    setFilmMode(!mFilmMode);
+
+
+                    // We need to call onScaleEnd() before setting mModeChanged
+                    // to true.
+                    onScaleEnd();
+                    mModeChanged = true;
+                    return true;
+                }
+           }
+
+            if (outOfRange != 0) {
+                startExtraScalingIfNeeded();
+            } else {
+                stopExtraScalingIfNeeded();
+            }
+            return true;
+        }
+
+        @Override
+        public void onScaleEnd() {
+            if (mIgnoreSwipingGesture) return;
+            if (mIgnoreScalingGesture) return;
+            if (mModeChanged) return;
+            mPositionController.endScale();
+        }
+
+        private void startExtraScalingIfNeeded() {
+            if (!mCancelExtraScalingPending) {
+                mHandler.sendEmptyMessageDelayed(
+                        MSG_CANCEL_EXTRA_SCALING, 700);
+                mPositionController.setExtraScalingRange(true);
+                mCancelExtraScalingPending = true;
+            }
+        }
+
+        private void stopExtraScalingIfNeeded() {
+            if (mCancelExtraScalingPending) {
+                mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
+                mPositionController.setExtraScalingRange(false);
+                mCancelExtraScalingPending = false;
+            }
+        }
+
+        @Override
+        public void onDown(float x, float y) {
+            checkHideUndoBar(UNDO_BAR_TOUCHED);
+
+            mDeltaY = 0;
+            mModeChanged = false;
+
+            if (mIgnoreSwipingGesture) return;
+
+            mHolding |= HOLD_TOUCH_DOWN;
+
+            if (mFilmMode && mPositionController.isScrolling()) {
+                mDownInScrolling = true;
+                mPositionController.stopScrolling();
+            } else {
+                mDownInScrolling = false;
+            }
+            mHadFling = false;
+            mScrolledAfterDown = false;
+            if (mFilmMode) {
+                int xi = (int) (x + 0.5f);
+                int yi = (int) (y + 0.5f);
+                // We only care about being within the x bounds, necessary for
+                // handling very wide images which are otherwise very hard to fling
+                mTouchBoxIndex = mPositionController.hitTest(xi, getHeight() / 2);
+
+                if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) {
+                    mTouchBoxIndex = Integer.MAX_VALUE;
+                } else {
+                    mTouchBoxDeletable =
+                            mPictures.get(mTouchBoxIndex).isDeletable();
+                }
+            } else {
+                mTouchBoxIndex = Integer.MAX_VALUE;
+            }
+        }
+
+        @Override
+        public void onUp() {
+            if (mIgnoreSwipingGesture) return;
+
+            mHolding &= ~HOLD_TOUCH_DOWN;
+            mEdgeView.onRelease();
+
+            // If we scrolled in Y direction far enough, treat it as a delete
+            // gesture.
+            if (mFilmMode && mScrolledAfterDown && !mFirstScrollX
+                    && mTouchBoxIndex != Integer.MAX_VALUE) {
+                Rect r = mPositionController.getPosition(mTouchBoxIndex);
+                int h = getHeight();
+                if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) {
+                    int duration = mPositionController
+                            .flingFilmY(mTouchBoxIndex, 0);
+                    if (duration >= 0) {
+                        mPositionController.setPopFromTop(r.centerY() < h * 0.5f);
+                        deleteAfterAnimation(duration);
+                    }
+                }
+            }
+
+            if (mIgnoreUpEvent) {
+                mIgnoreUpEvent = false;
+                return;
+            }
+
+            if (!(mFilmMode && !mHadFling && mFirstScrollX
+                    && snapToNeighborImage())) {
+                snapback();
+            }
+        }
+
+        public void setSwipingEnabled(boolean enabled) {
+            mIgnoreSwipingGesture = !enabled;
+        }
+    }
+
+    public void setSwipingEnabled(boolean enabled) {
+        mGestureListener.setSwipingEnabled(enabled);
+    }
+
+    private void updateActionBar() {
+        boolean isCamera = mPictures.get(0).isCamera();
+        if (isCamera && !mFilmMode) {
+            // Move into camera in page mode, lock
+            mListener.onActionBarAllowed(false);
+        } else {
+            mListener.onActionBarAllowed(true);
+            if (mFilmMode) mListener.onActionBarWanted();
+        }
+    }
+
+    public void setFilmMode(boolean enabled) {
+        if (mFilmMode == enabled) return;
+        mFilmMode = enabled;
+        mPositionController.setFilmMode(mFilmMode);
+        mModel.setNeedFullImage(!enabled);
+        mModel.setFocusHintDirection(
+                mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT);
+        updateActionBar();
+        mListener.onFilmModeChanged(enabled);
+    }
+
+    public boolean getFilmMode() {
+        return mFilmMode;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Framework events
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void pause() {
+        mPositionController.skipAnimation();
+        mTileView.freeTextures();
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            mPictures.get(i).setScreenNail(null);
+        }
+        hideUndoBar();
+    }
+
+    public void resume() {
+        mTileView.prepareTextures();
+        mPositionController.skipToFinalPosition();
+    }
+
+    // move to the camera preview and show controls after resume
+    public void resetToFirstPicture() {
+        mModel.moveTo(0);
+        setFilmMode(false);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Undo Bar
+    ////////////////////////////////////////////////////////////////////////////
+
+    private int mUndoBarState;
+    private static final int UNDO_BAR_SHOW = 1;
+    private static final int UNDO_BAR_TIMEOUT = 2;
+    private static final int UNDO_BAR_TOUCHED = 4;
+    private static final int UNDO_BAR_FULL_CAMERA = 8;
+    private static final int UNDO_BAR_DELETE_LAST = 16;
+
+    // "deleteLast" means if the deletion is on the last remaining picture in
+    // the album.
+    private void showUndoBar(boolean deleteLast) {
+        mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
+        mUndoBarState = UNDO_BAR_SHOW;
+        if(deleteLast) mUndoBarState |= UNDO_BAR_DELETE_LAST;
+        mUndoBar.animateVisibility(GLView.VISIBLE);
+        mHandler.sendEmptyMessageDelayed(MSG_UNDO_BAR_TIMEOUT, 3000);
+        if (mListener != null) mListener.onUndoBarVisibilityChanged(true);
+    }
+
+    private void hideUndoBar() {
+        mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
+        mListener.onCommitDeleteImage();
+        mUndoBar.animateVisibility(GLView.INVISIBLE);
+        mUndoBarState = 0;
+        mUndoIndexHint = Integer.MAX_VALUE;
+        mListener.onUndoBarVisibilityChanged(false);
+    }
+
+    // Check if the one of the conditions for hiding the undo bar has been
+    // met. The conditions are:
+    //
+    // 1. It has been three seconds since last showing, and (a) the user has
+    // touched, or (b) the deleted picture is the last remaining picture in the
+    // album.
+    //
+    // 2. The camera is shown in full screen.
+    private void checkHideUndoBar(int addition) {
+        mUndoBarState |= addition;
+        if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return;
+        boolean timeout = (mUndoBarState & UNDO_BAR_TIMEOUT) != 0;
+        boolean touched = (mUndoBarState & UNDO_BAR_TOUCHED) != 0;
+        boolean fullCamera = (mUndoBarState & UNDO_BAR_FULL_CAMERA) != 0;
+        boolean deleteLast = (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0;
+        if ((timeout && deleteLast) || fullCamera || touched) {
+            hideUndoBar();
+        }
+    }
+
+    public boolean canUndo() {
+        return (mUndoBarState & UNDO_BAR_SHOW) != 0;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Rendering
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        if (mFirst) {
+            // Make sure the fields are properly initialized before checking
+            // whether isCamera()
+            mPictures.get(0).reload();
+        }
+        // Check if the camera preview occupies the full screen.
+        boolean full = !mFilmMode && mPictures.get(0).isCamera()
+                && mPositionController.isCenter()
+                && mPositionController.isAtMinimalScale();
+        if (mFirst || full != mFullScreenCamera) {
+            mFullScreenCamera = full;
+            mFirst = false;
+            mListener.onFullScreenChanged(full);
+            if (full) mHandler.sendEmptyMessage(MSG_UNDO_BAR_FULL_CAMERA);
+        }
+
+        // Determine how many photos we need to draw in addition to the center
+        // one.
+        int neighbors;
+        if (mFullScreenCamera) {
+            neighbors = 0;
+        } else {
+            // In page mode, we draw only one previous/next photo. But if we are
+            // doing capture animation, we want to draw all photos.
+            boolean inPageMode = (mPositionController.getFilmRatio() == 0f);
+            boolean inCaptureAnimation =
+                    ((mHolding & HOLD_CAPTURE_ANIMATION) != 0);
+            if (inPageMode && !inCaptureAnimation) {
+                neighbors = 1;
+            } else {
+                neighbors = SCREEN_NAIL_MAX;
+            }
+        }
+
+        // Draw photos from back to front
+        for (int i = neighbors; i >= -neighbors; i--) {
+            Rect r = mPositionController.getPosition(i);
+            mPictures.get(i).draw(canvas, r);
+        }
+
+        renderChild(canvas, mEdgeView);
+        renderChild(canvas, mUndoBar);
+
+        mPositionController.advanceAnimation();
+        checkFocusSwitching();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Film mode focus switching
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Runs in GL thread.
+    private void checkFocusSwitching() {
+        if (!mFilmMode) return;
+        if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return;
+        if (switchPosition() != 0) {
+            mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS);
+        }
+    }
+
+    // Runs in main thread.
+    private void switchFocus() {
+        if (mHolding != 0) return;
+        switch (switchPosition()) {
+            case -1:
+                switchToPrevImage();
+                break;
+            case 1:
+                switchToNextImage();
+                break;
+        }
+    }
+
+    // Returns -1 if we should switch focus to the previous picture, +1 if we
+    // should switch to the next, 0 otherwise.
+    private int switchPosition() {
+        Rect curr = mPositionController.getPosition(0);
+        int center = getWidth() / 2;
+
+        if (curr.left > center && mPrevBound < 0) {
+            Rect prev = mPositionController.getPosition(-1);
+            int currDist = curr.left - center;
+            int prevDist = center - prev.right;
+            if (prevDist < currDist) {
+                return -1;
+            }
+        } else if (curr.right < center && mNextBound > 0) {
+            Rect next = mPositionController.getPosition(1);
+            int currDist = center - curr.right;
+            int nextDist = next.left - center;
+            if (nextDist < currDist) {
+                return 1;
+            }
+        }
+
+        return 0;
+    }
+
+    // Switch to the previous or next picture if the hit position is inside
+    // one of their boxes. This runs in main thread.
+    private void switchToHitPicture(int x, int y) {
+        if (mPrevBound < 0) {
+            Rect r = mPositionController.getPosition(-1);
+            if (r.right >= x) {
+                slideToPrevPicture();
+                return;
+            }
+        }
+
+        if (mNextBound > 0) {
+            Rect r = mPositionController.getPosition(1);
+            if (r.left <= x) {
+                slideToNextPicture();
+                return;
+            }
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Page mode focus switching
+    //
+    //  We slide image to the next one or the previous one in two cases: 1: If
+    //  the user did a fling gesture with enough velocity.  2 If the user has
+    //  moved the picture a lot.
+    ////////////////////////////////////////////////////////////////////////////
+
+    private boolean swipeImages(float velocityX, float velocityY) {
+        if (mFilmMode) return false;
+
+        // Avoid swiping images if we're possibly flinging to view the
+        // zoomed in picture vertically.
+        PositionController controller = mPositionController;
+        boolean isMinimal = controller.isAtMinimalScale();
+        int edges = controller.getImageAtEdges();
+        if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
+            if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
+                    || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
+                return false;
+
+        // If we are at the edge of the current photo and the sweeping velocity
+        // exceeds the threshold, slide to the next / previous image.
+        if (velocityX < -SWIPE_THRESHOLD && (isMinimal
+                || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
+            return slideToNextPicture();
+        } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
+                || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
+            return slideToPrevPicture();
+        }
+
+        return false;
+    }
+
+    private void snapback() {
+        if ((mHolding & ~HOLD_DELETE) != 0) return;
+        if (mFilmMode || !snapToNeighborImage()) {
+            mPositionController.snapback();
+        }
+    }
+
+    private boolean snapToNeighborImage() {
+        Rect r = mPositionController.getPosition(0);
+        int viewW = getWidth();
+        // Setting the move threshold proportional to the width of the view
+        int moveThreshold = viewW / 5 ;
+        int threshold = moveThreshold + gapToSide(r.width(), viewW);
+
+        // If we have moved the picture a lot, switching.
+        if (viewW - r.right > threshold) {
+            return slideToNextPicture();
+        } else if (r.left > threshold) {
+            return slideToPrevPicture();
+        }
+
+        return false;
+    }
+
+    private boolean slideToNextPicture() {
+        if (mNextBound <= 0) return false;
+        switchToNextImage();
+        mPositionController.startHorizontalSlide();
+        return true;
+    }
+
+    private boolean slideToPrevPicture() {
+        if (mPrevBound >= 0) return false;
+        switchToPrevImage();
+        mPositionController.startHorizontalSlide();
+        return true;
+    }
+
+    private static int gapToSide(int imageWidth, int viewWidth) {
+        return Math.max(0, (viewWidth - imageWidth) / 2);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Focus switching
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void switchToImage(int index) {
+        mModel.moveTo(index);
+    }
+
+    private void switchToNextImage() {
+        mModel.moveTo(mModel.getCurrentIndex() + 1);
+    }
+
+    private void switchToPrevImage() {
+        mModel.moveTo(mModel.getCurrentIndex() - 1);
+    }
+
+    private void switchToFirstImage() {
+        mModel.moveTo(0);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Opening Animation
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void setOpenAnimationRect(Rect rect) {
+        mPositionController.setOpenAnimationRect(rect);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Capture Animation
+    ////////////////////////////////////////////////////////////////////////////
+
+    public boolean switchWithCaptureAnimation(int offset) {
+        GLRoot root = getGLRoot();
+        if(root == null) return false;
+        root.lockRenderThread();
+        try {
+            return switchWithCaptureAnimationLocked(offset);
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    private boolean switchWithCaptureAnimationLocked(int offset) {
+        if (mHolding != 0) return true;
+        if (offset == 1) {
+            if (mNextBound <= 0) return false;
+            // Temporary disable action bar until the capture animation is done.
+            if (!mFilmMode) mListener.onActionBarAllowed(false);
+            switchToNextImage();
+            mPositionController.startCaptureAnimationSlide(-1);
+        } else if (offset == -1) {
+            if (mPrevBound >= 0) return false;
+            if (mFilmMode) setFilmMode(false);
+
+            // If we are too far away from the first image (so that we don't
+            // have all the ScreenNails in-between), we go directly without
+            // animation.
+            if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) {
+                switchToFirstImage();
+                mPositionController.skipToFinalPosition();
+                return true;
+            }
+
+            switchToFirstImage();
+            mPositionController.startCaptureAnimationSlide(1);
+        } else {
+            return false;
+        }
+        mHolding |= HOLD_CAPTURE_ANIMATION;
+        Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
+        mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
+        return true;
+    }
+
+    private void captureAnimationDone(int offset) {
+        mHolding &= ~HOLD_CAPTURE_ANIMATION;
+        if (offset == 1 && !mFilmMode) {
+            // Now the capture animation is done, enable the action bar.
+            mListener.onActionBarAllowed(true);
+            mListener.onActionBarWanted();
+        }
+        snapback();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Card deck effect calculation
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Returns the scrolling progress value for an object moving out of a
+    // view. The progress value measures how much the object has moving out of
+    // the view. The object currently displays in [left, right), and the view is
+    // at [0, viewWidth].
+    //
+    // The returned value is negative when the object is moving right, and
+    // positive when the object is moving left. The value goes to -1 or 1 when
+    // the object just moves out of the view completely. The value is 0 if the
+    // object currently fills the view.
+    private static float calculateMoveOutProgress(int left, int right,
+            int viewWidth) {
+        // w = object width
+        // viewWidth = view width
+        int w = right - left;
+
+        // If the object width is smaller than the view width,
+        //      |....view....|
+        //                   |<-->|      progress = -1 when left = viewWidth
+        //          |<-->|               progress = 0 when left = viewWidth / 2 - w / 2
+        // |<-->|                        progress = 1 when left = -w
+        if (w < viewWidth) {
+            int zx = viewWidth / 2 - w / 2;
+            if (left > zx) {
+                return -(left - zx) / (float) (viewWidth - zx);  // progress = (0, -1]
+            } else {
+                return (left - zx) / (float) (-w - zx);  // progress = [0, 1]
+            }
+        }
+
+        // If the object width is larger than the view width,
+        //             |..view..|
+        //                      |<--------->| progress = -1 when left = viewWidth
+        //             |<--------->|          progress = 0 between left = 0
+        //          |<--------->|                          and right = viewWidth
+        // |<--------->|                      progress = 1 when right = 0
+        if (left > 0) {
+            return -left / (float) viewWidth;
+        }
+
+        if (right < viewWidth) {
+            return (viewWidth - right) / (float) viewWidth;
+        }
+
+        return 0;
+    }
+
+    // Maps a scrolling progress value to the alpha factor in the fading
+    // animation.
+    private float getScrollAlpha(float scrollProgress) {
+        return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation(
+                     1 - Math.abs(scrollProgress)) : 1.0f;
+    }
+
+    // Maps a scrolling progress value to the scaling factor in the fading
+    // animation.
+    private float getScrollScale(float scrollProgress) {
+        float interpolatedProgress = mScaleInterpolator.getInterpolation(
+                Math.abs(scrollProgress));
+        float scale = (1 - interpolatedProgress) +
+                interpolatedProgress * TRANSITION_SCALE_FACTOR;
+        return scale;
+    }
+
+
+    // This interpolator emulates the rate at which the perceived scale of an
+    // object changes as its distance from a camera increases. When this
+    // interpolator is applied to a scale animation on a view, it evokes the
+    // sense that the object is shrinking due to moving away from the camera.
+    private static class ZInterpolator {
+        private float focalLength;
+
+        public ZInterpolator(float foc) {
+            focalLength = foc;
+        }
+
+        public float getInterpolation(float input) {
+            return (1.0f - focalLength / (focalLength + input)) /
+                (1.0f - focalLength / (focalLength + 1.0f));
+        }
+    }
+
+    // Returns an interpolated value for the page/film transition.
+    // When ratio = 0, the result is from.
+    // When ratio = 1, the result is to.
+    private static float interpolate(float ratio, float from, float to) {
+        return from + (to - from) * ratio * ratio;
+    }
+
+    // Returns the alpha factor in film mode if a picture is not in the center.
+    // The 0.03 lower bound is to make the item always visible a bit.
+    private float getOffsetAlpha(float offset) {
+        offset /= 0.5f;
+        float alpha = (offset > 0) ? (1 - offset) : (1 + offset);
+        return Utils.clamp(alpha, 0.03f, 1f);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Simple public utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public Rect getPhotoRect(int index) {
+        return mPositionController.getPosition(index);
+    }
+
+    public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
+        Rect location = new Rect();
+        Utils.assertTrue(root.getBoundsOf(this, location));
+
+        Rect fullRect = bounds();
+        PhotoFallbackEffect effect = new PhotoFallbackEffect();
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            MediaItem item = mModel.getMediaItem(i);
+            if (item == null) continue;
+            ScreenNail sc = mModel.getScreenNail(i);
+            if (!(sc instanceof TiledScreenNail)
+                    || ((TiledScreenNail) sc).isShowingPlaceholder()) continue;
+
+            // Now, sc is BitmapScreenNail and is not showing placeholder
+            Rect rect = new Rect(getPhotoRect(i));
+            if (!Rect.intersects(fullRect, rect)) continue;
+            rect.offset(location.left, location.top);
+
+            int width = sc.getWidth();
+            int height = sc.getHeight();
+
+            int rotation = mModel.getImageRotation(i);
+            RawTexture texture;
+            if ((rotation % 180) == 0) {
+                texture = new RawTexture(width, height, true);
+                canvas.beginRenderTarget(texture);
+                canvas.translate(width / 2f, height / 2f);
+            } else {
+                texture = new RawTexture(height, width, true);
+                canvas.beginRenderTarget(texture);
+                canvas.translate(height / 2f, width / 2f);
+            }
+
+            canvas.rotate(rotation, 0, 0, 1);
+            canvas.translate(-width / 2f, -height / 2f);
+            sc.draw(canvas, 0, 0, width, height);
+            canvas.endRenderTarget();
+            effect.addEntry(item.getPath(), rect, texture);
+        }
+        return effect;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/PopupList.java b/src/com/android/gallery3d/ui/PopupList.java
new file mode 100644
index 0000000..248f50b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PopupList.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class PopupList {
+
+    public static interface OnPopupItemClickListener {
+        public boolean onPopupItemClick(int itemId);
+    }
+
+    public static class Item {
+        public final int id;
+        public String title;
+
+        public Item(int id, String title) {
+            this.id = id;
+            this.title = title;
+        }
+
+        public void setTitle(String title) {
+            this.title = title;
+        }
+    }
+
+    private final Context mContext;
+    private final View mAnchorView;
+    private final ArrayList<Item> mItems = new ArrayList<Item>();
+    private PopupWindow mPopupWindow;
+    private ListView mContentList;
+    private OnPopupItemClickListener mOnPopupItemClickListener;
+    private int mPopupOffsetX;
+    private int mPopupOffsetY;
+    private int mPopupWidth;
+    private int mPopupHeight;
+
+    public PopupList(Context context, View anchorView) {
+        mContext = context;
+        mAnchorView = anchorView;
+    }
+
+    public void setOnPopupItemClickListener(OnPopupItemClickListener listener) {
+        mOnPopupItemClickListener = listener;
+    }
+
+    public void addItem(int id, String title) {
+        mItems.add(new Item(id, title));
+    }
+
+    public void clearItems() {
+        mItems.clear();
+    }
+
+    private final PopupWindow.OnDismissListener mOnDismissListener =
+            new PopupWindow.OnDismissListener() {
+        @SuppressWarnings("deprecation")
+        @Override
+        public void onDismiss() {
+            if (mPopupWindow == null) return;
+            mPopupWindow = null;
+            ViewTreeObserver observer = mAnchorView.getViewTreeObserver();
+            if (observer.isAlive()) {
+                // We used the deprecated function for backward compatibility
+                // The new "removeOnGlobalLayoutListener" is introduced in API level 16
+                observer.removeGlobalOnLayoutListener(mOnGLobalLayoutListener);
+            }
+        }
+    };
+
+    private final OnItemClickListener mOnItemClickListener =
+            new OnItemClickListener() {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            if (mPopupWindow == null) return;
+            mPopupWindow.dismiss();
+            if (mOnPopupItemClickListener != null) {
+                mOnPopupItemClickListener.onPopupItemClick((int) id);
+            }
+        }
+    };
+
+    private final OnGlobalLayoutListener mOnGLobalLayoutListener =
+            new OnGlobalLayoutListener() {
+        @Override
+        public void onGlobalLayout() {
+            if (mPopupWindow == null) return;
+            updatePopupLayoutParams();
+            // Need to update the position of the popup window
+            mPopupWindow.update(mAnchorView,
+                    mPopupOffsetX, mPopupOffsetY, mPopupWidth, mPopupHeight);
+        }
+    };
+
+    public void show() {
+        if (mPopupWindow != null) return;
+        mAnchorView.getViewTreeObserver()
+                .addOnGlobalLayoutListener(mOnGLobalLayoutListener);
+        mPopupWindow = createPopupWindow();
+        updatePopupLayoutParams();
+        mPopupWindow.setWidth(mPopupWidth);
+        mPopupWindow.setHeight(mPopupHeight);
+        mPopupWindow.showAsDropDown(mAnchorView, mPopupOffsetX, mPopupOffsetY);
+    }
+
+    private void updatePopupLayoutParams() {
+        ListView content = mContentList;
+        PopupWindow popup = mPopupWindow;
+
+        Rect p = new Rect();
+        popup.getBackground().getPadding(p);
+
+        int maxHeight = mPopupWindow.getMaxAvailableHeight(mAnchorView) - p.top - p.bottom;
+        mContentList.measure(
+                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST));
+        mPopupWidth = content.getMeasuredWidth() + p.top + p.bottom;
+        mPopupHeight = Math.min(maxHeight, content.getMeasuredHeight() + p.left + p.right);
+        mPopupOffsetX = -p.left;
+        mPopupOffsetY = -p.top;
+    }
+
+    private PopupWindow createPopupWindow() {
+        PopupWindow popup = new PopupWindow(mContext);
+        popup.setOnDismissListener(mOnDismissListener);
+
+        popup.setBackgroundDrawable(mContext.getResources().getDrawable(
+                R.drawable.menu_dropdown_panel_holo_dark));
+
+        mContentList = new ListView(mContext, null,
+                android.R.attr.dropDownListViewStyle);
+        mContentList.setAdapter(new ItemDataAdapter());
+        mContentList.setOnItemClickListener(mOnItemClickListener);
+        popup.setContentView(mContentList);
+        popup.setFocusable(true);
+        popup.setOutsideTouchable(true);
+
+        return popup;
+    }
+
+    public Item findItem(int id) {
+        for (Item item : mItems) {
+            if (item.id == id) return item;
+        }
+        return null;
+    }
+
+    private class ItemDataAdapter extends BaseAdapter {
+        @Override
+        public int getCount() {
+            return mItems.size();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mItems.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return mItems.get(position).id;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = LayoutInflater.from(mContext)
+                        .inflate(R.layout.popup_list_item, null);
+            }
+            TextView text = (TextView) convertView.findViewById(android.R.id.text1);
+            text.setText(mItems.get(position).title);
+            return convertView;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
new file mode 100644
index 0000000..6a4bcea
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -0,0 +1,1821 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.Log;
+import android.widget.Scroller;
+
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.PhotoView.Size;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.RangeIntArray;
+
+class PositionController {
+    private static final String TAG = "PositionController";
+
+    public static final int IMAGE_AT_LEFT_EDGE = 1;
+    public static final int IMAGE_AT_RIGHT_EDGE = 2;
+    public static final int IMAGE_AT_TOP_EDGE = 4;
+    public static final int IMAGE_AT_BOTTOM_EDGE = 8;
+
+    public static final int CAPTURE_ANIMATION_TIME = 700;
+    public static final int SNAPBACK_ANIMATION_TIME = 600;
+
+    // Special values for animation time.
+    private static final long NO_ANIMATION = -1;
+    private static final long LAST_ANIMATION = -2;
+
+    private static final int ANIM_KIND_NONE = -1;
+    private static final int ANIM_KIND_SCROLL = 0;
+    private static final int ANIM_KIND_SCALE = 1;
+    private static final int ANIM_KIND_SNAPBACK = 2;
+    private static final int ANIM_KIND_SLIDE = 3;
+    private static final int ANIM_KIND_ZOOM = 4;
+    private static final int ANIM_KIND_OPENING = 5;
+    private static final int ANIM_KIND_FLING = 6;
+    private static final int ANIM_KIND_FLING_X = 7;
+    private static final int ANIM_KIND_DELETE = 8;
+    private static final int ANIM_KIND_CAPTURE = 9;
+
+    // Animation time in milliseconds. The order must match ANIM_KIND_* above.
+    //
+    // The values for ANIM_KIND_FLING_X does't matter because we use
+    // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's
+    // faster for Animatable.advanceAnimation() to calculate the progress
+    // (always 1).
+    private static final int ANIM_TIME[] = {
+        0,    // ANIM_KIND_SCROLL
+        0,    // ANIM_KIND_SCALE
+        SNAPBACK_ANIMATION_TIME,  // ANIM_KIND_SNAPBACK
+        400,  // ANIM_KIND_SLIDE
+        300,  // ANIM_KIND_ZOOM
+        300,  // ANIM_KIND_OPENING
+        0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
+        0,    // ANIM_KIND_FLING_X (see the comment above)
+        0,    // ANIM_KIND_DELETE (the duration is calculated dynamically)
+        CAPTURE_ANIMATION_TIME,  // ANIM_KIND_CAPTURE
+    };
+
+    // We try to scale up the image to fill the screen. But in order not to
+    // scale too much for small icons, we limit the max up-scaling factor here.
+    private static final float SCALE_LIMIT = 4;
+
+    // For user's gestures, we give a temporary extra scaling range which goes
+    // above or below the usual scaling limits.
+    private static final float SCALE_MIN_EXTRA = 0.7f;
+    private static final float SCALE_MAX_EXTRA = 1.4f;
+
+    // Setting this true makes the extra scaling range permanent (until this is
+    // set to false again).
+    private boolean mExtraScalingRange = false;
+
+    // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
+    private boolean mFilmMode = false;
+
+    // These are the limits for width / height of the picture in film mode.
+    private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
+    private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
+    private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
+    private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
+
+    // In addition to the focused box (index == 0). We also keep information
+    // about this many boxes on each side.
+    private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
+    private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1];
+
+    private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
+    private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
+
+    // These are constants for the delete gesture.
+    private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms
+    private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms
+
+    private Listener mListener;
+    private volatile Rect mOpenAnimationRect;
+
+    // Use a large enough value, so we won't see the gray shadow in the beginning.
+    private int mViewW = 1200;
+    private int mViewH = 1200;
+
+    // A scaling gesture is in progress.
+    private boolean mInScale;
+    // The focus point of the scaling gesture, relative to the center of the
+    // picture in bitmap pixels.
+    private float mFocusX, mFocusY;
+
+    // whether there is a previous/next picture.
+    private boolean mHasPrev, mHasNext;
+
+    // This is used by the fling animation (page mode).
+    private FlingScroller mPageScroller;
+
+    // This is used by the fling animation (film mode).
+    private Scroller mFilmScroller;
+
+    // The bound of the stable region that the focused box can stay, see the
+    // comments above calculateStableBound() for details.
+    private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
+
+    // Constrained frame is a rectangle that the focused box should fit into if
+    // it is constrained. It has two effects:
+    //
+    // (1) In page mode, if the focused box is constrained, scaling for the
+    // focused box is adjusted to fit into the constrained frame, instead of the
+    // whole view.
+    //
+    // (2) In page mode, if the focused box is constrained, the mPlatform's
+    // default center (mDefaultX/Y) is moved to the center of the constrained
+    // frame, instead of the view center.
+    //
+    private Rect mConstrainedFrame = new Rect();
+
+    // Whether the focused box is constrained.
+    //
+    // Our current program's first call to moveBox() sets constrained = true, so
+    // we set the initial value of this variable to true, and we will not see
+    // see unwanted transition animation.
+    private boolean mConstrained = true;
+
+    //
+    //  ___________________________________________________________
+    // |   _____       _____       _____       _____       _____   |
+    // |  |     |     |     |     |     |     |     |     |     |  |
+    // |  | Box |     | Box |     | Box*|     | Box |     | Box |  |
+    // |  |_____|.....|_____|.....|_____|.....|_____|.....|_____|  |
+    // |          Gap         Gap         Gap         Gap          |
+    // |___________________________________________________________|
+    //
+    //                       <--  Platform  -->
+    //
+    // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY)
+
+    private Platform mPlatform = new Platform();
+    private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+    // The gap at the right of a Box i is at index i. The gap at the left of a
+    // Box i is at index i - 1.
+    private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+    private FilmRatio mFilmRatio = new FilmRatio();
+
+    // These are only used during moveBox().
+    private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+    private RangeArray<Gap> mTempGaps =
+        new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+
+    // The output of the PositionController. Available through getPosition().
+    private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
+
+    // The direction of a new picture should appear. New pictures pop from top
+    // if this value is true, or from bottom if this value is false.
+    boolean mPopFromTop;
+
+    public interface Listener {
+        void invalidate();
+        boolean isHoldingDown();
+        boolean isHoldingDelete();
+
+        // EdgeView
+        void onPull(int offset, int direction);
+        void onRelease();
+        void onAbsorb(int velocity, int direction);
+    }
+
+    static {
+        // Initialize the CENTER_OUT_INDEX array.
+        // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX
+        // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX
+        for (int i = 0; i < CENTER_OUT_INDEX.length; i++) {
+            int j = (i + 1) / 2;
+            if ((i & 1) == 0) j = -j;
+            CENTER_OUT_INDEX[i] = j;
+        }
+    }
+
+    public PositionController(Context context, Listener listener) {
+        mListener = listener;
+        mPageScroller = new FlingScroller();
+        mFilmScroller = new Scroller(context, null, false);
+
+        // Initialize the areas.
+        initPlatform();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mBoxes.put(i, new Box());
+            initBox(i);
+            mRects.put(i, new Rect());
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mGaps.put(i, new Gap());
+            initGap(i);
+        }
+    }
+
+    public void setOpenAnimationRect(Rect r) {
+        mOpenAnimationRect = r;
+    }
+
+    public void setViewSize(int viewW, int viewH) {
+        if (viewW == mViewW && viewH == mViewH) return;
+
+        boolean wasMinimal = isAtMinimalScale();
+
+        mViewW = viewW;
+        mViewH = viewH;
+        initPlatform();
+
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            setBoxSize(i, viewW, viewH, true);
+        }
+
+        updateScaleAndGapLimit();
+
+        // If the focused box was at minimal scale, we try to make it the
+        // minimal scale under the new view size.
+        if (wasMinimal) {
+            Box b = mBoxes.get(0);
+            b.mCurrentScale = b.mScaleMin;
+        }
+
+        // If we have the opening animation, do it. Otherwise go directly to the
+        // right position.
+        if (!startOpeningAnimationIfNeeded()) {
+            skipToFinalPosition();
+        }
+    }
+
+    public void setConstrainedFrame(Rect cFrame) {
+        if (mConstrainedFrame.equals(cFrame)) return;
+        mConstrainedFrame.set(cFrame);
+        mPlatform.updateDefaultXY();
+        updateScaleAndGapLimit();
+        snapAndRedraw();
+    }
+
+    public void forceImageSize(int index, Size s) {
+        if (s.width == 0 || s.height == 0) return;
+        Box b = mBoxes.get(index);
+        b.mImageW = s.width;
+        b.mImageH = s.height;
+        return;
+    }
+
+    public void setImageSize(int index, Size s, Rect cFrame) {
+        if (s.width == 0 || s.height == 0) return;
+
+        boolean needUpdate = false;
+        if (cFrame != null && !mConstrainedFrame.equals(cFrame)) {
+            mConstrainedFrame.set(cFrame);
+            mPlatform.updateDefaultXY();
+            needUpdate = true;
+        }
+        needUpdate |= setBoxSize(index, s.width, s.height, false);
+
+        if (!needUpdate) return;
+        updateScaleAndGapLimit();
+        snapAndRedraw();
+    }
+
+    // Returns false if the box size doesn't change.
+    private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
+        Box b = mBoxes.get(i);
+        boolean wasViewSize = b.mUseViewSize;
+
+        // If we already have an image size, we don't want to use the view size.
+        if (!wasViewSize && isViewSize) return false;
+
+        b.mUseViewSize = isViewSize;
+
+        if (width == b.mImageW && height == b.mImageH) {
+            return false;
+        }
+
+        // The ratio of the old size and the new size.
+        //
+        // If the aspect ratio changes, we don't know if it is because one side
+        // grows or the other side shrinks. Currently we just assume the view
+        // angle of the longer side doesn't change (so the aspect ratio change
+        // is because the view angle of the shorter side changes). This matches
+        // what camera preview does.
+        float ratio = (width > height)
+                ? (float) b.mImageW / width
+                : (float) b.mImageH / height;
+
+        b.mImageW = width;
+        b.mImageH = height;
+
+        // If this is the first time we receive an image size or we are in fullscreen,
+        // we change the scale directly. Otherwise adjust the scales by a ratio,
+        // and snapback will animate the scale into the min/max bounds if necessary.
+        if ((wasViewSize && !isViewSize) || !mFilmMode) {
+            b.mCurrentScale = getMinimalScale(b);
+            b.mAnimationStartTime = NO_ANIMATION;
+        } else {
+            b.mCurrentScale *= ratio;
+            b.mFromScale *= ratio;
+            b.mToScale *= ratio;
+        }
+
+        if (i == 0) {
+            mFocusX /= ratio;
+            mFocusY /= ratio;
+        }
+
+        return true;
+    }
+
+    private boolean startOpeningAnimationIfNeeded() {
+        if (mOpenAnimationRect == null) return false;
+        Box b = mBoxes.get(0);
+        if (b.mUseViewSize) return false;
+
+        // Start animation from the saved rectangle if we have one.
+        Rect r = mOpenAnimationRect;
+        mOpenAnimationRect = null;
+
+        mPlatform.mCurrentX = r.centerX() - mViewW / 2;
+        b.mCurrentY = r.centerY() - mViewH / 2;
+        b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
+                r.height() / (float) b.mImageH);
+        startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin,
+                ANIM_KIND_OPENING);
+
+        // Animate from large gaps for neighbor boxes to avoid them
+        // shown on the screen during opening animation.
+        for (int i = -1; i < 1; i++) {
+            Gap g = mGaps.get(i);
+            g.mCurrentGap = mViewW;
+            g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING);
+        }
+
+        return true;
+    }
+
+    public void setFilmMode(boolean enabled) {
+        if (enabled == mFilmMode) return;
+        mFilmMode = enabled;
+
+        mPlatform.updateDefaultXY();
+        updateScaleAndGapLimit();
+        stopAnimation();
+        snapAndRedraw();
+    }
+
+    public void setExtraScalingRange(boolean enabled) {
+        if (mExtraScalingRange == enabled) return;
+        mExtraScalingRange = enabled;
+        if (!enabled) {
+            snapAndRedraw();
+        }
+    }
+
+    // This should be called whenever the scale range of boxes or the default
+    // gap size may change. Currently this can happen due to change of view
+    // size, image size, mFilmMode, mConstrained, and mConstrainedFrame.
+    private void updateScaleAndGapLimit() {
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            Box b = mBoxes.get(i);
+            b.mScaleMin = getMinimalScale(b);
+            b.mScaleMax = getMaximalScale(b);
+        }
+
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            Gap g = mGaps.get(i);
+            g.mDefaultSize = getDefaultGapSize(i);
+        }
+    }
+
+    // Returns the default gap size according the the size of the boxes around
+    // the gap and the current mode.
+    private int getDefaultGapSize(int i) {
+        if (mFilmMode) return IMAGE_GAP;
+        Box a = mBoxes.get(i);
+        Box b = mBoxes.get(i + 1);
+        return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
+    }
+
+    // Here is how we layout the boxes in the page mode.
+    //
+    //   previous             current             next
+    //  ___________       ________________     __________
+    // |  _______  |     |   __________   |   |  ______  |
+    // | |       | |     |  |   right->|  |   | |      | |
+    // | |       |<-------->|<--left   |  |   | |      | |
+    // | |_______| |  |  |  |__________|  |   | |______| |
+    // |___________|  |  |________________|   |__________|
+    //                |  <--> gapToSide()
+    //                |
+    // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
+    private int gapToSide(Box b) {
+        return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
+    }
+
+    // Stop all animations at where they are now.
+    public void stopAnimation() {
+        mPlatform.mAnimationStartTime = NO_ANIMATION;
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
+        }
+    }
+
+    public void skipAnimation() {
+        if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
+            mPlatform.mCurrentX = mPlatform.mToX;
+            mPlatform.mCurrentY = mPlatform.mToY;
+            mPlatform.mAnimationStartTime = NO_ANIMATION;
+        }
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            Box b = mBoxes.get(i);
+            if (b.mAnimationStartTime == NO_ANIMATION) continue;
+            b.mCurrentY = b.mToY;
+            b.mCurrentScale = b.mToScale;
+            b.mAnimationStartTime = NO_ANIMATION;
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            Gap g = mGaps.get(i);
+            if (g.mAnimationStartTime == NO_ANIMATION) continue;
+            g.mCurrentGap = g.mToGap;
+            g.mAnimationStartTime = NO_ANIMATION;
+        }
+        redraw();
+    }
+
+    public void snapback() {
+        snapAndRedraw();
+    }
+
+    public void skipToFinalPosition() {
+        stopAnimation();
+        snapAndRedraw();
+        skipAnimation();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Start an animations for the focused box
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void zoomIn(float tapX, float tapY, float targetScale) {
+        tapX -= mViewW / 2;
+        tapY -= mViewH / 2;
+        Box b = mBoxes.get(0);
+
+        // Convert the tap position to distance to center in bitmap coordinates
+        float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
+        float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
+
+        int x = (int) (-tempX * targetScale + 0.5f);
+        int y = (int) (-tempY * targetScale + 0.5f);
+
+        calculateStableBound(targetScale);
+        int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
+        int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
+        targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
+
+        startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
+    }
+
+    public void resetToFullView() {
+        Box b = mBoxes.get(0);
+        startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM);
+    }
+
+    public void beginScale(float focusX, float focusY) {
+        focusX -= mViewW / 2;
+        focusY -= mViewH / 2;
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+        mInScale = true;
+        mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
+        mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
+    }
+
+    // Scales the image by the given factor.
+    // Returns an out-of-range indicator:
+    //   1 if the intended scale is too large for the stable range.
+    //   0 if the intended scale is in the stable range.
+    //  -1 if the intended scale is too small for the stable range.
+    public int scaleBy(float s, float focusX, float focusY) {
+        focusX -= mViewW / 2;
+        focusY -= mViewH / 2;
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
+        // We want to keep the focus point (on the bitmap) the same as when we
+        // begin the scale gesture, that is,
+        //
+        // (focusX' - currentX') / scale' = (focusX - currentX) / scale
+        //
+        s = b.clampScale(s * getTargetScale(b));
+        int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
+        int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
+        startAnimation(x, y, s, ANIM_KIND_SCALE);
+        if (s < b.mScaleMin) return -1;
+        if (s > b.mScaleMax) return 1;
+        return 0;
+    }
+
+    public void endScale() {
+        mInScale = false;
+        snapAndRedraw();
+    }
+
+    // Slide the focused box to the center of the view.
+    public void startHorizontalSlide() {
+        Box b = mBoxes.get(0);
+        startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE);
+    }
+
+    // Slide the focused box to the center of the view with the capture
+    // animation. In addition to the sliding, the animation will also scale the
+    // the focused box, the specified neighbor box, and the gap between the
+    // two. The specified offset should be 1 or -1.
+    public void startCaptureAnimationSlide(int offset) {
+        Box b = mBoxes.get(0);
+        Box n = mBoxes.get(offset);  // the neighbor box
+        Gap g = mGaps.get(offset);  // the gap between the two boxes
+
+        mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY,
+                ANIM_KIND_CAPTURE);
+        b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE);
+        n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE);
+        g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
+        redraw();
+    }
+
+    // Only allow scrolling when we are not currently in an animation or we
+    // are in some animation with can be interrupted.
+    private boolean canScroll() {
+        Box b = mBoxes.get(0);
+        if (b.mAnimationStartTime == NO_ANIMATION) return true;
+        switch (b.mAnimationKind) {
+            case ANIM_KIND_SCROLL:
+            case ANIM_KIND_FLING:
+            case ANIM_KIND_FLING_X:
+                return true;
+        }
+        return false;
+    }
+
+    public void scrollPage(int dx, int dy) {
+        if (!canScroll()) return;
+
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
+        calculateStableBound(b.mCurrentScale);
+
+        int x = p.mCurrentX + dx;
+        int y = b.mCurrentY + dy;
+
+        // Vertical direction: If we have space to move in the vertical
+        // direction, we show the edge effect when scrolling reaches the edge.
+        if (mBoundTop != mBoundBottom) {
+            if (y < mBoundTop) {
+                mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
+            } else if (y > mBoundBottom) {
+                mListener.onPull(y - mBoundBottom, EdgeView.TOP);
+            }
+        }
+
+        y = Utils.clamp(y, mBoundTop, mBoundBottom);
+
+        // Horizontal direction: we show the edge effect when the scrolling
+        // tries to go left of the first image or go right of the last image.
+        if (!mHasPrev && x > mBoundRight) {
+            int pixels = x - mBoundRight;
+            mListener.onPull(pixels, EdgeView.LEFT);
+            x = mBoundRight;
+        } else if (!mHasNext && x < mBoundLeft) {
+            int pixels = mBoundLeft - x;
+            mListener.onPull(pixels, EdgeView.RIGHT);
+            x = mBoundLeft;
+        }
+
+        startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
+    }
+
+    public void scrollFilmX(int dx) {
+        if (!canScroll()) return;
+
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
+        // Only allow scrolling when we are not currently in an animation or we
+        // are in some animation with can be interrupted.
+        if (b.mAnimationStartTime != NO_ANIMATION) {
+            switch (b.mAnimationKind) {
+                case ANIM_KIND_SCROLL:
+                case ANIM_KIND_FLING:
+                case ANIM_KIND_FLING_X:
+                    break;
+                default:
+                    return;
+            }
+        }
+
+        int x = p.mCurrentX + dx;
+
+        // Horizontal direction: we show the edge effect when the scrolling
+        // tries to go left of the first image or go right of the last image.
+        x -= mPlatform.mDefaultX;
+        if (!mHasPrev && x > 0) {
+            mListener.onPull(x, EdgeView.LEFT);
+            x = 0;
+        } else if (!mHasNext && x < 0) {
+            mListener.onPull(-x, EdgeView.RIGHT);
+            x = 0;
+        }
+        x += mPlatform.mDefaultX;
+        startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL);
+    }
+
+    public void scrollFilmY(int boxIndex, int dy) {
+        if (!canScroll()) return;
+
+        Box b = mBoxes.get(boxIndex);
+        int y = b.mCurrentY + dy;
+        b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL);
+        redraw();
+    }
+
+    public boolean flingPage(int velocityX, int velocityY) {
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
+        // We only want to do fling when the picture is zoomed-in.
+        if (viewWiderThanScaledImage(b.mCurrentScale) &&
+            viewTallerThanScaledImage(b.mCurrentScale)) {
+            return false;
+        }
+
+        // We only allow flinging in the directions where it won't go over the
+        // picture.
+        int edges = getImageAtEdges();
+        if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
+            (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
+            velocityX = 0;
+        }
+        if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
+            (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
+            velocityY = 0;
+        }
+
+        if (velocityX == 0 && velocityY == 0) return false;
+
+        mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
+                mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
+        int targetX = mPageScroller.getFinalX();
+        int targetY = mPageScroller.getFinalY();
+        ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
+        return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
+    }
+
+    public boolean flingFilmX(int velocityX) {
+        if (velocityX == 0) return false;
+
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
+        // If we are already at the edge, don't start the fling.
+        int defaultX = p.mDefaultX;
+        if ((!mHasPrev && p.mCurrentX >= defaultX)
+                || (!mHasNext && p.mCurrentX <= defaultX)) {
+            return false;
+        }
+
+        mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
+                Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+        int targetX = mFilmScroller.getFinalX();
+        return startAnimation(
+                targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X);
+    }
+
+    // Moves the specified box out of screen. If velocityY is 0, a default
+    // velocity is used. Returns the time for the duration, or -1 if we cannot
+    // not do the animation.
+    public int flingFilmY(int boxIndex, int velocityY) {
+        Box b = mBoxes.get(boxIndex);
+
+        // Calculate targetY
+        int h = heightOf(b);
+        int targetY;
+        int FUZZY = 3;  // TODO: figure out why this is needed.
+        if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) {
+            targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY;
+        } else {
+            targetY = (mViewH + 1) / 2 + h / 2 + FUZZY;
+        }
+
+        // Calculate duration
+        int duration;
+        if (velocityY != 0) {
+            duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f
+                    / Math.abs(velocityY));
+            duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration);
+        } else {
+            duration = DEFAULT_DELETE_ANIMATION_DURATION;
+        }
+
+        // Start animation
+        ANIM_TIME[ANIM_KIND_DELETE] = duration;
+        if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) {
+            redraw();
+            return duration;
+        }
+        return -1;
+    }
+
+    // Returns the index of the box which contains the given point (x, y)
+    // Returns Integer.MAX_VALUE if there is no hit. There may be more than
+    // one box contains the given point, and we want to give priority to the
+    // one closer to the focused index (0).
+    public int hitTest(int x, int y) {
+        for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+            int j = CENTER_OUT_INDEX[i];
+            Rect r = mRects.get(j);
+            if (r.contains(x, y)) {
+                return j;
+            }
+        }
+
+        return Integer.MAX_VALUE;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Redraw
+    //
+    //  If a method changes box positions directly, redraw()
+    //  should be called.
+    //
+    //  If a method may also cause a snapback to happen, snapAndRedraw() should
+    //  be called.
+    //
+    //  If a method starts an animation to change the position of focused box,
+    //  startAnimation() should be called.
+    //
+    //  If time advances to change the box position, advanceAnimation() should
+    //  be called.
+    ////////////////////////////////////////////////////////////////////////////
+    private void redraw() {
+        layoutAndSetPosition();
+        mListener.invalidate();
+    }
+
+    private void snapAndRedraw() {
+        mPlatform.startSnapback();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mBoxes.get(i).startSnapback();
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mGaps.get(i).startSnapback();
+        }
+        mFilmRatio.startSnapback();
+        redraw();
+    }
+
+    private boolean startAnimation(int targetX, int targetY, float targetScale,
+            int kind) {
+        boolean changed = false;
+        changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
+        changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
+        if (changed) redraw();
+        return changed;
+    }
+
+    public void advanceAnimation() {
+        boolean changed = false;
+        changed |= mPlatform.advanceAnimation();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            changed |= mBoxes.get(i).advanceAnimation();
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            changed |= mGaps.get(i).advanceAnimation();
+        }
+        changed |= mFilmRatio.advanceAnimation();
+        if (changed) redraw();
+    }
+
+    public boolean inOpeningAnimation() {
+        return (mPlatform.mAnimationKind == ANIM_KIND_OPENING &&
+                mPlatform.mAnimationStartTime != NO_ANIMATION) ||
+               (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING &&
+                mBoxes.get(0).mAnimationStartTime != NO_ANIMATION);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Layout
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Returns the display width of this box.
+    private int widthOf(Box b) {
+        return (int) (b.mImageW * b.mCurrentScale + 0.5f);
+    }
+
+    // Returns the display height of this box.
+    private int heightOf(Box b) {
+        return (int) (b.mImageH * b.mCurrentScale + 0.5f);
+    }
+
+    // Returns the display width of this box, using the given scale.
+    private int widthOf(Box b, float scale) {
+        return (int) (b.mImageW * scale + 0.5f);
+    }
+
+    // Returns the display height of this box, using the given scale.
+    private int heightOf(Box b, float scale) {
+        return (int) (b.mImageH * scale + 0.5f);
+    }
+
+    // Convert the information in mPlatform and mBoxes to mRects, so the user
+    // can get the position of each box by getPosition().
+    //
+    // Note we go from center-out because each box's X coordinate
+    // is relative to its anchor box (except the focused box).
+    private void layoutAndSetPosition() {
+        for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+            convertBoxToRect(CENTER_OUT_INDEX[i]);
+        }
+        //dumpState();
+    }
+
+    @SuppressWarnings("unused")
+    private void dumpState() {
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
+        }
+
+        for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+            dumpRect(CENTER_OUT_INDEX[i]);
+        }
+
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            for (int j = i + 1; j <= BOX_MAX; j++) {
+                if (Rect.intersects(mRects.get(i), mRects.get(j))) {
+                    Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
+                }
+            }
+        }
+    }
+
+    private void dumpRect(int i) {
+        StringBuilder sb = new StringBuilder();
+        Rect r = mRects.get(i);
+        sb.append("Rect " + i + ":");
+        sb.append("(");
+        sb.append(r.centerX());
+        sb.append(",");
+        sb.append(r.centerY());
+        sb.append(") [");
+        sb.append(r.width());
+        sb.append("x");
+        sb.append(r.height());
+        sb.append("]");
+        Log.d(TAG, sb.toString());
+    }
+
+    private void convertBoxToRect(int i) {
+        Box b = mBoxes.get(i);
+        Rect r = mRects.get(i);
+        int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
+        int w = widthOf(b);
+        int h = heightOf(b);
+        if (i == 0) {
+            int x = mPlatform.mCurrentX + mViewW / 2;
+            r.left = x - w / 2;
+            r.right = r.left + w;
+        } else if (i > 0) {
+            Rect a = mRects.get(i - 1);
+            Gap g = mGaps.get(i - 1);
+            r.left = a.right + g.mCurrentGap;
+            r.right = r.left + w;
+        } else {  // i < 0
+            Rect a = mRects.get(i + 1);
+            Gap g = mGaps.get(i);
+            r.right = a.left - g.mCurrentGap;
+            r.left = r.right - w;
+        }
+        r.top = y - h / 2;
+        r.bottom = r.top + h;
+    }
+
+    // Returns the position of a box.
+    public Rect getPosition(int index) {
+        return mRects.get(index);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Box management
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Initialize the platform to be at the view center.
+    private void initPlatform() {
+        mPlatform.updateDefaultXY();
+        mPlatform.mCurrentX = mPlatform.mDefaultX;
+        mPlatform.mCurrentY = mPlatform.mDefaultY;
+        mPlatform.mAnimationStartTime = NO_ANIMATION;
+    }
+
+    // Initialize a box to have the size of the view.
+    private void initBox(int index) {
+        Box b = mBoxes.get(index);
+        b.mImageW = mViewW;
+        b.mImageH = mViewH;
+        b.mUseViewSize = true;
+        b.mScaleMin = getMinimalScale(b);
+        b.mScaleMax = getMaximalScale(b);
+        b.mCurrentY = 0;
+        b.mCurrentScale = b.mScaleMin;
+        b.mAnimationStartTime = NO_ANIMATION;
+        b.mAnimationKind = ANIM_KIND_NONE;
+    }
+
+    // Initialize a box to a given size.
+    private void initBox(int index, Size size) {
+        if (size.width == 0 || size.height == 0) {
+            initBox(index);
+            return;
+        }
+        Box b = mBoxes.get(index);
+        b.mImageW = size.width;
+        b.mImageH = size.height;
+        b.mUseViewSize = false;
+        b.mScaleMin = getMinimalScale(b);
+        b.mScaleMax = getMaximalScale(b);
+        b.mCurrentY = 0;
+        b.mCurrentScale = b.mScaleMin;
+        b.mAnimationStartTime = NO_ANIMATION;
+        b.mAnimationKind = ANIM_KIND_NONE;
+    }
+
+    // Initialize a gap. This can only be called after the boxes around the gap
+    // has been initialized.
+    private void initGap(int index) {
+        Gap g = mGaps.get(index);
+        g.mDefaultSize = getDefaultGapSize(index);
+        g.mCurrentGap = g.mDefaultSize;
+        g.mAnimationStartTime = NO_ANIMATION;
+    }
+
+    private void initGap(int index, int size) {
+        Gap g = mGaps.get(index);
+        g.mDefaultSize = getDefaultGapSize(index);
+        g.mCurrentGap = size;
+        g.mAnimationStartTime = NO_ANIMATION;
+    }
+
+    @SuppressWarnings("unused")
+    private void debugMoveBox(int fromIndex[]) {
+        StringBuilder s = new StringBuilder("moveBox:");
+        for (int i = 0; i < fromIndex.length; i++) {
+            int j = fromIndex[i];
+            if (j == Integer.MAX_VALUE) {
+                s.append(" N");
+            } else {
+                s.append(" ");
+                s.append(fromIndex[i]);
+            }
+        }
+        Log.d(TAG, s.toString());
+    }
+
+    // Move the boxes: it may indicate focus change, box deleted, box appearing,
+    // box reordered, etc.
+    //
+    // Each element in the fromIndex array indicates where each box was in the
+    // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
+    // means the box is new.
+    //
+    // For example:
+    // N N N N N N N -- all new boxes
+    // -3 -2 -1 0 1 2 3 -- nothing changed
+    // -2 -1 0 1 2 3 N -- focus goes to the next box
+    // N -3 -2 -1 0 1 2 -- focus goes to the previous box
+    // -3 -2 -1 1 2 3 N -- the focused box was deleted.
+    //
+    // hasPrev/hasNext indicates if there are previous/next boxes for the
+    // focused box. constrained indicates whether the focused box should be put
+    // into the constrained frame.
+    public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
+            boolean constrained, Size[] sizes) {
+        //debugMoveBox(fromIndex);
+        mHasPrev = hasPrev;
+        mHasNext = hasNext;
+
+        RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
+
+        // 1. Get the absolute X coordinates for the boxes.
+        layoutAndSetPosition();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            Box b = mBoxes.get(i);
+            Rect r = mRects.get(i);
+            b.mAbsoluteX = r.centerX() - mViewW / 2;
+        }
+
+        // 2. copy boxes and gaps to temporary storage.
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mTempBoxes.put(i, mBoxes.get(i));
+            mBoxes.put(i, null);
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mTempGaps.put(i, mGaps.get(i));
+            mGaps.put(i, null);
+        }
+
+        // 3. move back boxes that are used in the new array.
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            int j = from.get(i);
+            if (j == Integer.MAX_VALUE) continue;
+            mBoxes.put(i, mTempBoxes.get(j));
+            mTempBoxes.put(j, null);
+        }
+
+        // 4. move back gaps if both boxes around it are kept together.
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            int j = from.get(i);
+            if (j == Integer.MAX_VALUE) continue;
+            int k = from.get(i + 1);
+            if (k == Integer.MAX_VALUE) continue;
+            if (j + 1 == k) {
+                mGaps.put(i, mTempGaps.get(j));
+                mTempGaps.put(j, null);
+            }
+        }
+
+        // 5. recycle the boxes that are not used in the new array.
+        int k = -BOX_MAX;
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            if (mBoxes.get(i) != null) continue;
+            while (mTempBoxes.get(k) == null) {
+                k++;
+            }
+            mBoxes.put(i, mTempBoxes.get(k++));
+            initBox(i, sizes[i + BOX_MAX]);
+        }
+
+        // 6. Now give the recycled box a reasonable absolute X position.
+        //
+        // First try to find the first and the last box which the absolute X
+        // position is known.
+        int first, last;
+        for (first = -BOX_MAX; first <= BOX_MAX; first++) {
+            if (from.get(first) != Integer.MAX_VALUE) break;
+        }
+        for (last = BOX_MAX; last >= -BOX_MAX; last--) {
+            if (from.get(last) != Integer.MAX_VALUE) break;
+        }
+        // If there is no box has known X position at all, make the focused one
+        // as known.
+        if (first > BOX_MAX) {
+            mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
+            first = last = 0;
+        }
+        // Now for those boxes between first and last, assign their position to
+        // align to the previous box or the next box with known position. For
+        // the boxes before first or after last, we will use a new default gap
+        // size below.
+
+        // Align to the previous box
+        for (int i = Math.max(0, first + 1); i < last; i++) {
+            if (from.get(i) != Integer.MAX_VALUE) continue;
+            Box a = mBoxes.get(i - 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
+                    + getDefaultGapSize(i);
+            if (mPopFromTop) {
+                b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+            } else {
+                b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+            }
+        }
+
+        // Align to the next box
+        for (int i = Math.min(-1, last - 1); i > first; i--) {
+            if (from.get(i) != Integer.MAX_VALUE) continue;
+            Box a = mBoxes.get(i + 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
+                    - getDefaultGapSize(i);
+            if (mPopFromTop) {
+                b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+            } else {
+                b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+            }
+        }
+
+        // 7. recycle the gaps that are not used in the new array.
+        k = -BOX_MAX;
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            if (mGaps.get(i) != null) continue;
+            while (mTempGaps.get(k) == null) {
+                k++;
+            }
+            mGaps.put(i, mTempGaps.get(k++));
+            Box a = mBoxes.get(i);
+            Box b = mBoxes.get(i + 1);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            if (i >= first && i < last) {
+                int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
+                initGap(i, g);
+            } else {
+                initGap(i);
+            }
+        }
+
+        // 8. calculate the new absolute X coordinates for those box before
+        // first or after last.
+        for (int i = first - 1; i >= -BOX_MAX; i--) {
+            Box a = mBoxes.get(i + 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            Gap g = mGaps.get(i);
+            b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
+        }
+
+        for (int i = last + 1; i <= BOX_MAX; i++) {
+            Box a = mBoxes.get(i - 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            Gap g = mGaps.get(i - 1);
+            b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
+        }
+
+        // 9. offset the Platform position
+        int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
+        mPlatform.mCurrentX += dx;
+        mPlatform.mFromX += dx;
+        mPlatform.mToX += dx;
+        mPlatform.mFlingOffset += dx;
+
+        if (mConstrained != constrained) {
+            mConstrained = constrained;
+            mPlatform.updateDefaultXY();
+            updateScaleAndGapLimit();
+        }
+
+        snapAndRedraw();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Public utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    public boolean isAtMinimalScale() {
+        Box b = mBoxes.get(0);
+        return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
+    }
+
+    public boolean isCenter() {
+        Box b = mBoxes.get(0);
+        return mPlatform.mCurrentX == mPlatform.mDefaultX
+            && b.mCurrentY == 0;
+    }
+
+    public int getImageWidth() {
+        Box b = mBoxes.get(0);
+        return b.mImageW;
+    }
+
+    public int getImageHeight() {
+        Box b = mBoxes.get(0);
+        return b.mImageH;
+    }
+
+    public float getImageScale() {
+        Box b = mBoxes.get(0);
+        return b.mCurrentScale;
+    }
+
+    public int getImageAtEdges() {
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+        calculateStableBound(b.mCurrentScale);
+        int edges = 0;
+        if (p.mCurrentX <= mBoundLeft) {
+            edges |= IMAGE_AT_RIGHT_EDGE;
+        }
+        if (p.mCurrentX >= mBoundRight) {
+            edges |= IMAGE_AT_LEFT_EDGE;
+        }
+        if (b.mCurrentY <= mBoundTop) {
+            edges |= IMAGE_AT_BOTTOM_EDGE;
+        }
+        if (b.mCurrentY >= mBoundBottom) {
+            edges |= IMAGE_AT_TOP_EDGE;
+        }
+        return edges;
+    }
+
+    public boolean isScrolling() {
+        return mPlatform.mAnimationStartTime != NO_ANIMATION
+                && mPlatform.mCurrentX != mPlatform.mToX;
+    }
+
+    public void stopScrolling() {
+        if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
+        if (mFilmMode) mFilmScroller.forceFinished(true);
+        mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
+    }
+
+    public float getFilmRatio() {
+        return mFilmRatio.mCurrentRatio;
+    }
+
+    public void setPopFromTop(boolean top) {
+        mPopFromTop = top;
+    }
+
+    public boolean hasDeletingBox() {
+        for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Private utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    private float getMinimalScale(Box b) {
+        float wFactor = 1.0f;
+        float hFactor = 1.0f;
+        int viewW, viewH;
+
+        if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
+                && b == mBoxes.get(0)) {
+            viewW = mConstrainedFrame.width();
+            viewH = mConstrainedFrame.height();
+        } else {
+            viewW = mViewW;
+            viewH = mViewH;
+        }
+
+        if (mFilmMode) {
+            if (mViewH > mViewW) {  // portrait
+                wFactor = FILM_MODE_PORTRAIT_WIDTH;
+                hFactor = FILM_MODE_PORTRAIT_HEIGHT;
+            } else {  // landscape
+                wFactor = FILM_MODE_LANDSCAPE_WIDTH;
+                hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
+            }
+        }
+
+        float s = Math.min(wFactor * viewW / b.mImageW,
+                hFactor * viewH / b.mImageH);
+        return Math.min(SCALE_LIMIT, s);
+    }
+
+    private float getMaximalScale(Box b) {
+        if (mFilmMode) return getMinimalScale(b);
+        if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
+        return SCALE_LIMIT;
+    }
+
+    private static boolean isAlmostEqual(float a, float b) {
+        float diff = a - b;
+        return (diff < 0 ? -diff : diff) < 0.02f;
+    }
+
+    // Calculates the stable region of mPlatform.mCurrentX and
+    // mBoxes.get(0).mCurrentY, where "stable" means
+    //
+    // (1) If the dimension of scaled image >= view dimension, we will not
+    // see black region outside the image (at that dimension).
+    // (2) If the dimension of scaled image < view dimension, we will center
+    // the scaled image.
+    //
+    // We might temporarily go out of this stable during user interaction,
+    // but will "snap back" after user stops interaction.
+    //
+    // The results are stored in mBound{Left/Right/Top/Bottom}.
+    //
+    // An extra parameter "horizontalSlack" (which has the value of 0 usually)
+    // is used to extend the stable region by some pixels on each side
+    // horizontally.
+    private void calculateStableBound(float scale, int horizontalSlack) {
+        Box b = mBoxes.get(0);
+
+        // The width and height of the box in number of view pixels
+        int w = widthOf(b, scale);
+        int h = heightOf(b, scale);
+
+        // When the edge of the view is aligned with the edge of the box
+        mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
+        mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
+        mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
+        mBoundBottom = h / 2 - mViewH / 2;
+
+        // If the scaled height is smaller than the view height,
+        // force it to be in the center.
+        if (viewTallerThanScaledImage(scale)) {
+            mBoundTop = mBoundBottom = 0;
+        }
+
+        // Same for width
+        if (viewWiderThanScaledImage(scale)) {
+            mBoundLeft = mBoundRight = mPlatform.mDefaultX;
+        }
+    }
+
+    private void calculateStableBound(float scale) {
+        calculateStableBound(scale, 0);
+    }
+
+    private boolean viewTallerThanScaledImage(float scale) {
+        return mViewH >= heightOf(mBoxes.get(0), scale);
+    }
+
+    private boolean viewWiderThanScaledImage(float scale) {
+        return mViewW >= widthOf(mBoxes.get(0), scale);
+    }
+
+    private float getTargetScale(Box b) {
+        return b.mAnimationStartTime == NO_ANIMATION
+                ? b.mCurrentScale : b.mToScale;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Animatable: an thing which can do animation.
+    ////////////////////////////////////////////////////////////////////////////
+    private abstract static class Animatable {
+        public long mAnimationStartTime;
+        public int mAnimationKind;
+        public int mAnimationDuration;
+
+        // This should be overridden in subclass to change the animation values
+        // give the progress value in [0, 1].
+        protected abstract boolean interpolate(float progress);
+        public abstract boolean startSnapback();
+
+        // Returns true if the animation values changes, so things need to be
+        // redrawn.
+        public boolean advanceAnimation() {
+            if (mAnimationStartTime == NO_ANIMATION) {
+                return false;
+            }
+            if (mAnimationStartTime == LAST_ANIMATION) {
+                mAnimationStartTime = NO_ANIMATION;
+                return startSnapback();
+            }
+
+            float progress;
+            if (mAnimationDuration == 0) {
+                progress = 1;
+            } else {
+                long now = AnimationTime.get();
+                progress =
+                    (float) (now - mAnimationStartTime) / mAnimationDuration;
+            }
+
+            if (progress >= 1) {
+                progress = 1;
+            } else {
+                progress = applyInterpolationCurve(mAnimationKind, progress);
+            }
+
+            boolean done = interpolate(progress);
+
+            if (done) {
+                mAnimationStartTime = LAST_ANIMATION;
+            }
+
+            return true;
+        }
+
+        private static float applyInterpolationCurve(int kind, float progress) {
+            float f = 1 - progress;
+            switch (kind) {
+                case ANIM_KIND_SCROLL:
+                case ANIM_KIND_FLING:
+                case ANIM_KIND_FLING_X:
+                case ANIM_KIND_DELETE:
+                case ANIM_KIND_CAPTURE:
+                    progress = 1 - f;  // linear
+                    break;
+                case ANIM_KIND_OPENING:
+                case ANIM_KIND_SCALE:
+                    progress = 1 - f * f;  // quadratic
+                    break;
+                case ANIM_KIND_SNAPBACK:
+                case ANIM_KIND_ZOOM:
+                case ANIM_KIND_SLIDE:
+                    progress = 1 - f * f * f * f * f; // x^5
+                    break;
+            }
+            return progress;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Platform: captures the global X/Y movement.
+    ////////////////////////////////////////////////////////////////////////////
+    private class Platform extends Animatable {
+        public int mCurrentX, mFromX, mToX, mDefaultX;
+        public int mCurrentY, mFromY, mToY, mDefaultY;
+        public int mFlingOffset;
+
+        @Override
+        public boolean startSnapback() {
+            if (mAnimationStartTime != NO_ANIMATION) return false;
+            if (mAnimationKind == ANIM_KIND_SCROLL
+                    && mListener.isHoldingDown()) return false;
+            if (mInScale) return false;
+
+            Box b = mBoxes.get(0);
+            float scaleMin = mExtraScalingRange ?
+                b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
+            float scaleMax = mExtraScalingRange ?
+                b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
+            float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
+            int x = mCurrentX;
+            int y = mDefaultY;
+            if (mFilmMode) {
+                x = mDefaultX;
+            } else {
+                calculateStableBound(scale, HORIZONTAL_SLACK);
+                // If the picture is zoomed-in, we want to keep the focus point
+                // stay in the same position on screen, so we need to adjust
+                // target mCurrentX (which is the center of the focused
+                // box). The position of the focus point on screen (relative the
+                // the center of the view) is:
+                //
+                // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
+                // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
+                //
+                if (!viewWiderThanScaledImage(scale)) {
+                    float scaleDiff = b.mCurrentScale - scale;
+                    x += (int) (mFocusX * scaleDiff + 0.5f);
+                }
+                x = Utils.clamp(x, mBoundLeft, mBoundRight);
+            }
+            if (mCurrentX != x || mCurrentY != y) {
+                return doAnimation(x, y, ANIM_KIND_SNAPBACK);
+            }
+            return false;
+        }
+
+        // The updateDefaultXY() should be called whenever these variables
+        // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
+        // mFilmMode
+        public void updateDefaultXY() {
+            // We don't check mFilmMode and return 0 for mDefaultX. Because
+            // otherwise if we decide to leave film mode because we are
+            // centered, we will immediately back into film mode because we find
+            // we are not centered.
+            if (mConstrained && !mConstrainedFrame.isEmpty()) {
+                mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
+                mDefaultY = mFilmMode ? 0 :
+                        mConstrainedFrame.centerY() - mViewH / 2;
+            } else {
+                mDefaultX = 0;
+                mDefaultY = 0;
+            }
+        }
+
+        // Starts an animation for the platform.
+        private boolean doAnimation(int targetX, int targetY, int kind) {
+            if (mCurrentX == targetX && mCurrentY == targetY) return false;
+            mAnimationKind = kind;
+            mFromX = mCurrentX;
+            mFromY = mCurrentY;
+            mToX = targetX;
+            mToY = targetY;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[kind];
+            mFlingOffset = 0;
+            advanceAnimation();
+            return true;
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (mAnimationKind == ANIM_KIND_FLING) {
+                return interpolateFlingPage(progress);
+            } else if (mAnimationKind == ANIM_KIND_FLING_X) {
+                return interpolateFlingFilm(progress);
+            } else {
+                return interpolateLinear(progress);
+            }
+        }
+
+        private boolean interpolateFlingFilm(float progress) {
+            mFilmScroller.computeScrollOffset();
+            mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
+
+            int dir = EdgeView.INVALID_DIRECTION;
+            if (mCurrentX < mDefaultX) {
+                if (!mHasNext) {
+                    dir = EdgeView.RIGHT;
+                }
+            } else if (mCurrentX > mDefaultX) {
+                if (!mHasPrev) {
+                    dir = EdgeView.LEFT;
+                }
+            }
+            if (dir != EdgeView.INVALID_DIRECTION) {
+                // TODO: restore this onAbsorb call
+                //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
+                //mListener.onAbsorb(v, dir);
+                mFilmScroller.forceFinished(true);
+                mCurrentX = mDefaultX;
+            }
+            return mFilmScroller.isFinished();
+        }
+
+        private boolean interpolateFlingPage(float progress) {
+            mPageScroller.computeScrollOffset(progress);
+            Box b = mBoxes.get(0);
+            calculateStableBound(b.mCurrentScale);
+
+            int oldX = mCurrentX;
+            mCurrentX = mPageScroller.getCurrX();
+
+            // Check if we hit the edges; show edge effects if we do.
+            if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
+                int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.RIGHT);
+            } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
+                int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.LEFT);
+            }
+
+            return progress >= 1;
+        }
+
+        private boolean interpolateLinear(float progress) {
+            // Other animations
+            if (progress >= 1) {
+                mCurrentX = mToX;
+                mCurrentY = mToY;
+                return true;
+            } else {
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    progress = CaptureAnimation.calculateSlide(progress);
+                }
+                mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+                mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    return false;
+                } else {
+                    return (mCurrentX == mToX && mCurrentY == mToY);
+                }
+            }
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Box: represents a rectangular area which shows a picture.
+    ////////////////////////////////////////////////////////////////////////////
+    private class Box extends Animatable {
+        // Size of the bitmap
+        public int mImageW, mImageH;
+
+        // This is true if we assume the image size is the same as view size
+        // until we know the actual size of image. This is also used to
+        // determine if there is an image ready to show.
+        public boolean mUseViewSize;
+
+        // The minimum and maximum scale we allow for this box.
+        public float mScaleMin, mScaleMax;
+
+        // The X/Y value indicates where the center of the box is on the view
+        // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
+        // actual values used currently. Note that the X values are implicitly
+        // defined by Platform and Gaps.
+        public int mCurrentY, mFromY, mToY;
+        public float mCurrentScale, mFromScale, mToScale;
+
+        // The absolute X coordinate of the center of the box. This is only used
+        // during moveBox().
+        public int mAbsoluteX;
+
+        @Override
+        public boolean startSnapback() {
+            if (mAnimationStartTime != NO_ANIMATION) return false;
+            if (mAnimationKind == ANIM_KIND_SCROLL
+                    && mListener.isHoldingDown()) return false;
+            if (mAnimationKind == ANIM_KIND_DELETE
+                    && mListener.isHoldingDelete()) return false;
+            if (mInScale && this == mBoxes.get(0)) return false;
+
+            int y = mCurrentY;
+            float scale;
+
+            if (this == mBoxes.get(0)) {
+                float scaleMin = mExtraScalingRange ?
+                    mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
+                float scaleMax = mExtraScalingRange ?
+                    mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
+                scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
+                if (mFilmMode) {
+                    y = 0;
+                } else {
+                    calculateStableBound(scale, HORIZONTAL_SLACK);
+                    // If the picture is zoomed-in, we want to keep the focus
+                    // point stay in the same position on screen. See the
+                    // comment in Platform.startSnapback for details.
+                    if (!viewTallerThanScaledImage(scale)) {
+                        float scaleDiff = mCurrentScale - scale;
+                        y += (int) (mFocusY * scaleDiff + 0.5f);
+                    }
+                    y = Utils.clamp(y, mBoundTop, mBoundBottom);
+                }
+            } else {
+                y = 0;
+                scale = mScaleMin;
+            }
+
+            if (mCurrentY != y || mCurrentScale != scale) {
+                return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
+            }
+            return false;
+        }
+
+        private boolean doAnimation(int targetY, float targetScale, int kind) {
+            targetScale = clampScale(targetScale);
+
+            if (mCurrentY == targetY && mCurrentScale == targetScale
+                    && kind != ANIM_KIND_CAPTURE) {
+                return false;
+            }
+
+            // Now starts an animation for the box.
+            mAnimationKind = kind;
+            mFromY = mCurrentY;
+            mFromScale = mCurrentScale;
+            mToY = targetY;
+            mToScale = targetScale;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[kind];
+            advanceAnimation();
+            return true;
+        }
+
+        // Clamps the input scale to the range that doAnimation() can reach.
+        public float clampScale(float s) {
+            return Utils.clamp(s,
+                    SCALE_MIN_EXTRA * mScaleMin,
+                    SCALE_MAX_EXTRA * mScaleMax);
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (mAnimationKind == ANIM_KIND_FLING) {
+                return interpolateFlingPage(progress);
+            } else {
+                return interpolateLinear(progress);
+            }
+        }
+
+        private boolean interpolateFlingPage(float progress) {
+            mPageScroller.computeScrollOffset(progress);
+            calculateStableBound(mCurrentScale);
+
+            int oldY = mCurrentY;
+            mCurrentY = mPageScroller.getCurrY();
+
+            // Check if we hit the edges; show edge effects if we do.
+            if (oldY > mBoundTop && mCurrentY == mBoundTop) {
+                int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.BOTTOM);
+            } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
+                int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.TOP);
+            }
+
+            return progress >= 1;
+        }
+
+        private boolean interpolateLinear(float progress) {
+            if (progress >= 1) {
+                mCurrentY = mToY;
+                mCurrentScale = mToScale;
+                return true;
+            } else {
+                mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+                mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    float f = CaptureAnimation.calculateScale(progress);
+                    mCurrentScale *= f;
+                    return false;
+                } else {
+                    return (mCurrentY == mToY && mCurrentScale == mToScale);
+                }
+            }
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Gap: represents a rectangular area which is between two boxes.
+    ////////////////////////////////////////////////////////////////////////////
+    private class Gap extends Animatable {
+        // The default gap size between two boxes. The value may vary for
+        // different image size of the boxes and for different modes (page or
+        // film).
+        public int mDefaultSize;
+
+        // The gap size between the two boxes.
+        public int mCurrentGap, mFromGap, mToGap;
+
+        @Override
+        public boolean startSnapback() {
+            if (mAnimationStartTime != NO_ANIMATION) return false;
+            return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
+        }
+
+        // Starts an animation for a gap.
+        public boolean doAnimation(int targetSize, int kind) {
+            if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
+                return false;
+            }
+            mAnimationKind = kind;
+            mFromGap = mCurrentGap;
+            mToGap = targetSize;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[mAnimationKind];
+            advanceAnimation();
+            return true;
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (progress >= 1) {
+                mCurrentGap = mToGap;
+                return true;
+            } else {
+                mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    float f = CaptureAnimation.calculateScale(progress);
+                    mCurrentGap = (int) (mCurrentGap * f);
+                    return false;
+                } else {
+                    return (mCurrentGap == mToGap);
+                }
+            }
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  FilmRatio: represents the progress of film mode change.
+    ////////////////////////////////////////////////////////////////////////////
+    private class FilmRatio extends Animatable {
+        // The film ratio: 1 means switching to film mode is complete, 0 means
+        // switching to page mode is complete.
+        public float mCurrentRatio, mFromRatio, mToRatio;
+
+        @Override
+        public boolean startSnapback() {
+            float target = mFilmMode ? 1f : 0f;
+            if (target == mToRatio) return false;
+            return doAnimation(target, ANIM_KIND_SNAPBACK);
+        }
+
+        // Starts an animation for the film ratio.
+        private boolean doAnimation(float targetRatio, int kind) {
+            mAnimationKind = kind;
+            mFromRatio = mCurrentRatio;
+            mToRatio = targetRatio;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[mAnimationKind];
+            advanceAnimation();
+            return true;
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (progress >= 1) {
+                mCurrentRatio = mToRatio;
+                return true;
+            } else {
+                mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
+                return (mCurrentRatio == mToRatio);
+            }
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
new file mode 100644
index 0000000..ce672f2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
@@ -0,0 +1,85 @@
+package com.android.gallery3d.ui;
+
+import android.os.ConditionVariable;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+public class PreparePageFadeoutTexture implements OnGLIdleListener {
+    private static final long TIMEOUT = 200;
+    public static final String KEY_FADE_TEXTURE = "fade_texture";
+
+    private RawTexture mTexture;
+    private ConditionVariable mResultReady = new ConditionVariable(false);
+    private boolean mCancelled = false;
+    private GLView mRootPane;
+
+    public PreparePageFadeoutTexture(GLView rootPane) {
+        if (rootPane == null) {
+            mCancelled = true;
+            return;
+        }
+        int w = rootPane.getWidth();
+        int h = rootPane.getHeight();
+        if (w == 0 || h == 0) {
+            mCancelled = true;
+            return;
+        }
+        mTexture = new RawTexture(w, h, true);
+        mRootPane =  rootPane;
+    }
+
+    public boolean isCancelled() {
+        return mCancelled;
+    }
+
+    public synchronized RawTexture get() {
+        if (mCancelled) {
+            return null;
+        } else if (mResultReady.block(TIMEOUT)) {
+            return mTexture;
+        } else {
+            mCancelled = true;
+            return null;
+        }
+    }
+
+    @Override
+    public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+        if (!mCancelled) {
+            try {
+                canvas.beginRenderTarget(mTexture);
+                mRootPane.render(canvas);
+                canvas.endRenderTarget();
+            } catch (RuntimeException e) {
+                mTexture = null;
+            }
+        } else {
+            mTexture = null;
+        }
+        mResultReady.open();
+        return false;
+    }
+
+    public static void prepareFadeOutTexture(AbstractGalleryActivity activity,
+            GLView rootPane) {
+        PreparePageFadeoutTexture task = new PreparePageFadeoutTexture(rootPane);
+        if (task.isCancelled()) return;
+        GLRoot root = activity.getGLRoot();
+        RawTexture texture = null;
+        root.unlockRenderThread();
+        try {
+            root.addOnGLIdleListener(task);
+            texture = task.get();
+        } finally {
+            root.lockRenderThread();
+        }
+
+        if (texture == null) {
+            return;
+        }
+        activity.getTransitionStore().put(KEY_FADE_TEXTURE, texture);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java
new file mode 100644
index 0000000..1b31af2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ProgressSpinner.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+
+public class ProgressSpinner {
+    private static float ROTATE_SPEED_OUTER = 1080f / 3500f;
+    private static float ROTATE_SPEED_INNER = -720f / 3500f;
+    private final ResourceTexture mOuter;
+    private final ResourceTexture mInner;
+    private final int mWidth;
+    private final int mHeight;
+
+    private float mInnerDegree = 0f;
+    private float mOuterDegree = 0f;
+    private long mAnimationTimestamp = -1;
+
+    public ProgressSpinner(Context context) {
+        mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo);
+        mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo);
+
+        mWidth = Math.max(mOuter.getWidth(), mInner.getWidth());
+        mHeight = Math.max(mOuter.getHeight(), mInner.getHeight());
+    }
+
+    public int getWidth() {
+        return mWidth;
+    }
+
+    public int getHeight() {
+        return mHeight;
+    }
+
+    public void startAnimation() {
+        mAnimationTimestamp = -1;
+        mOuterDegree = 0;
+        mInnerDegree = 0;
+    }
+
+    public void draw(GLCanvas canvas, int x, int y) {
+        long now = AnimationTime.get();
+        if (mAnimationTimestamp == -1) mAnimationTimestamp = now;
+        mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER;
+        mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER;
+
+        mAnimationTimestamp = now;
+
+        // just preventing overflow
+        if (mOuterDegree > 360) mOuterDegree -= 360f;
+        if (mInnerDegree < 0) mInnerDegree += 360f;
+
+        canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+        canvas.translate(x + mWidth / 2, y + mHeight / 2);
+        canvas.rotate(mInnerDegree, 0, 0, 1);
+        mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2);
+        canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1);
+        mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2);
+        canvas.restore();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/RelativePosition.java b/src/com/android/gallery3d/ui/RelativePosition.java
new file mode 100644
index 0000000..0f2bfd8
--- /dev/null
+++ b/src/com/android/gallery3d/ui/RelativePosition.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+public class RelativePosition {
+    private float mAbsoluteX;
+    private float mAbsoluteY;
+    private float mReferenceX;
+    private float mReferenceY;
+
+    public void setAbsolutePosition(int absoluteX, int absoluteY) {
+        mAbsoluteX = absoluteX;
+        mAbsoluteY = absoluteY;
+    }
+
+    public void setReferencePosition(int x, int y) {
+        mReferenceX = x;
+        mReferenceY = y;
+    }
+
+    public float getX() {
+        return mAbsoluteX - mReferenceX;
+    }
+
+    public float getY() {
+        return mAbsoluteY - mReferenceY;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java
new file mode 100644
index 0000000..965bf0b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScreenNail.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.graphics.RectF;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public interface ScreenNail {
+    public int getWidth();
+    public int getHeight();
+    public void draw(GLCanvas canvas, int x, int y, int width, int height);
+
+    // We do not need to draw this ScreenNail in this frame.
+    public void noDraw();
+
+    // This ScreenNail will not be used anymore. Release related resources.
+    public void recycle();
+
+    // This is only used by TileImageView to back up the tiles not yet loaded.
+    public void draw(GLCanvas canvas, RectF source, RectF dest);
+}
diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java
new file mode 100644
index 0000000..34fbcef
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollBarView.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.TypedValue;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+
+public class ScrollBarView extends GLView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ScrollBarView";
+
+    private int mBarHeight;
+
+    private int mGripHeight;
+    private int mGripPosition;  // left side of the grip
+    private int mGripWidth;     // zero if the grip is disabled
+    private int mGivenGripWidth;
+
+    private int mContentPosition;
+    private int mContentTotal;
+
+    private NinePatchTexture mScrollBarTexture;
+
+    public ScrollBarView(Context context, int gripHeight, int gripWidth) {
+        TypedValue outValue = new TypedValue();
+        context.getTheme().resolveAttribute(
+                android.R.attr.scrollbarThumbHorizontal, outValue, true);
+        mScrollBarTexture = new NinePatchTexture(
+                context, outValue.resourceId);
+        mGripPosition = 0;
+        mGripWidth = 0;
+        mGivenGripWidth = gripWidth;
+        mGripHeight = gripHeight;
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changed, int left, int top, int right, int bottom) {
+        if (!changed) return;
+        mBarHeight = bottom - top;
+    }
+
+    // The content position is between 0 to "total". The current position is
+    // in "position".
+    public void setContentPosition(int position, int total) {
+        if (position == mContentPosition && total == mContentTotal) {
+            return;
+        }
+
+        invalidate();
+
+        mContentPosition = position;
+        mContentTotal = total;
+
+        // If the grip cannot move, don't draw it.
+        if (mContentTotal <= 0) {
+            mGripPosition = 0;
+            mGripWidth = 0;
+            return;
+        }
+
+        // Map from the content range to scroll bar range.
+        //
+        // mContentTotal --> getWidth() - mGripWidth
+        // mContentPosition --> mGripPosition
+        mGripWidth = mGivenGripWidth;
+        float r = (getWidth() - mGripWidth) / (float) mContentTotal;
+        mGripPosition = Math.round(r * mContentPosition);
+    }
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        super.render(canvas);
+        if (mGripWidth == 0) return;
+        Rect b = bounds();
+        int y = (mBarHeight - mGripHeight) / 2;
+        mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
new file mode 100644
index 0000000..aa68d19
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.view.ViewConfiguration;
+
+import com.android.gallery3d.common.OverScroller;
+import com.android.gallery3d.common.Utils;
+
+public class ScrollerHelper {
+    private OverScroller mScroller;
+    private int mOverflingDistance;
+    private boolean mOverflingEnabled;
+
+    public ScrollerHelper(Context context) {
+        mScroller = new OverScroller(context);
+        ViewConfiguration configuration = ViewConfiguration.get(context);
+        mOverflingDistance = configuration.getScaledOverflingDistance();
+    }
+
+    public void setOverfling(boolean enabled) {
+        mOverflingEnabled = enabled;
+    }
+
+    /**
+     * Call this when you want to know the new location. The position will be
+     * updated and can be obtained by getPosition(). Returns true if  the
+     * animation is not yet finished.
+     */
+    public boolean advanceAnimation(long currentTimeMillis) {
+        return mScroller.computeScrollOffset();
+    }
+
+    public boolean isFinished() {
+        return mScroller.isFinished();
+    }
+
+    public void forceFinished() {
+        mScroller.forceFinished(true);
+    }
+
+    public int getPosition() {
+        return mScroller.getCurrX();
+    }
+
+    public float getCurrVelocity() {
+        return mScroller.getCurrVelocity();
+    }
+
+    public void setPosition(int position) {
+        mScroller.startScroll(
+                position, 0,    // startX, startY
+                0, 0, 0);       // dx, dy, duration
+
+        // This forces the scroller to reach the final position.
+        mScroller.abortAnimation();
+    }
+
+    public void fling(int velocity, int min, int max) {
+        int currX = getPosition();
+        mScroller.fling(
+                currX, 0,      // startX, startY
+                velocity, 0,   // velocityX, velocityY
+                min, max,      // minX, maxX
+                0, 0,          // minY, maxY
+                mOverflingEnabled ? mOverflingDistance : 0, 0);
+    }
+
+    // Returns the distance that over the scroll limit.
+    public int startScroll(int distance, int min, int max) {
+        int currPosition = mScroller.getCurrX();
+        int finalPosition = mScroller.isFinished() ? currPosition :
+                mScroller.getFinalX();
+        int newPosition = Utils.clamp(finalPosition + distance, min, max);
+        if (newPosition != currPosition) {
+            mScroller.startScroll(
+                currPosition, 0,                    // startX, startY
+                newPosition - currPosition, 0, 0);  // dx, dy, duration
+        }
+        return finalPosition + distance - newPosition;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java
new file mode 100644
index 0000000..be6811b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionManager.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SelectionManager {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SelectionManager";
+
+    public static final int ENTER_SELECTION_MODE = 1;
+    public static final int LEAVE_SELECTION_MODE = 2;
+    public static final int SELECT_ALL_MODE = 3;
+
+    private Set<Path> mClickedSet;
+    private MediaSet mSourceMediaSet;
+    private SelectionListener mListener;
+    private DataManager mDataManager;
+    private boolean mInverseSelection;
+    private boolean mIsAlbumSet;
+    private boolean mInSelectionMode;
+    private boolean mAutoLeave = true;
+    private int mTotal;
+
+    public interface SelectionListener {
+        public void onSelectionModeChange(int mode);
+        public void onSelectionChange(Path path, boolean selected);
+    }
+
+    public SelectionManager(AbstractGalleryActivity activity, boolean isAlbumSet) {
+        mDataManager = activity.getDataManager();
+        mClickedSet = new HashSet<Path>();
+        mIsAlbumSet = isAlbumSet;
+        mTotal = -1;
+    }
+
+    // Whether we will leave selection mode automatically once the number of
+    // selected items is down to zero.
+    public void setAutoLeaveSelectionMode(boolean enable) {
+        mAutoLeave = enable;
+    }
+
+    public void setSelectionListener(SelectionListener listener) {
+        mListener = listener;
+    }
+
+    public void selectAll() {
+        mInverseSelection = true;
+        mClickedSet.clear();
+        enterSelectionMode();
+        if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE);
+    }
+
+    public void deSelectAll() {
+        leaveSelectionMode();
+        mInverseSelection = false;
+        mClickedSet.clear();
+    }
+
+    public boolean inSelectAllMode() {
+        return mInverseSelection;
+    }
+
+    public boolean inSelectionMode() {
+        return mInSelectionMode;
+    }
+
+    public void enterSelectionMode() {
+        if (mInSelectionMode) return;
+
+        mInSelectionMode = true;
+        if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE);
+    }
+
+    public void leaveSelectionMode() {
+        if (!mInSelectionMode) return;
+
+        mInSelectionMode = false;
+        mInverseSelection = false;
+        mClickedSet.clear();
+        if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE);
+    }
+
+    public boolean isItemSelected(Path itemId) {
+        return mInverseSelection ^ mClickedSet.contains(itemId);
+    }
+
+    private int getTotalCount() {
+        if (mSourceMediaSet == null) return -1;
+
+        if (mTotal < 0) {
+            mTotal = mIsAlbumSet
+                    ? mSourceMediaSet.getSubMediaSetCount()
+                    : mSourceMediaSet.getMediaItemCount();
+        }
+        return mTotal;
+    }
+
+    public int getSelectedCount() {
+        int count = mClickedSet.size();
+        if (mInverseSelection) {
+            count = getTotalCount() - count;
+        }
+        return count;
+    }
+
+    public void toggle(Path path) {
+        if (mClickedSet.contains(path)) {
+            mClickedSet.remove(path);
+        } else {
+            enterSelectionMode();
+            mClickedSet.add(path);
+        }
+
+        // Convert to inverse selection mode if everything is selected.
+        int count = getSelectedCount();
+        if (count == getTotalCount()) {
+            selectAll();
+        }
+
+        if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path));
+        if (count == 0 && mAutoLeave) {
+            leaveSelectionMode();
+        }
+    }
+
+    private static boolean expandMediaSet(ArrayList<Path> items, MediaSet set, int maxSelection) {
+        int subCount = set.getSubMediaSetCount();
+        for (int i = 0; i < subCount; i++) {
+            if (!expandMediaSet(items, set.getSubMediaSet(i), maxSelection)) {
+                return false;
+            }
+        }
+        int total = set.getMediaItemCount();
+        int batch = 50;
+        int index = 0;
+
+        while (index < total) {
+            int count = index + batch < total
+                    ? batch
+                    : total - index;
+            ArrayList<MediaItem> list = set.getMediaItem(index, count);
+            if (list != null
+                    && list.size() > (maxSelection - items.size())) {
+                return false;
+            }
+            for (MediaItem item : list) {
+                items.add(item.getPath());
+            }
+            index += batch;
+        }
+        return true;
+    }
+
+    public ArrayList<Path> getSelected(boolean expandSet) {
+        return getSelected(expandSet, Integer.MAX_VALUE);
+    }
+
+    public ArrayList<Path> getSelected(boolean expandSet, int maxSelection) {
+        ArrayList<Path> selected = new ArrayList<Path>();
+        if (mIsAlbumSet) {
+            if (mInverseSelection) {
+                int total = getTotalCount();
+                for (int i = 0; i < total; i++) {
+                    MediaSet set = mSourceMediaSet.getSubMediaSet(i);
+                    Path id = set.getPath();
+                    if (!mClickedSet.contains(id)) {
+                        if (expandSet) {
+                            if (!expandMediaSet(selected, set, maxSelection)) {
+                                return null;
+                            }
+                        } else {
+                            selected.add(id);
+                            if (selected.size() > maxSelection) {
+                                return null;
+                            }
+                        }
+                    }
+                }
+            } else {
+                for (Path id : mClickedSet) {
+                    if (expandSet) {
+                        if (!expandMediaSet(selected, mDataManager.getMediaSet(id),
+                                maxSelection)) {
+                            return null;
+                        }
+                    } else {
+                        selected.add(id);
+                        if (selected.size() > maxSelection) {
+                            return null;
+                        }
+                    }
+                }
+            }
+        } else {
+            if (mInverseSelection) {
+                int total = getTotalCount();
+                int index = 0;
+                while (index < total) {
+                    int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT);
+                    ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count);
+                    for (MediaItem item : list) {
+                        Path id = item.getPath();
+                        if (!mClickedSet.contains(id)) {
+                            selected.add(id);
+                            if (selected.size() > maxSelection) {
+                                return null;
+                            }
+                        }
+                    }
+                    index += count;
+                }
+            } else {
+                for (Path id : mClickedSet) {
+                    selected.add(id);
+                    if (selected.size() > maxSelection) {
+                        return null;
+                    }
+                }
+            }
+        }
+        return selected;
+    }
+
+    public void setSourceMediaSet(MediaSet set) {
+        mSourceMediaSet = set;
+        mTotal = -1;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionMenu.java b/src/com/android/gallery3d/ui/SelectionMenu.java
new file mode 100644
index 0000000..5b08283
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionMenu.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.PopupList.OnPopupItemClickListener;
+
+public class SelectionMenu implements OnClickListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SelectionMenu";
+
+    private final Context mContext;
+    private final Button mButton;
+    private final PopupList mPopupList;
+
+    public SelectionMenu(Context context, Button button, OnPopupItemClickListener listener) {
+        mContext = context;
+        mButton = button;
+        mPopupList = new PopupList(context, mButton);
+        mPopupList.addItem(R.id.action_select_all,
+                context.getString(R.string.select_all));
+        mPopupList.setOnPopupItemClickListener(listener);
+        mButton.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View v) {
+        mPopupList.show();
+    }
+
+    public void updateSelectAllMode(boolean inSelectAllMode) {
+        PopupList.Item item = mPopupList.findItem(R.id.action_select_all);
+        if (item != null) {
+            item.setTitle(mContext.getString(
+                    inSelectAllMode ? R.string.deselect_all : R.string.select_all));
+        }
+    }
+
+    public void setTitle(CharSequence title) {
+        mButton.setText(title);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java
new file mode 100644
index 0000000..4378423
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlideshowView.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.FloatAnimation;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+import java.util.Random;
+
+public class SlideshowView extends GLView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SlideshowView";
+
+    private static final int SLIDESHOW_DURATION = 3500;
+    private static final int TRANSITION_DURATION = 1000;
+
+    private static final float SCALE_SPEED = 0.20f ;
+    private static final float MOVE_SPEED = SCALE_SPEED;
+
+    private int mCurrentRotation;
+    private BitmapTexture mCurrentTexture;
+    private SlideshowAnimation mCurrentAnimation;
+
+    private int mPrevRotation;
+    private BitmapTexture mPrevTexture;
+    private SlideshowAnimation mPrevAnimation;
+
+    private final FloatAnimation mTransitionAnimation =
+            new FloatAnimation(0, 1, TRANSITION_DURATION);
+
+    private Random mRandom = new Random();
+
+    public void next(Bitmap bitmap, int rotation) {
+
+        mTransitionAnimation.start();
+
+        if (mPrevTexture != null) {
+            mPrevTexture.getBitmap().recycle();
+            mPrevTexture.recycle();
+        }
+
+        mPrevTexture = mCurrentTexture;
+        mPrevAnimation = mCurrentAnimation;
+        mPrevRotation = mCurrentRotation;
+
+        mCurrentRotation = rotation;
+        mCurrentTexture = new BitmapTexture(bitmap);
+        if (((rotation / 90) & 0x01) == 0) {
+            mCurrentAnimation = new SlideshowAnimation(
+                    mCurrentTexture.getWidth(), mCurrentTexture.getHeight(),
+                    mRandom);
+        } else {
+            mCurrentAnimation = new SlideshowAnimation(
+                    mCurrentTexture.getHeight(), mCurrentTexture.getWidth(),
+                    mRandom);
+        }
+        mCurrentAnimation.start();
+
+        invalidate();
+    }
+
+    public void release() {
+        if (mPrevTexture != null) {
+            mPrevTexture.recycle();
+            mPrevTexture = null;
+        }
+        if (mCurrentTexture != null) {
+            mCurrentTexture.recycle();
+            mCurrentTexture = null;
+        }
+    }
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        long animTime = AnimationTime.get();
+        boolean requestRender = mTransitionAnimation.calculate(animTime);
+        float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get();
+
+        if (mPrevTexture != null && alpha != 1f) {
+            requestRender |= mPrevAnimation.calculate(animTime);
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+            canvas.setAlpha(1f - alpha);
+            mPrevAnimation.apply(canvas);
+            canvas.rotate(mPrevRotation, 0, 0, 1);
+            mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2,
+                    -mPrevTexture.getHeight() / 2);
+            canvas.restore();
+        }
+        if (mCurrentTexture != null) {
+            requestRender |= mCurrentAnimation.calculate(animTime);
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+            canvas.setAlpha(alpha);
+            mCurrentAnimation.apply(canvas);
+            canvas.rotate(mCurrentRotation, 0, 0, 1);
+            mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2,
+                    -mCurrentTexture.getHeight() / 2);
+            canvas.restore();
+        }
+        if (requestRender) invalidate();
+    }
+
+    private class SlideshowAnimation extends CanvasAnimation {
+        private final int mWidth;
+        private final int mHeight;
+
+        private final PointF mMovingVector;
+        private float mProgress;
+
+        public SlideshowAnimation(int width, int height, Random random) {
+            mWidth = width;
+            mHeight = height;
+            mMovingVector = new PointF(
+                    MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f),
+                    MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f));
+            setDuration(SLIDESHOW_DURATION);
+        }
+
+        @Override
+        public void apply(GLCanvas canvas) {
+            int viewWidth = getWidth();
+            int viewHeight = getHeight();
+
+            float initScale = Math.min((float)
+                    viewWidth / mWidth, (float) viewHeight / mHeight);
+            float scale = initScale * (1 + SCALE_SPEED * mProgress);
+
+            float centerX = viewWidth / 2 + mMovingVector.x * mProgress;
+            float centerY = viewHeight / 2 + mMovingVector.y * mProgress;
+
+            canvas.translate(centerX, centerY);
+            canvas.scale(scale, scale, 0);
+        }
+
+        @Override
+        public int getCanvasSaveFlags() {
+            return GLCanvas.SAVE_FLAG_MATRIX;
+        }
+
+        @Override
+        protected void onCalculate(float progress) {
+            mProgress = progress;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
new file mode 100644
index 0000000..bd0ffdc
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -0,0 +1,788 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.os.Handler;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public class SlotView extends GLView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SlotView";
+
+    private static final boolean WIDE = true;
+    private static final int INDEX_NONE = -1;
+
+    public static final int RENDER_MORE_PASS = 1;
+    public static final int RENDER_MORE_FRAME = 2;
+
+    public interface Listener {
+        public void onDown(int index);
+        public void onUp(boolean followedByLongPress);
+        public void onSingleTapUp(int index);
+        public void onLongTap(int index);
+        public void onScrollPositionChanged(int position, int total);
+    }
+
+    public static class SimpleListener implements Listener {
+        @Override public void onDown(int index) {}
+        @Override public void onUp(boolean followedByLongPress) {}
+        @Override public void onSingleTapUp(int index) {}
+        @Override public void onLongTap(int index) {}
+        @Override public void onScrollPositionChanged(int position, int total) {}
+    }
+
+    public static interface SlotRenderer {
+        public void prepareDrawing();
+        public void onVisibleRangeChanged(int visibleStart, int visibleEnd);
+        public void onSlotSizeChanged(int width, int height);
+        public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height);
+    }
+
+    private final GestureDetector mGestureDetector;
+    private final ScrollerHelper mScroller;
+    private final Paper mPaper = new Paper();
+
+    private Listener mListener;
+    private UserInteractionListener mUIListener;
+
+    private boolean mMoreAnimation = false;
+    private SlotAnimation mAnimation = null;
+    private final Layout mLayout = new Layout();
+    private int mStartIndex = INDEX_NONE;
+
+    // whether the down action happened while the view is scrolling.
+    private boolean mDownInScrolling;
+    private int mOverscrollEffect = OVERSCROLL_3D;
+    private final Handler mHandler;
+
+    private SlotRenderer mRenderer;
+
+    private int[] mRequestRenderSlots = new int[16];
+
+    public static final int OVERSCROLL_3D = 0;
+    public static final int OVERSCROLL_SYSTEM = 1;
+    public static final int OVERSCROLL_NONE = 2;
+
+    // to prevent allocating memory
+    private final Rect mTempRect = new Rect();
+
+    public SlotView(AbstractGalleryActivity activity, Spec spec) {
+        mGestureDetector = new GestureDetector(activity, new MyGestureListener());
+        mScroller = new ScrollerHelper(activity);
+        mHandler = new SynchronizedHandler(activity.getGLRoot());
+        setSlotSpec(spec);
+    }
+
+    public void setSlotRenderer(SlotRenderer slotDrawer) {
+        mRenderer = slotDrawer;
+        if (mRenderer != null) {
+            mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight);
+            mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd());
+        }
+    }
+
+    public void setCenterIndex(int index) {
+        int slotCount = mLayout.mSlotCount;
+        if (index < 0 || index >= slotCount) {
+            return;
+        }
+        Rect rect = mLayout.getSlotRect(index, mTempRect);
+        int position = WIDE
+                ? (rect.left + rect.right - getWidth()) / 2
+                : (rect.top + rect.bottom - getHeight()) / 2;
+        setScrollPosition(position);
+    }
+
+    public void makeSlotVisible(int index) {
+        Rect rect = mLayout.getSlotRect(index, mTempRect);
+        int visibleBegin = WIDE ? mScrollX : mScrollY;
+        int visibleLength = WIDE ? getWidth() : getHeight();
+        int visibleEnd = visibleBegin + visibleLength;
+        int slotBegin = WIDE ? rect.left : rect.top;
+        int slotEnd = WIDE ? rect.right : rect.bottom;
+
+        int position = visibleBegin;
+        if (visibleLength < slotEnd - slotBegin) {
+            position = visibleBegin;
+        } else if (slotBegin < visibleBegin) {
+            position = slotBegin;
+        } else if (slotEnd > visibleEnd) {
+            position = slotEnd - visibleLength;
+        }
+
+        setScrollPosition(position);
+    }
+
+    public void setScrollPosition(int position) {
+        position = Utils.clamp(position, 0, mLayout.getScrollLimit());
+        mScroller.setPosition(position);
+        updateScrollPosition(position, false);
+    }
+
+    public void setSlotSpec(Spec spec) {
+        mLayout.setSlotSpec(spec);
+    }
+
+    @Override
+    public void addComponent(GLView view) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
+        if (!changeSize) return;
+
+        // Make sure we are still at a resonable scroll position after the size
+        // is changed (like orientation change). We choose to keep the center
+        // visible slot still visible. This is arbitrary but reasonable.
+        int visibleIndex =
+                (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
+        mLayout.setSize(r - l, b - t);
+        makeSlotVisible(visibleIndex);
+        if (mOverscrollEffect == OVERSCROLL_3D) {
+            mPaper.setSize(r - l, b - t);
+        }
+    }
+
+    public void startScatteringAnimation(RelativePosition position) {
+        mAnimation = new ScatteringAnimation(position);
+        mAnimation.start();
+        if (mLayout.mSlotCount != 0) invalidate();
+    }
+
+    public void startRisingAnimation() {
+        mAnimation = new RisingAnimation();
+        mAnimation.start();
+        if (mLayout.mSlotCount != 0) invalidate();
+    }
+
+    private void updateScrollPosition(int position, boolean force) {
+        if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
+        if (WIDE) {
+            mScrollX = position;
+        } else {
+            mScrollY = position;
+        }
+        mLayout.setScrollPosition(position);
+        onScrollPositionChanged(position);
+    }
+
+    protected void onScrollPositionChanged(int newPosition) {
+        int limit = mLayout.getScrollLimit();
+        mListener.onScrollPositionChanged(newPosition, limit);
+    }
+
+    public Rect getSlotRect(int slotIndex) {
+        return mLayout.getSlotRect(slotIndex, new Rect());
+    }
+
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        if (mUIListener != null) mUIListener.onUserInteraction();
+        mGestureDetector.onTouchEvent(event);
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mDownInScrolling = !mScroller.isFinished();
+                mScroller.forceFinished();
+                break;
+            case MotionEvent.ACTION_UP:
+                mPaper.onRelease();
+                invalidate();
+                break;
+        }
+        return true;
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void setUserInteractionListener(UserInteractionListener listener) {
+        mUIListener = listener;
+    }
+
+    public void setOverscrollEffect(int kind) {
+        mOverscrollEffect = kind;
+        mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
+    }
+
+    private static int[] expandIntArray(int array[], int capacity) {
+        while (array.length < capacity) {
+            array = new int[array.length * 2];
+        }
+        return array;
+    }
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        super.render(canvas);
+
+        if (mRenderer == null) return;
+        mRenderer.prepareDrawing();
+
+        long animTime = AnimationTime.get();
+        boolean more = mScroller.advanceAnimation(animTime);
+        more |= mLayout.advanceAnimation(animTime);
+        int oldX = mScrollX;
+        updateScrollPosition(mScroller.getPosition(), false);
+
+        boolean paperActive = false;
+        if (mOverscrollEffect == OVERSCROLL_3D) {
+            // Check if an edge is reached and notify mPaper if so.
+            int newX = mScrollX;
+            int limit = mLayout.getScrollLimit();
+            if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
+                float v = mScroller.getCurrVelocity();
+                if (newX == limit) v = -v;
+
+                // I don't know why, but getCurrVelocity() can return NaN.
+                if (!Float.isNaN(v)) {
+                    mPaper.edgeReached(v);
+                }
+            }
+            paperActive = mPaper.advanceAnimation();
+        }
+
+        more |= paperActive;
+
+        if (mAnimation != null) {
+            more |= mAnimation.calculate(animTime);
+        }
+
+        canvas.translate(-mScrollX, -mScrollY);
+
+        int requestCount = 0;
+        int requestedSlot[] = expandIntArray(mRequestRenderSlots,
+                mLayout.mVisibleEnd - mLayout.mVisibleStart);
+
+        for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) {
+            int r = renderItem(canvas, i, 0, paperActive);
+            if ((r & RENDER_MORE_FRAME) != 0) more = true;
+            if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i;
+        }
+
+        for (int pass = 1; requestCount != 0; ++pass) {
+            int newCount = 0;
+            for (int i = 0; i < requestCount; ++i) {
+                int r = renderItem(canvas,
+                        requestedSlot[i], pass, paperActive);
+                if ((r & RENDER_MORE_FRAME) != 0) more = true;
+                if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i;
+            }
+            requestCount = newCount;
+        }
+
+        canvas.translate(mScrollX, mScrollY);
+
+        if (more) invalidate();
+
+        final UserInteractionListener listener = mUIListener;
+        if (mMoreAnimation && !more && listener != null) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    listener.onUserInteractionEnd();
+                }
+            });
+        }
+        mMoreAnimation = more;
+    }
+
+    private int renderItem(
+            GLCanvas canvas, int index, int pass, boolean paperActive) {
+        canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+        Rect rect = mLayout.getSlotRect(index, mTempRect);
+        if (paperActive) {
+            canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0);
+        } else {
+            canvas.translate(rect.left, rect.top, 0);
+        }
+        if (mAnimation != null && mAnimation.isActive()) {
+            mAnimation.apply(canvas, index, rect);
+        }
+        int result = mRenderer.renderSlot(
+                canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top);
+        canvas.restore();
+        return result;
+    }
+
+    public static abstract class SlotAnimation extends Animation {
+        protected float mProgress = 0;
+
+        public SlotAnimation() {
+            setInterpolator(new DecelerateInterpolator(4));
+            setDuration(1500);
+        }
+
+        @Override
+        protected void onCalculate(float progress) {
+            mProgress = progress;
+        }
+
+        abstract public void apply(GLCanvas canvas, int slotIndex, Rect target);
+    }
+
+    public static class RisingAnimation extends SlotAnimation {
+        private static final int RISING_DISTANCE = 128;
+
+        @Override
+        public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+            canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress));
+        }
+    }
+
+    public static class ScatteringAnimation extends SlotAnimation {
+        private int PHOTO_DISTANCE = 1000;
+        private RelativePosition mCenter;
+
+        public ScatteringAnimation(RelativePosition center) {
+            mCenter = center;
+        }
+
+        @Override
+        public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+            canvas.translate(
+                    (mCenter.getX() - target.centerX()) * (1 - mProgress),
+                    (mCenter.getY() - target.centerY()) * (1 - mProgress),
+                    slotIndex * PHOTO_DISTANCE * (1 - mProgress));
+            canvas.setAlpha(mProgress);
+        }
+    }
+
+    // This Spec class is used to specify the size of each slot in the SlotView.
+    // There are two ways to do it:
+    //
+    // (1) Specify slotWidth and slotHeight: they specify the width and height
+    //     of each slot. The number of rows and the gap between slots will be
+    //     determined automatically.
+    // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number
+    //     of rows in landscape/portrait mode and the gap between slots. The
+    //     width and height of each slot is determined automatically.
+    //
+    // The initial value of -1 means they are not specified.
+    public static class Spec {
+        public int slotWidth = -1;
+        public int slotHeight = -1;
+        public int slotHeightAdditional = 0;
+
+        public int rowsLand = -1;
+        public int rowsPort = -1;
+        public int slotGap = -1;
+    }
+
+    public class Layout {
+
+        private int mVisibleStart;
+        private int mVisibleEnd;
+
+        private int mSlotCount;
+        private int mSlotWidth;
+        private int mSlotHeight;
+        private int mSlotGap;
+
+        private Spec mSpec;
+
+        private int mWidth;
+        private int mHeight;
+
+        private int mUnitCount;
+        private int mContentLength;
+        private int mScrollPosition;
+
+        private IntegerAnimation mVerticalPadding = new IntegerAnimation();
+        private IntegerAnimation mHorizontalPadding = new IntegerAnimation();
+
+        public void setSlotSpec(Spec spec) {
+            mSpec = spec;
+        }
+
+        public boolean setSlotCount(int slotCount) {
+            if (slotCount == mSlotCount) return false;
+            if (mSlotCount != 0) {
+                mHorizontalPadding.setEnabled(true);
+                mVerticalPadding.setEnabled(true);
+            }
+            mSlotCount = slotCount;
+            int hPadding = mHorizontalPadding.getTarget();
+            int vPadding = mVerticalPadding.getTarget();
+            initLayoutParameters();
+            return vPadding != mVerticalPadding.getTarget()
+                    || hPadding != mHorizontalPadding.getTarget();
+        }
+
+        public Rect getSlotRect(int index, Rect rect) {
+            int col, row;
+            if (WIDE) {
+                col = index / mUnitCount;
+                row = index - col * mUnitCount;
+            } else {
+                row = index / mUnitCount;
+                col = index - row * mUnitCount;
+            }
+
+            int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap);
+            int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap);
+            rect.set(x, y, x + mSlotWidth, y + mSlotHeight);
+            return rect;
+        }
+
+        public int getSlotWidth() {
+            return mSlotWidth;
+        }
+
+        public int getSlotHeight() {
+            return mSlotHeight;
+        }
+
+        // Calculate
+        // (1) mUnitCount: the number of slots we can fit into one column (or row).
+        // (2) mContentLength: the width (or height) we need to display all the
+        //     columns (rows).
+        // (3) padding[]: the vertical and horizontal padding we need in order
+        //     to put the slots towards to the center of the display.
+        //
+        // The "major" direction is the direction the user can scroll. The other
+        // direction is the "minor" direction.
+        //
+        // The comments inside this method are the description when the major
+        // directon is horizontal (X), and the minor directon is vertical (Y).
+        private void initLayoutParameters(
+                int majorLength, int minorLength,  /* The view width and height */
+                int majorUnitSize, int minorUnitSize,  /* The slot width and height */
+                int[] padding) {
+            int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap);
+            if (unitCount == 0) unitCount = 1;
+            mUnitCount = unitCount;
+
+            // We put extra padding above and below the column.
+            int availableUnits = Math.min(mUnitCount, mSlotCount);
+            int usedMinorLength = availableUnits * minorUnitSize +
+                    (availableUnits - 1) * mSlotGap;
+            padding[0] = (minorLength - usedMinorLength) / 2;
+
+            // Then calculate how many columns we need for all slots.
+            int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
+            mContentLength = count * majorUnitSize + (count - 1) * mSlotGap;
+
+            // If the content length is less then the screen width, put
+            // extra padding in left and right.
+            padding[1] = Math.max(0, (majorLength - mContentLength) / 2);
+        }
+
+        private void initLayoutParameters() {
+            // Initialize mSlotWidth and mSlotHeight from mSpec
+            if (mSpec.slotWidth != -1) {
+                mSlotGap = 0;
+                mSlotWidth = mSpec.slotWidth;
+                mSlotHeight = mSpec.slotHeight;
+            } else {
+                int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort;
+                mSlotGap = mSpec.slotGap;
+                mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows);
+                mSlotWidth = mSlotHeight - mSpec.slotHeightAdditional;
+            }
+
+            if (mRenderer != null) {
+                mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight);
+            }
+
+            int[] padding = new int[2];
+            if (WIDE) {
+                initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
+                mVerticalPadding.startAnimateTo(padding[0]);
+                mHorizontalPadding.startAnimateTo(padding[1]);
+            } else {
+                initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
+                mVerticalPadding.startAnimateTo(padding[1]);
+                mHorizontalPadding.startAnimateTo(padding[0]);
+            }
+            updateVisibleSlotRange();
+        }
+
+        public void setSize(int width, int height) {
+            mWidth = width;
+            mHeight = height;
+            initLayoutParameters();
+        }
+
+        private void updateVisibleSlotRange() {
+            int position = mScrollPosition;
+
+            if (WIDE) {
+                int startCol = position / (mSlotWidth + mSlotGap);
+                int start = Math.max(0, mUnitCount * startCol);
+                int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) /
+                        (mSlotWidth + mSlotGap);
+                int end = Math.min(mSlotCount, mUnitCount * endCol);
+                setVisibleRange(start, end);
+            } else {
+                int startRow = position / (mSlotHeight + mSlotGap);
+                int start = Math.max(0, mUnitCount * startRow);
+                int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) /
+                        (mSlotHeight + mSlotGap);
+                int end = Math.min(mSlotCount, mUnitCount * endRow);
+                setVisibleRange(start, end);
+            }
+        }
+
+        public void setScrollPosition(int position) {
+            if (mScrollPosition == position) return;
+            mScrollPosition = position;
+            updateVisibleSlotRange();
+        }
+
+        private void setVisibleRange(int start, int end) {
+            if (start == mVisibleStart && end == mVisibleEnd) return;
+            if (start < end) {
+                mVisibleStart = start;
+                mVisibleEnd = end;
+            } else {
+                mVisibleStart = mVisibleEnd = 0;
+            }
+            if (mRenderer != null) {
+                mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd);
+            }
+        }
+
+        public int getVisibleStart() {
+            return mVisibleStart;
+        }
+
+        public int getVisibleEnd() {
+            return mVisibleEnd;
+        }
+
+        public int getSlotIndexByPosition(float x, float y) {
+            int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0);
+            int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition);
+
+            absoluteX -= mHorizontalPadding.get();
+            absoluteY -= mVerticalPadding.get();
+
+            if (absoluteX < 0 || absoluteY < 0) {
+                return INDEX_NONE;
+            }
+
+            int columnIdx = absoluteX / (mSlotWidth + mSlotGap);
+            int rowIdx = absoluteY / (mSlotHeight + mSlotGap);
+
+            if (!WIDE && columnIdx >= mUnitCount) {
+                return INDEX_NONE;
+            }
+
+            if (WIDE && rowIdx >= mUnitCount) {
+                return INDEX_NONE;
+            }
+
+            if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) {
+                return INDEX_NONE;
+            }
+
+            if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) {
+                return INDEX_NONE;
+            }
+
+            int index = WIDE
+                    ? (columnIdx * mUnitCount + rowIdx)
+                    : (rowIdx * mUnitCount + columnIdx);
+
+            return index >= mSlotCount ? INDEX_NONE : index;
+        }
+
+        public int getScrollLimit() {
+            int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
+            return limit <= 0 ? 0 : limit;
+        }
+
+        public boolean advanceAnimation(long animTime) {
+            // use '|' to make sure both sides will be executed
+            return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime);
+        }
+    }
+
+    private class MyGestureListener implements GestureDetector.OnGestureListener {
+        private boolean isDown;
+
+        // We call the listener's onDown() when our onShowPress() is called and
+        // call the listener's onUp() when we receive any further event.
+        @Override
+        public void onShowPress(MotionEvent e) {
+            GLRoot root = getGLRoot();
+            root.lockRenderThread();
+            try {
+                if (isDown) return;
+                int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+                if (index != INDEX_NONE) {
+                    isDown = true;
+                    mListener.onDown(index);
+                }
+            } finally {
+                root.unlockRenderThread();
+            }
+        }
+
+        private void cancelDown(boolean byLongPress) {
+            if (!isDown) return;
+            isDown = false;
+            mListener.onUp(byLongPress);
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            return false;
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1,
+                MotionEvent e2, float velocityX, float velocityY) {
+            cancelDown(false);
+            int scrollLimit = mLayout.getScrollLimit();
+            if (scrollLimit == 0) return false;
+            float velocity = WIDE ? velocityX : velocityY;
+            mScroller.fling((int) -velocity, 0, scrollLimit);
+            if (mUIListener != null) mUIListener.onUserInteractionBegin();
+            invalidate();
+            return true;
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent e1,
+                MotionEvent e2, float distanceX, float distanceY) {
+            cancelDown(false);
+            float distance = WIDE ? distanceX : distanceY;
+            int overDistance = mScroller.startScroll(
+                    Math.round(distance), 0, mLayout.getScrollLimit());
+            if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) {
+                mPaper.overScroll(overDistance);
+            }
+            invalidate();
+            return true;
+        }
+
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            cancelDown(false);
+            if (mDownInScrolling) return true;
+            int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+            if (index != INDEX_NONE) mListener.onSingleTapUp(index);
+            return true;
+        }
+
+        @Override
+        public void onLongPress(MotionEvent e) {
+            cancelDown(true);
+            if (mDownInScrolling) return;
+            lockRendering();
+            try {
+                int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+                if (index != INDEX_NONE) mListener.onLongTap(index);
+            } finally {
+                unlockRendering();
+            }
+        }
+    }
+
+    public void setStartIndex(int index) {
+        mStartIndex = index;
+    }
+
+    // Return true if the layout parameters have been changed
+    public boolean setSlotCount(int slotCount) {
+        boolean changed = mLayout.setSlotCount(slotCount);
+
+        // mStartIndex is applied the first time setSlotCount is called.
+        if (mStartIndex != INDEX_NONE) {
+            setCenterIndex(mStartIndex);
+            mStartIndex = INDEX_NONE;
+        }
+        // Reset the scroll position to avoid scrolling over the updated limit.
+        setScrollPosition(WIDE ? mScrollX : mScrollY);
+        return changed;
+    }
+
+    public int getVisibleStart() {
+        return mLayout.getVisibleStart();
+    }
+
+    public int getVisibleEnd() {
+        return mLayout.getVisibleEnd();
+    }
+
+    public int getScrollX() {
+        return mScrollX;
+    }
+
+    public int getScrollY() {
+        return mScrollY;
+    }
+
+    public Rect getSlotRect(int slotIndex, GLView rootPane) {
+        // Get slot rectangle relative to this root pane.
+        Rect offset = new Rect();
+        rootPane.getBoundsOf(this, offset);
+        Rect r = getSlotRect(slotIndex);
+        r.offset(offset.left - getScrollX(),
+                offset.top - getScrollY());
+        return r;
+    }
+
+    private static class IntegerAnimation extends Animation {
+        private int mTarget;
+        private int mCurrent = 0;
+        private int mFrom = 0;
+        private boolean mEnabled = false;
+
+        public void setEnabled(boolean enabled) {
+            mEnabled = enabled;
+        }
+
+        public void startAnimateTo(int target) {
+            if (!mEnabled) {
+                mTarget = mCurrent = target;
+                return;
+            }
+            if (target == mTarget) return;
+
+            mFrom = mCurrent;
+            mTarget = target;
+            setDuration(180);
+            start();
+        }
+
+        public int get() {
+            return mCurrent;
+        }
+
+        public int getTarget() {
+            return mTarget;
+        }
+
+        @Override
+        protected void onCalculate(float progress) {
+            mCurrent = Math.round(mFrom + progress * (mTarget - mFrom));
+            if (progress == 1f) mEnabled = false;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
new file mode 100644
index 0000000..18121e6
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.annotation.TargetApi;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.glrenderer.ExtTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+public abstract class SurfaceTextureScreenNail implements ScreenNail,
+        SurfaceTexture.OnFrameAvailableListener {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SurfaceTextureScreenNail";
+    // This constant is not available in API level before 15, but it was just an
+    // oversight.
+    private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
+
+    protected ExtTexture mExtTexture;
+    private SurfaceTexture mSurfaceTexture;
+    private int mWidth, mHeight;
+    private float[] mTransform = new float[16];
+    private boolean mHasTexture = false;
+
+    public SurfaceTextureScreenNail() {
+    }
+
+    public void acquireSurfaceTexture(GLCanvas canvas) {
+        mExtTexture = new ExtTexture(canvas, GL_TEXTURE_EXTERNAL_OES);
+        mExtTexture.setSize(mWidth, mHeight);
+        mSurfaceTexture = new SurfaceTexture(mExtTexture.getId());
+        setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight);
+        mSurfaceTexture.setOnFrameAvailableListener(this);
+        synchronized (this) {
+            mHasTexture = true;
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+    private static void setDefaultBufferSize(SurfaceTexture st, int width, int height) {
+        if (ApiHelper.HAS_SET_DEFALT_BUFFER_SIZE) {
+            st.setDefaultBufferSize(width, height);
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private static void releaseSurfaceTexture(SurfaceTexture st) {
+        st.setOnFrameAvailableListener(null);
+        if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) {
+            st.release();
+        }
+    }
+
+    public SurfaceTexture getSurfaceTexture() {
+        return mSurfaceTexture;
+    }
+
+    public void releaseSurfaceTexture() {
+        synchronized (this) {
+            mHasTexture = false;
+        }
+        mExtTexture.recycle();
+        mExtTexture = null;
+        releaseSurfaceTexture(mSurfaceTexture);
+        mSurfaceTexture = null;
+    }
+
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+    }
+
+    public void resizeTexture() {
+        if (mExtTexture != null) {
+            mExtTexture.setSize(mWidth, mHeight);
+            setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight);
+        }
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        synchronized (this) {
+            if (!mHasTexture) return;
+            mSurfaceTexture.updateTexImage();
+            mSurfaceTexture.getTransformMatrix(mTransform);
+
+            // Flip vertically.
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            int cx = x + width / 2;
+            int cy = y + height / 2;
+            canvas.translate(cx, cy);
+            canvas.scale(1, -1, 1);
+            canvas.translate(-cx, -cy);
+            updateTransformMatrix(mTransform);
+            canvas.drawTexture(mExtTexture, mTransform, x, y, width, height);
+            canvas.restore();
+        }
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        throw new UnsupportedOperationException();
+    }
+
+    protected void updateTransformMatrix(float[] matrix) {}
+
+    @Override
+    abstract public void noDraw();
+
+    @Override
+    abstract public void recycle();
+
+    @Override
+    abstract public void onFrameAvailable(SurfaceTexture surfaceTexture);
+}
diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java
new file mode 100644
index 0000000..ba10357
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.Utils;
+
+public class SynchronizedHandler extends Handler {
+
+    private final GLRoot mRoot;
+
+    public SynchronizedHandler(GLRoot root) {
+        mRoot = Utils.checkNotNull(root);
+    }
+
+    @Override
+    public void dispatchMessage(Message message) {
+        mRoot.lockRenderThread();
+        try {
+            super.dispatchMessage(message);
+        } finally {
+            mRoot.unlockRenderThread();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
new file mode 100644
index 0000000..3185c75
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.v4.util.LongSparseArray;
+import android.util.DisplayMetrics;
+import android.util.FloatMath;
+import android.view.WindowManager;
+
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DecodeUtils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class TileImageView extends GLView {
+    public static final int SIZE_UNKNOWN = -1;
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "TileImageView";
+    private static final int UPLOAD_LIMIT = 1;
+
+    // TILE_SIZE must be 2^N
+    private static int sTileSize;
+
+    /*
+     *  This is the tile state in the CPU side.
+     *  Life of a Tile:
+     *      ACTIVATED (initial state)
+     *              --> IN_QUEUE - by queueForDecode()
+     *              --> RECYCLED - by recycleTile()
+     *      IN_QUEUE --> DECODING - by decodeTile()
+     *               --> RECYCLED - by recycleTile)
+     *      DECODING --> RECYCLING - by recycleTile()
+     *               --> DECODED  - by decodeTile()
+     *               --> DECODE_FAIL - by decodeTile()
+     *      RECYCLING --> RECYCLED - by decodeTile()
+     *      DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
+     *      DECODED --> RECYCLED - by recycleTile()
+     *      DECODE_FAIL -> RECYCLED - by recycleTile()
+     *      RECYCLED --> ACTIVATED - by obtainTile()
+     */
+    private static final int STATE_ACTIVATED = 0x01;
+    private static final int STATE_IN_QUEUE = 0x02;
+    private static final int STATE_DECODING = 0x04;
+    private static final int STATE_DECODED = 0x08;
+    private static final int STATE_DECODE_FAIL = 0x10;
+    private static final int STATE_RECYCLING = 0x20;
+    private static final int STATE_RECYCLED = 0x40;
+
+    private TileSource mModel;
+    private ScreenNail mScreenNail;
+    protected int mLevelCount;  // cache the value of mScaledBitmaps.length
+
+    // The mLevel variable indicates which level of bitmap we should use.
+    // Level 0 means the original full-sized bitmap, and a larger value means
+    // a smaller scaled bitmap (The width and height of each scaled bitmap is
+    // half size of the previous one). If the value is in [0, mLevelCount), we
+    // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
+    // is mLevelCount, and that means we use mScreenNail for display.
+    private int mLevel = 0;
+
+    // The offsets of the (left, top) of the upper-left tile to the (left, top)
+    // of the view.
+    private int mOffsetX;
+    private int mOffsetY;
+
+    private int mUploadQuota;
+    private boolean mRenderComplete;
+
+    private final RectF mSourceRect = new RectF();
+    private final RectF mTargetRect = new RectF();
+
+    private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
+
+    // The following three queue is guarded by TileImageView.this
+    private final TileQueue mRecycledQueue = new TileQueue();
+    private final TileQueue mUploadQueue = new TileQueue();
+    private final TileQueue mDecodeQueue = new TileQueue();
+
+    // The width and height of the full-sized bitmap
+    protected int mImageWidth = SIZE_UNKNOWN;
+    protected int mImageHeight = SIZE_UNKNOWN;
+
+    protected int mCenterX;
+    protected int mCenterY;
+    protected float mScale;
+    protected int mRotation;
+
+    // Temp variables to avoid memory allocation
+    private final Rect mTileRange = new Rect();
+    private final Rect mActiveRange[] = {new Rect(), new Rect()};
+
+    private final TileUploader mTileUploader = new TileUploader();
+    private boolean mIsTextureFreed;
+    private Future<Void> mTileDecoder;
+    private final ThreadPool mThreadPool;
+    private boolean mBackgroundTileUploaded;
+
+    public static interface TileSource {
+        public int getLevelCount();
+        public ScreenNail getScreenNail();
+        public int getImageWidth();
+        public int getImageHeight();
+
+        // The tile returned by this method can be specified this way: Assuming
+        // the image size is (width, height), first take the intersection of (0,
+        // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
+        // in extending the region, we found some part of the region are outside
+        // the image, those pixels are filled with black.
+        //
+        // If level > 0, it does the same operation on a down-scaled version of
+        // the original image (down-scaled by a factor of 2^level), but (x, y)
+        // still refers to the coordinate on the original image.
+        //
+        // The method would be called in another thread.
+        public Bitmap getTile(int level, int x, int y, int tileSize);
+    }
+
+    public static boolean isHighResolution(Context context) {
+        DisplayMetrics metrics = new DisplayMetrics();
+        WindowManager wm = (WindowManager)
+                context.getSystemService(Context.WINDOW_SERVICE);
+        wm.getDefaultDisplay().getMetrics(metrics);
+        return metrics.heightPixels > 2048 ||  metrics.widthPixels > 2048;
+    }
+
+    public TileImageView(GalleryContext context) {
+        mThreadPool = context.getThreadPool();
+        mTileDecoder = mThreadPool.submit(new TileDecoder());
+        if (sTileSize == 0) {
+            if (isHighResolution(context.getAndroidContext())) {
+                sTileSize = 512 ;
+            } else {
+                sTileSize = 256;
+            }
+        }
+    }
+
+    public void setModel(TileSource model) {
+        mModel = model;
+        if (model != null) notifyModelInvalidated();
+    }
+
+    public void setScreenNail(ScreenNail s) {
+        mScreenNail = s;
+    }
+
+    public void notifyModelInvalidated() {
+        invalidateTiles();
+        if (mModel == null) {
+            mScreenNail = null;
+            mImageWidth = 0;
+            mImageHeight = 0;
+            mLevelCount = 0;
+        } else {
+            setScreenNail(mModel.getScreenNail());
+            mImageWidth = mModel.getImageWidth();
+            mImageHeight = mModel.getImageHeight();
+            mLevelCount = mModel.getLevelCount();
+        }
+        layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(
+            boolean changeSize, int left, int top, int right, int bottom) {
+        super.onLayout(changeSize, left, top, right, bottom);
+        if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+    }
+
+    // Prepare the tiles we want to use for display.
+    //
+    // 1. Decide the tile level we want to use for display.
+    // 2. Decide the tile levels we want to keep as texture (in addition to
+    //    the one we use for display).
+    // 3. Recycle unused tiles.
+    // 4. Activate the tiles we want.
+    private void layoutTiles(int centerX, int centerY, float scale, int rotation) {
+        // The width and height of this view.
+        int width = getWidth();
+        int height = getHeight();
+
+        // The tile levels we want to keep as texture is in the range
+        // [fromLevel, endLevel).
+        int fromLevel;
+        int endLevel;
+
+        // We want to use a texture larger than or equal to the display size.
+        mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount);
+
+        // We want to keep one more tile level as texture in addition to what
+        // we use for display. So it can be faster when the scale moves to the
+        // next level. We choose a level closer to the current scale.
+        if (mLevel != mLevelCount) {
+            Rect range = mTileRange;
+            getRange(range, centerX, centerY, mLevel, scale, rotation);
+            mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale);
+            mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale);
+            fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
+        } else {
+            // Activate the tiles of the smallest two levels.
+            fromLevel = mLevel - 2;
+            mOffsetX = Math.round(width / 2f - centerX * scale);
+            mOffsetY = Math.round(height / 2f - centerY * scale);
+        }
+
+        fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
+        endLevel = Math.min(fromLevel + 2, mLevelCount);
+
+        Rect range[] = mActiveRange;
+        for (int i = fromLevel; i < endLevel; ++i) {
+            getRange(range[i - fromLevel], centerX, centerY, i, rotation);
+        }
+
+        // If rotation is transient, don't update the tile.
+        if (rotation % 90 != 0) return;
+
+        synchronized (this) {
+            mDecodeQueue.clean();
+            mUploadQueue.clean();
+            mBackgroundTileUploaded = false;
+
+            // Recycle unused tiles: if the level of the active tile is outside the
+            // range [fromLevel, endLevel) or not in the visible range.
+            int n = mActiveTiles.size();
+            for (int i = 0; i < n; i++) {
+                Tile tile = mActiveTiles.valueAt(i);
+                int level = tile.mTileLevel;
+                if (level < fromLevel || level >= endLevel
+                        || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
+                    mActiveTiles.removeAt(i);
+                    i--;
+                    n--;
+                    recycleTile(tile);
+                }
+            }
+        }
+
+        for (int i = fromLevel; i < endLevel; ++i) {
+            int size = sTileSize << i;
+            Rect r = range[i - fromLevel];
+            for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
+                for (int x = r.left, right = r.right; x < right; x += size) {
+                    activateTile(x, y, i);
+                }
+            }
+        }
+        invalidate();
+    }
+
+    protected synchronized void invalidateTiles() {
+        mDecodeQueue.clean();
+        mUploadQueue.clean();
+
+        // TODO disable decoder
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile tile = mActiveTiles.valueAt(i);
+            recycleTile(tile);
+        }
+        mActiveTiles.clear();
+    }
+
+    private void getRange(Rect out, int cX, int cY, int level, int rotation) {
+        getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
+    }
+
+    // If the bitmap is scaled by the given factor "scale", return the
+    // rectangle containing visible range. The left-top coordinate returned is
+    // aligned to the tile boundary.
+    //
+    // (cX, cY) is the point on the original bitmap which will be put in the
+    // center of the ImageViewer.
+    private void getRange(Rect out,
+            int cX, int cY, int level, float scale, int rotation) {
+
+        double radians = Math.toRadians(-rotation);
+        double w = getWidth();
+        double h = getHeight();
+
+        double cos = Math.cos(radians);
+        double sin = Math.sin(radians);
+        int width = (int) Math.ceil(Math.max(
+                Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
+        int height = (int) Math.ceil(Math.max(
+                Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
+
+        int left = (int) FloatMath.floor(cX - width / (2f * scale));
+        int top = (int) FloatMath.floor(cY - height / (2f * scale));
+        int right = (int) FloatMath.ceil(left + width / scale);
+        int bottom = (int) FloatMath.ceil(top + height / scale);
+
+        // align the rectangle to tile boundary
+        int size = sTileSize << level;
+        left = Math.max(0, size * (left / size));
+        top = Math.max(0, size * (top / size));
+        right = Math.min(mImageWidth, right);
+        bottom = Math.min(mImageHeight, bottom);
+
+        out.set(left, top, right, bottom);
+    }
+
+    // Calculate where the center of the image is, in the view coordinates.
+    public void getImageCenter(Point center) {
+        // The width and height of this view.
+        int viewW = getWidth();
+        int viewH = getHeight();
+
+        // The distance between the center of the view to the center of the
+        // bitmap, in bitmap units. (mCenterX and mCenterY are the bitmap
+        // coordinates correspond to the center of view)
+        int distW, distH;
+        if (mRotation % 180 == 0) {
+            distW = mImageWidth / 2 - mCenterX;
+            distH = mImageHeight / 2 - mCenterY;
+        } else {
+            distW = mImageHeight / 2 - mCenterY;
+            distH = mImageWidth / 2 - mCenterX;
+        }
+
+        // Convert to view coordinates. mScale translates from bitmap units to
+        // view units.
+        center.x = Math.round(viewW / 2f + distW * mScale);
+        center.y = Math.round(viewH / 2f + distH * mScale);
+    }
+
+    public boolean setPosition(int centerX, int centerY, float scale, int rotation) {
+        if (mCenterX == centerX && mCenterY == centerY
+                && mScale == scale && mRotation == rotation) return false;
+        mCenterX = centerX;
+        mCenterY = centerY;
+        mScale = scale;
+        mRotation = rotation;
+        layoutTiles(centerX, centerY, scale, rotation);
+        invalidate();
+        return true;
+    }
+
+    public void freeTextures() {
+        mIsTextureFreed = true;
+
+        if (mTileDecoder != null) {
+            mTileDecoder.cancel();
+            mTileDecoder.get();
+            mTileDecoder = null;
+        }
+
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile texture = mActiveTiles.valueAt(i);
+            texture.recycle();
+        }
+        mActiveTiles.clear();
+        mTileRange.set(0, 0, 0, 0);
+
+        synchronized (this) {
+            mUploadQueue.clean();
+            mDecodeQueue.clean();
+            Tile tile = mRecycledQueue.pop();
+            while (tile != null) {
+                tile.recycle();
+                tile = mRecycledQueue.pop();
+            }
+        }
+        setScreenNail(null);
+    }
+
+    public void prepareTextures() {
+        if (mTileDecoder == null) {
+            mTileDecoder = mThreadPool.submit(new TileDecoder());
+        }
+        if (mIsTextureFreed) {
+            layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+            mIsTextureFreed = false;
+            setScreenNail(mModel == null ? null : mModel.getScreenNail());
+        }
+    }
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        mUploadQuota = UPLOAD_LIMIT;
+        mRenderComplete = true;
+
+        int level = mLevel;
+        int rotation = mRotation;
+        int flags = 0;
+        if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
+
+        if (flags != 0) {
+            canvas.save(flags);
+            if (rotation != 0) {
+                int centerX = getWidth() / 2, centerY = getHeight() / 2;
+                canvas.translate(centerX, centerY);
+                canvas.rotate(rotation, 0, 0, 1);
+                canvas.translate(-centerX, -centerY);
+            }
+        }
+        try {
+            if (level != mLevelCount && !isScreenNailAnimating()) {
+                if (mScreenNail != null) {
+                    mScreenNail.noDraw();
+                }
+
+                int size = (sTileSize << level);
+                float length = size * mScale;
+                Rect r = mTileRange;
+
+                for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
+                    float y = mOffsetY + i * length;
+                    for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
+                        float x = mOffsetX + j * length;
+                        drawTile(canvas, tx, ty, level, x, y, length);
+                    }
+                }
+            } else if (mScreenNail != null) {
+                mScreenNail.draw(canvas, mOffsetX, mOffsetY,
+                        Math.round(mImageWidth * mScale),
+                        Math.round(mImageHeight * mScale));
+                if (isScreenNailAnimating()) {
+                    invalidate();
+                }
+            }
+        } finally {
+            if (flags != 0) canvas.restore();
+        }
+
+        if (mRenderComplete) {
+            if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas);
+        } else {
+            invalidate();
+        }
+    }
+
+    private boolean isScreenNailAnimating() {
+        return (mScreenNail instanceof TiledScreenNail)
+                && ((TiledScreenNail) mScreenNail).isAnimating();
+    }
+
+    private void uploadBackgroundTiles(GLCanvas canvas) {
+        mBackgroundTileUploaded = true;
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile tile = mActiveTiles.valueAt(i);
+            if (!tile.isContentValid()) queueForDecode(tile);
+        }
+    }
+
+    void queueForUpload(Tile tile) {
+        synchronized (this) {
+            mUploadQueue.push(tile);
+        }
+        if (mTileUploader.mActive.compareAndSet(false, true)) {
+            getGLRoot().addOnGLIdleListener(mTileUploader);
+        }
+    }
+
+    synchronized void queueForDecode(Tile tile) {
+        if (tile.mTileState == STATE_ACTIVATED) {
+            tile.mTileState = STATE_IN_QUEUE;
+            if (mDecodeQueue.push(tile)) notifyAll();
+        }
+    }
+
+    boolean decodeTile(Tile tile) {
+        synchronized (this) {
+            if (tile.mTileState != STATE_IN_QUEUE) return false;
+            tile.mTileState = STATE_DECODING;
+        }
+        boolean decodeComplete = tile.decode();
+        synchronized (this) {
+            if (tile.mTileState == STATE_RECYCLING) {
+                tile.mTileState = STATE_RECYCLED;
+                if (tile.mDecodedTile != null) {
+                    GalleryBitmapPool.getInstance().put(tile.mDecodedTile);
+                    tile.mDecodedTile = null;
+                }
+                mRecycledQueue.push(tile);
+                return false;
+            }
+            tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
+            return decodeComplete;
+        }
+    }
+
+    private synchronized Tile obtainTile(int x, int y, int level) {
+        Tile tile = mRecycledQueue.pop();
+        if (tile != null) {
+            tile.mTileState = STATE_ACTIVATED;
+            tile.update(x, y, level);
+            return tile;
+        }
+        return new Tile(x, y, level);
+    }
+
+    synchronized void recycleTile(Tile tile) {
+        if (tile.mTileState == STATE_DECODING) {
+            tile.mTileState = STATE_RECYCLING;
+            return;
+        }
+        tile.mTileState = STATE_RECYCLED;
+        if (tile.mDecodedTile != null) {
+            GalleryBitmapPool.getInstance().put(tile.mDecodedTile);
+            tile.mDecodedTile = null;
+        }
+        mRecycledQueue.push(tile);
+    }
+
+    private void activateTile(int x, int y, int level) {
+        long key = makeTileKey(x, y, level);
+        Tile tile = mActiveTiles.get(key);
+        if (tile != null) {
+            if (tile.mTileState == STATE_IN_QUEUE) {
+                tile.mTileState = STATE_ACTIVATED;
+            }
+            return;
+        }
+        tile = obtainTile(x, y, level);
+        mActiveTiles.put(key, tile);
+    }
+
+    private Tile getTile(int x, int y, int level) {
+        return mActiveTiles.get(makeTileKey(x, y, level));
+    }
+
+    private static long makeTileKey(int x, int y, int level) {
+        long result = x;
+        result = (result << 16) | y;
+        result = (result << 16) | level;
+        return result;
+    }
+
+    private class TileUploader implements GLRoot.OnGLIdleListener {
+        AtomicBoolean mActive = new AtomicBoolean(false);
+
+        @Override
+        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+            // Skips uploading if there is a pending rendering request.
+            // Returns true to keep uploading in next rendering loop.
+            if (renderRequested) return true;
+            int quota = UPLOAD_LIMIT;
+            Tile tile = null;
+            while (quota > 0) {
+                synchronized (TileImageView.this) {
+                    tile = mUploadQueue.pop();
+                }
+                if (tile == null) break;
+                if (!tile.isContentValid()) {
+                    boolean hasBeenLoaded = tile.isLoaded();
+                    Utils.assertTrue(tile.mTileState == STATE_DECODED);
+                    tile.updateContent(canvas);
+                    if (!hasBeenLoaded) tile.draw(canvas, 0, 0);
+                    --quota;
+                }
+            }
+            if (tile == null) mActive.set(false);
+            return tile != null;
+        }
+    }
+
+    // Draw the tile to a square at canvas that locates at (x, y) and
+    // has a side length of length.
+    public void drawTile(GLCanvas canvas,
+            int tx, int ty, int level, float x, float y, float length) {
+        RectF source = mSourceRect;
+        RectF target = mTargetRect;
+        target.set(x, y, x + length, y + length);
+        source.set(0, 0, sTileSize, sTileSize);
+
+        Tile tile = getTile(tx, ty, level);
+        if (tile != null) {
+            if (!tile.isContentValid()) {
+                if (tile.mTileState == STATE_DECODED) {
+                    if (mUploadQuota > 0) {
+                        --mUploadQuota;
+                        tile.updateContent(canvas);
+                    } else {
+                        mRenderComplete = false;
+                    }
+                } else if (tile.mTileState != STATE_DECODE_FAIL){
+                    mRenderComplete = false;
+                    queueForDecode(tile);
+                }
+            }
+            if (drawTile(tile, canvas, source, target)) return;
+        }
+        if (mScreenNail != null) {
+            int size = sTileSize << level;
+            float scaleX = (float) mScreenNail.getWidth() / mImageWidth;
+            float scaleY = (float) mScreenNail.getHeight() / mImageHeight;
+            source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+                    (ty + size) * scaleY);
+            mScreenNail.draw(canvas, source, target);
+        }
+    }
+
+    static boolean drawTile(
+            Tile tile, GLCanvas canvas, RectF source, RectF target) {
+        while (true) {
+            if (tile.isContentValid()) {
+                canvas.drawTexture(tile, source, target);
+                return true;
+            }
+
+            // Parent can be divided to four quads and tile is one of the four.
+            Tile parent = tile.getParentTile();
+            if (parent == null) return false;
+            if (tile.mX == parent.mX) {
+                source.left /= 2f;
+                source.right /= 2f;
+            } else {
+                source.left = (sTileSize + source.left) / 2f;
+                source.right = (sTileSize + source.right) / 2f;
+            }
+            if (tile.mY == parent.mY) {
+                source.top /= 2f;
+                source.bottom /= 2f;
+            } else {
+                source.top = (sTileSize + source.top) / 2f;
+                source.bottom = (sTileSize + source.bottom) / 2f;
+            }
+            tile = parent;
+        }
+    }
+
+    private class Tile extends UploadedTexture {
+        public int mX;
+        public int mY;
+        public int mTileLevel;
+        public Tile mNext;
+        public Bitmap mDecodedTile;
+        public volatile int mTileState = STATE_ACTIVATED;
+
+        public Tile(int x, int y, int level) {
+            mX = x;
+            mY = y;
+            mTileLevel = level;
+        }
+
+        @Override
+        protected void onFreeBitmap(Bitmap bitmap) {
+            GalleryBitmapPool.getInstance().put(bitmap);
+        }
+
+        boolean decode() {
+            // Get a tile from the original image. The tile is down-scaled
+            // by (1 << mTilelevel) from a region in the original image.
+            try {
+                mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile(
+                        mTileLevel, mX, mY, sTileSize));
+            } catch (Throwable t) {
+                Log.w(TAG, "fail to decode tile", t);
+            }
+            return mDecodedTile != null;
+        }
+
+        @Override
+        protected Bitmap onGetBitmap() {
+            Utils.assertTrue(mTileState == STATE_DECODED);
+
+            // We need to override the width and height, so that we won't
+            // draw beyond the boundaries.
+            int rightEdge = ((mImageWidth - mX) >> mTileLevel);
+            int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
+            setSize(Math.min(sTileSize, rightEdge), Math.min(sTileSize, bottomEdge));
+
+            Bitmap bitmap = mDecodedTile;
+            mDecodedTile = null;
+            mTileState = STATE_ACTIVATED;
+            return bitmap;
+        }
+
+        // We override getTextureWidth() and getTextureHeight() here, so the
+        // texture can be re-used for different tiles regardless of the actual
+        // size of the tile (which may be small because it is a tile at the
+        // boundary).
+        @Override
+        public int getTextureWidth() {
+            return sTileSize;
+        }
+
+        @Override
+        public int getTextureHeight() {
+            return sTileSize;
+        }
+
+        public void update(int x, int y, int level) {
+            mX = x;
+            mY = y;
+            mTileLevel = level;
+            invalidateContent();
+        }
+
+        public Tile getParentTile() {
+            if (mTileLevel + 1 == mLevelCount) return null;
+            int size = sTileSize << (mTileLevel + 1);
+            int x = size * (mX / size);
+            int y = size * (mY / size);
+            return getTile(x, y, mTileLevel + 1);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("tile(%s, %s, %s / %s)",
+                    mX / sTileSize, mY / sTileSize, mLevel, mLevelCount);
+        }
+    }
+
+    private static class TileQueue {
+        private Tile mHead;
+
+        public Tile pop() {
+            Tile tile = mHead;
+            if (tile != null) mHead = tile.mNext;
+            return tile;
+        }
+
+        public boolean push(Tile tile) {
+            boolean wasEmpty = mHead == null;
+            tile.mNext = mHead;
+            mHead = tile;
+            return wasEmpty;
+        }
+
+        public void clean() {
+            mHead = null;
+        }
+    }
+
+    private class TileDecoder implements ThreadPool.Job<Void> {
+
+        private CancelListener mNotifier = new CancelListener() {
+            @Override
+            public void onCancel() {
+                synchronized (TileImageView.this) {
+                    TileImageView.this.notifyAll();
+                }
+            }
+        };
+
+        @Override
+        public Void run(JobContext jc) {
+            jc.setMode(ThreadPool.MODE_NONE);
+            jc.setCancelListener(mNotifier);
+            while (!jc.isCancelled()) {
+                Tile tile = null;
+                synchronized(TileImageView.this) {
+                    tile = mDecodeQueue.pop();
+                    if (tile == null && !jc.isCancelled()) {
+                        Utils.waitWithoutInterrupt(TileImageView.this);
+                    }
+                }
+                if (tile == null) continue;
+                if (decodeTile(tile)) queueForUpload(tile);
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
new file mode 100644
index 0000000..0c1f66d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class TileImageViewAdapter implements TileImageView.TileSource {
+    private static final String TAG = "TileImageViewAdapter";
+    protected ScreenNail mScreenNail;
+    protected boolean mOwnScreenNail;
+    protected BitmapRegionDecoder mRegionDecoder;
+    protected int mImageWidth;
+    protected int mImageHeight;
+    protected int mLevelCount;
+
+    public TileImageViewAdapter() {
+    }
+
+    public synchronized void clear() {
+        mScreenNail = null;
+        mImageWidth = 0;
+        mImageHeight = 0;
+        mLevelCount = 0;
+        mRegionDecoder = null;
+    }
+
+    // Caller is responsible to recycle the ScreenNail
+    public synchronized void setScreenNail(
+            ScreenNail screenNail, int width, int height) {
+        Utils.checkNotNull(screenNail);
+        mScreenNail = screenNail;
+        mImageWidth = width;
+        mImageHeight = height;
+        mRegionDecoder = null;
+        mLevelCount = 0;
+    }
+
+    public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
+        mRegionDecoder = Utils.checkNotNull(decoder);
+        mImageWidth = decoder.getWidth();
+        mImageHeight = decoder.getHeight();
+        mLevelCount = calculateLevelCount();
+    }
+
+    private int calculateLevelCount() {
+        return Math.max(0, Utils.ceilLog2(
+                (float) mImageWidth / mScreenNail.getWidth()));
+    }
+
+    // Gets a sub image on a rectangle of the current photo. For example,
+    // getTile(1, 50, 50, 100, 3, pool) means to get the region located
+    // at (50, 50) with sample level 1 (ie, down sampled by 2^1) and the
+    // target tile size (after sampling) 100 with border 3.
+    //
+    // From this spec, we can infer the actual tile size to be
+    // 100 + 3x2 = 106, and the size of the region to be extracted from the
+    // photo to be 200 with border 6.
+    //
+    // As a result, we should decode region (50-6, 50-6, 250+6, 250+6) or
+    // (44, 44, 256, 256) from the original photo and down sample it to 106.
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    @Override
+    public Bitmap getTile(int level, int x, int y, int tileSize) {
+        if (!ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER) {
+            return getTileWithoutReusingBitmap(level, x, y, tileSize);
+        }
+
+        int t = tileSize << level;
+
+        Rect wantRegion = new Rect(x, y, x + t, y + t);
+
+        boolean needClear;
+        BitmapRegionDecoder regionDecoder = null;
+
+        synchronized (this) {
+            regionDecoder = mRegionDecoder;
+            if (regionDecoder == null) return null;
+
+            // We need to clear a reused bitmap, if wantRegion is not fully
+            // within the image.
+            needClear = !new Rect(0, 0, mImageWidth, mImageHeight)
+                    .contains(wantRegion);
+        }
+
+        Bitmap bitmap = GalleryBitmapPool.getInstance().get(tileSize, tileSize);
+        if (bitmap != null) {
+            if (needClear) bitmap.eraseColor(0);
+        } else {
+            bitmap = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+        }
+
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Config.ARGB_8888;
+        options.inPreferQualityOverSpeed = true;
+        options.inSampleSize =  (1 << level);
+        options.inBitmap = bitmap;
+
+        try {
+            // In CropImage, we may call the decodeRegion() concurrently.
+            synchronized (regionDecoder) {
+                bitmap = regionDecoder.decodeRegion(wantRegion, options);
+            }
+        } finally {
+            if (options.inBitmap != bitmap && options.inBitmap != null) {
+                GalleryBitmapPool.getInstance().put(options.inBitmap);
+                options.inBitmap = null;
+            }
+        }
+
+        if (bitmap == null) {
+            Log.w(TAG, "fail in decoding region");
+        }
+        return bitmap;
+    }
+
+    private Bitmap getTileWithoutReusingBitmap(
+            int level, int x, int y, int tileSize) {
+        int t = tileSize << level;
+        Rect wantRegion = new Rect(x, y, x + t, y + t);
+
+        BitmapRegionDecoder regionDecoder;
+        Rect overlapRegion;
+
+        synchronized (this) {
+            regionDecoder = mRegionDecoder;
+            if (regionDecoder == null) return null;
+            overlapRegion = new Rect(0, 0, mImageWidth, mImageHeight);
+            Utils.assertTrue(overlapRegion.intersect(wantRegion));
+        }
+
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Config.ARGB_8888;
+        options.inPreferQualityOverSpeed = true;
+        options.inSampleSize =  (1 << level);
+        Bitmap bitmap = null;
+
+        // In CropImage, we may call the decodeRegion() concurrently.
+        synchronized (regionDecoder) {
+            bitmap = regionDecoder.decodeRegion(overlapRegion, options);
+        }
+
+        if (bitmap == null) {
+            Log.w(TAG, "fail in decoding region");
+        }
+
+        if (wantRegion.equals(overlapRegion)) return bitmap;
+
+        Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+        Canvas canvas = new Canvas(result);
+        canvas.drawBitmap(bitmap,
+                (overlapRegion.left - wantRegion.left) >> level,
+                (overlapRegion.top - wantRegion.top) >> level, null);
+        return result;
+    }
+
+
+    @Override
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
+    }
+
+    @Override
+    public int getImageHeight() {
+        return mImageHeight;
+    }
+
+    @Override
+    public int getImageWidth() {
+        return mImageWidth;
+    }
+
+    @Override
+    public int getLevelCount() {
+        return mLevelCount;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/TiledScreenNail.java b/src/com/android/gallery3d/ui/TiledScreenNail.java
new file mode 100644
index 0000000..860e230
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TiledScreenNail.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.RectF;
+
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.TiledTexture;
+
+// This is a ScreenNail wraps a Bitmap. There are some extra functions:
+//
+// - If we need to draw before the bitmap is available, we draw a rectange of
+// placeholder color (gray).
+//
+// - When the the bitmap is available, and we have drawn the placeholder color
+// before, we will do a fade-in animation.
+public class TiledScreenNail implements ScreenNail {
+    @SuppressWarnings("unused")
+    private static final String TAG = "TiledScreenNail";
+
+    // The duration of the fading animation in milliseconds
+    private static final int DURATION = 180;
+
+    private static int sMaxSide = 640;
+
+    // These are special values for mAnimationStartTime
+    private static final long ANIMATION_NOT_NEEDED = -1;
+    private static final long ANIMATION_NEEDED = -2;
+    private static final long ANIMATION_DONE = -3;
+
+    private int mWidth;
+    private int mHeight;
+    private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
+
+    private Bitmap mBitmap;
+    private TiledTexture mTexture;
+
+    public TiledScreenNail(Bitmap bitmap) {
+        mWidth = bitmap.getWidth();
+        mHeight = bitmap.getHeight();
+        mBitmap = bitmap;
+        mTexture = new TiledTexture(bitmap);
+    }
+
+    public TiledScreenNail(int width, int height) {
+        setSize(width, height);
+    }
+
+    // This gets overridden by bitmap_screennail_placeholder
+    // in GalleryUtils.initialize
+    private static int mPlaceholderColor = 0xFF222222;
+    private static boolean mDrawPlaceholder = true;
+
+    public static void setPlaceholderColor(int color) {
+        mPlaceholderColor = color;
+    }
+
+    private void setSize(int width, int height) {
+        if (width == 0 || height == 0) {
+            width = sMaxSide;
+            height = sMaxSide * 3 / 4;
+        }
+        float scale = Math.min(1, (float) sMaxSide / Math.max(width, height));
+        mWidth = Math.round(scale * width);
+        mHeight = Math.round(scale * height);
+    }
+
+    // Combines the two ScreenNails.
+    // Returns the used one and recycle the unused one.
+    public ScreenNail combine(ScreenNail other) {
+        if (other == null) {
+            return this;
+        }
+
+        if (!(other instanceof TiledScreenNail)) {
+            recycle();
+            return other;
+        }
+
+        // Now both are TiledScreenNail. Move over the information about width,
+        // height, and Bitmap, then recycle the other.
+        TiledScreenNail newer = (TiledScreenNail) other;
+        mWidth = newer.mWidth;
+        mHeight = newer.mHeight;
+        if (newer.mTexture != null) {
+            if (mBitmap != null) GalleryBitmapPool.getInstance().put(mBitmap);
+            if (mTexture != null) mTexture.recycle();
+            mBitmap = newer.mBitmap;
+            mTexture = newer.mTexture;
+            newer.mBitmap = null;
+            newer.mTexture = null;
+        }
+        newer.recycle();
+        return this;
+    }
+
+    public void updatePlaceholderSize(int width, int height) {
+        if (mBitmap != null) return;
+        if (width == 0 || height == 0) return;
+        setSize(width, height);
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public void noDraw() {
+    }
+
+    @Override
+    public void recycle() {
+        if (mTexture != null) {
+            mTexture.recycle();
+            mTexture = null;
+        }
+        if (mBitmap != null) {
+            GalleryBitmapPool.getInstance().put(mBitmap);
+            mBitmap = null;
+        }
+    }
+
+    public static void disableDrawPlaceholder() {
+        mDrawPlaceholder = false;
+    }
+
+    public static void enableDrawPlaceholder() {
+        mDrawPlaceholder = true;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        if (mTexture == null || !mTexture.isReady()) {
+            if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
+                mAnimationStartTime = ANIMATION_NEEDED;
+            }
+            if(mDrawPlaceholder) {
+                canvas.fillRect(x, y, width, height, mPlaceholderColor);
+            }
+            return;
+        }
+
+        if (mAnimationStartTime == ANIMATION_NEEDED) {
+            mAnimationStartTime = AnimationTime.get();
+        }
+
+        if (isAnimating()) {
+            mTexture.drawMixed(canvas, mPlaceholderColor, getRatio(), x, y,
+                    width, height);
+        } else {
+            mTexture.draw(canvas, x, y, width, height);
+        }
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        if (mTexture == null || !mTexture.isReady()) {
+            canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
+                    mPlaceholderColor);
+            return;
+        }
+
+        mTexture.draw(canvas, source, dest);
+    }
+
+    public boolean isAnimating() {
+        // The TiledTexture may not be uploaded completely yet.
+        // In that case, we count it as animating state and we will draw
+        // the placeholder in TileImageView.
+        if (mTexture == null || !mTexture.isReady()) return true;
+        if (mAnimationStartTime < 0) return false;
+        if (AnimationTime.get() - mAnimationStartTime >= DURATION) {
+            mAnimationStartTime = ANIMATION_DONE;
+            return false;
+        }
+        return true;
+    }
+
+    private float getRatio() {
+        float r = (float) (AnimationTime.get() - mAnimationStartTime) / DURATION;
+        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+    }
+
+    public boolean isShowingPlaceholder() {
+        return (mBitmap == null) || isAnimating();
+    }
+
+    public TiledTexture getTexture() {
+        return mTexture;
+    }
+
+    public static void setMaxSide(int size) {
+        sMaxSide = size;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/UndoBarView.java b/src/com/android/gallery3d/ui/UndoBarView.java
new file mode 100644
index 0000000..42f12ae
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UndoBarView.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.content.Context;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.util.GalleryUtils;
+
+public class UndoBarView extends GLView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "UndoBarView";
+
+    private static final int WHITE = 0xFFFFFFFF;
+    private static final int GRAY = 0xFFAAAAAA;
+
+    private final NinePatchTexture mPanel;
+    private final StringTexture mUndoText;
+    private final StringTexture mDeletedText;
+    private final ResourceTexture mUndoIcon;
+    private final int mBarHeight;
+    private final int mBarMargin;
+    private final int mUndoTextMargin;
+    private final int mIconSize;
+    private final int mIconMargin;
+    private final int mSeparatorTopMargin;
+    private final int mSeparatorBottomMargin;
+    private final int mSeparatorRightMargin;
+    private final int mSeparatorWidth;
+    private final int mDeletedTextMargin;
+    private final int mClickRegion;
+
+    private OnClickListener mOnClickListener;
+    private boolean mDownOnButton;
+
+    // This is the layout of UndoBarView. The unit is dp.
+    //
+    //    +-+----+----------------+-+--+----+-+------+--+-+
+    // 48 | |    | Deleted        | |  | <- | | UNDO |  | |
+    //    +-+----+----------------+-+--+----+-+------+--+-+
+    //     4  16                   1 12  32  8        16 4
+    public UndoBarView(Context context) {
+        mBarHeight = GalleryUtils.dpToPixel(48);
+        mBarMargin = GalleryUtils.dpToPixel(4);
+        mUndoTextMargin = GalleryUtils.dpToPixel(16);
+        mIconMargin = GalleryUtils.dpToPixel(8);
+        mIconSize = GalleryUtils.dpToPixel(32);
+        mSeparatorRightMargin = GalleryUtils.dpToPixel(12);
+        mSeparatorTopMargin = GalleryUtils.dpToPixel(10);
+        mSeparatorBottomMargin = GalleryUtils.dpToPixel(10);
+        mSeparatorWidth = GalleryUtils.dpToPixel(1);
+        mDeletedTextMargin = GalleryUtils.dpToPixel(16);
+
+        mPanel = new NinePatchTexture(context, R.drawable.panel_undo_holo);
+        mUndoText = StringTexture.newInstance(context.getString(R.string.undo),
+                GalleryUtils.dpToPixel(12), GRAY, 0, true);
+        mDeletedText = StringTexture.newInstance(
+                context.getString(R.string.deleted),
+                GalleryUtils.dpToPixel(16), WHITE);
+        mUndoIcon = new ResourceTexture(
+                context, R.drawable.ic_menu_revert_holo_dark);
+        mClickRegion = mBarMargin + mUndoTextMargin + mUndoText.getWidth()
+                + mIconMargin + mIconSize + mSeparatorRightMargin;
+    }
+
+    public void setOnClickListener(OnClickListener listener) {
+        mOnClickListener = listener;
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        setMeasuredSize(0 /* unused */, mBarHeight);
+    }
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        super.render(canvas);
+        advanceAnimation();
+
+        canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+        canvas.multiplyAlpha(mAlpha);
+
+        int w = getWidth();
+        int h = getHeight();
+        mPanel.draw(canvas, mBarMargin, 0, w - mBarMargin * 2, mBarHeight);
+
+        int x = w - mBarMargin;
+        int y;
+
+        x -= mUndoTextMargin + mUndoText.getWidth();
+        y = (mBarHeight - mUndoText.getHeight()) / 2;
+        mUndoText.draw(canvas, x, y);
+
+        x -= mIconMargin + mIconSize;
+        y = (mBarHeight - mIconSize) / 2;
+        mUndoIcon.draw(canvas, x, y, mIconSize, mIconSize);
+
+        x -= mSeparatorRightMargin + mSeparatorWidth;
+        y = mSeparatorTopMargin;
+        canvas.fillRect(x, y, mSeparatorWidth,
+                mBarHeight - mSeparatorTopMargin - mSeparatorBottomMargin, GRAY);
+
+        x = mBarMargin + mDeletedTextMargin;
+        y = (mBarHeight - mDeletedText.getHeight()) / 2;
+        mDeletedText.draw(canvas, x, y);
+
+        canvas.restore();
+    }
+
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mDownOnButton = inUndoButton(event);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mDownOnButton) {
+                    if (mOnClickListener != null && inUndoButton(event)) {
+                        mOnClickListener.onClick(this);
+                    }
+                    mDownOnButton = false;
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                mDownOnButton = false;
+                break;
+        }
+        return true;
+    }
+
+    // Check if the event is on the right of the separator
+    private boolean inUndoButton(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        int w = getWidth();
+        int h = getHeight();
+        return (x >= w - mClickRegion && x < w && y >= 0 && y < h);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Alpha Animation
+    ////////////////////////////////////////////////////////////////////////////
+
+    private static final long NO_ANIMATION = -1;
+    private static long ANIM_TIME = 200;
+    private long mAnimationStartTime = NO_ANIMATION;
+    private float mFromAlpha, mToAlpha;
+    private float mAlpha;
+
+    private static float getTargetAlpha(int visibility) {
+        return (visibility == VISIBLE) ? 1f : 0f;
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        mAlpha = getTargetAlpha(visibility);
+        mAnimationStartTime = NO_ANIMATION;
+        super.setVisibility(visibility);
+        invalidate();
+    }
+
+    public void animateVisibility(int visibility) {
+        float target = getTargetAlpha(visibility);
+        if (mAnimationStartTime == NO_ANIMATION && mAlpha == target) return;
+        if (mAnimationStartTime != NO_ANIMATION && mToAlpha == target) return;
+
+        mFromAlpha = mAlpha;
+        mToAlpha = target;
+        mAnimationStartTime = AnimationTime.startTime();
+
+        super.setVisibility(VISIBLE);
+        invalidate();
+    }
+
+    private void advanceAnimation() {
+        if (mAnimationStartTime == NO_ANIMATION) return;
+
+        float delta = (float) (AnimationTime.get() - mAnimationStartTime) /
+                ANIM_TIME;
+        mAlpha = mFromAlpha + ((mToAlpha > mFromAlpha) ? delta : -delta);
+        mAlpha = Utils.clamp(mAlpha, 0f, 1f);
+
+        if (mAlpha == mToAlpha) {
+            mAnimationStartTime = NO_ANIMATION;
+            if (mAlpha == 0) {
+                super.setVisibility(INVISIBLE);
+            }
+        }
+        invalidate();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java
new file mode 100644
index 0000000..bc4a718
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UserInteractionListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+public interface UserInteractionListener {
+    // Called when a user interaction begins (for example, fling).
+    public void onUserInteractionBegin();
+    // Called when the user interaction ends.
+    public void onUserInteractionEnd();
+    // Other one-shot user interactions.
+    public void onUserInteraction();
+}
diff --git a/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java
new file mode 100644
index 0000000..ee61d8e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.PowerManager;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+
+public class WakeLockHoldingProgressListener implements MenuExecutor.ProgressListener {
+    static private final String DEFAULT_WAKE_LOCK_LABEL = "Gallery Progress Listener";
+    private AbstractGalleryActivity mActivity;
+    private PowerManager.WakeLock mWakeLock;
+
+    public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity) {
+        this(galleryActivity, DEFAULT_WAKE_LOCK_LABEL);
+    }
+
+    public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity, String label) {
+        mActivity = galleryActivity;
+        PowerManager pm =
+                (PowerManager) ((Activity) mActivity).getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, label);
+    }
+
+    @Override
+    public void onProgressComplete(int result) {
+        mWakeLock.release();
+    }
+
+    @Override
+    public void onProgressStart() {
+        mWakeLock.acquire();
+    }
+
+    protected AbstractGalleryActivity getActivity() {
+        return mActivity;
+    }
+
+    @Override
+    public void onProgressUpdate(int index) {
+    }
+
+    @Override
+    public void onConfirmDialogDismissed(boolean confirmed) {
+    }
+
+    @Override
+    public void onConfirmDialogShown() {
+    }
+}
diff --git a/src/com/android/gallery3d/util/AccessibilityUtils.java b/src/com/android/gallery3d/util/AccessibilityUtils.java
new file mode 100644
index 0000000..9df8e4e
--- /dev/null
+++ b/src/com/android/gallery3d/util/AccessibilityUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import android.content.Context;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * AccessibilityUtils provides functions needed in accessibility mode. All the functions
+ * in this class are made compatible with gingerbread and later API's
+*/
+public class AccessibilityUtils {
+    public static void makeAnnouncement(View view, CharSequence announcement) {
+        if (view == null)
+            return;
+        if (ApiHelper.HAS_ANNOUNCE_FOR_ACCESSIBILITY) {
+            view.announceForAccessibility(announcement);
+        } else {
+            // For API 15 and earlier, we need to construct an accessibility event
+            Context ctx = view.getContext();
+            AccessibilityManager am = (AccessibilityManager) ctx.getSystemService(
+                    Context.ACCESSIBILITY_SERVICE);
+            if (!am.isEnabled()) return;
+            AccessibilityEvent event = AccessibilityEvent.obtain(
+                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+            AccessibilityRecordCompat arc = new AccessibilityRecordCompat(event);
+            arc.setSource(view);
+            event.setClassName(view.getClass().getName());
+            event.setPackageName(view.getContext().getPackageName());
+            event.setEnabled(view.isEnabled());
+            event.getText().add(announcement);
+            am.sendAccessibilityEvent(event);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/util/BucketNames.java b/src/com/android/gallery3d/util/BucketNames.java
new file mode 100644
index 0000000..990dc82
--- /dev/null
+++ b/src/com/android/gallery3d/util/BucketNames.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+/**
+ * Bucket names for buckets that are created and used in the Gallery.
+ */
+public class BucketNames {
+
+    public static final String CAMERA = "DCIM/Camera";
+    public static final String IMPORTED = "Imported";
+    public static final String DOWNLOAD = "download";
+    public static final String EDITED_ONLINE_PHOTOS = "EditedOnlinePhotos";
+    public static final String SCREENSHOTS = "Pictures/Screenshots";
+}
diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java
new file mode 100644
index 0000000..ba466f7
--- /dev/null
+++ b/src/com/android/gallery3d/util/CacheManager.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.common.BlobCache;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+public class CacheManager {
+    private static final String TAG = "CacheManager";
+    private static final String KEY_CACHE_UP_TO_DATE = "cache-up-to-date";
+    private static HashMap<String, BlobCache> sCacheMap =
+            new HashMap<String, BlobCache>();
+    private static boolean sOldCheckDone = false;
+
+    // Return null when we cannot instantiate a BlobCache, e.g.:
+    // there is no SD card found.
+    // This can only be called from data thread.
+    public static BlobCache getCache(Context context, String filename,
+            int maxEntries, int maxBytes, int version) {
+        synchronized (sCacheMap) {
+            if (!sOldCheckDone) {
+                removeOldFilesIfNecessary(context);
+                sOldCheckDone = true;
+            }
+            BlobCache cache = sCacheMap.get(filename);
+            if (cache == null) {
+                File cacheDir = context.getExternalCacheDir();
+                String path = cacheDir.getAbsolutePath() + "/" + filename;
+                try {
+                    cache = new BlobCache(path, maxEntries, maxBytes, false,
+                            version);
+                    sCacheMap.put(filename, cache);
+                } catch (IOException e) {
+                    Log.e(TAG, "Cannot instantiate cache!", e);
+                }
+            }
+            return cache;
+        }
+    }
+
+    // Removes the old files if the data is wiped.
+    private static void removeOldFilesIfNecessary(Context context) {
+        SharedPreferences pref = PreferenceManager
+                .getDefaultSharedPreferences(context);
+        int n = 0;
+        try {
+            n = pref.getInt(KEY_CACHE_UP_TO_DATE, 0);
+        } catch (Throwable t) {
+            // ignore.
+        }
+        if (n != 0) return;
+        pref.edit().putInt(KEY_CACHE_UP_TO_DATE, 1).commit();
+
+        File cacheDir = context.getExternalCacheDir();
+        String prefix = cacheDir.getAbsolutePath() + "/";
+
+        BlobCache.deleteFiles(prefix + "imgcache");
+        BlobCache.deleteFiles(prefix + "rev_geocoding");
+        BlobCache.deleteFiles(prefix + "bookmark");
+    }
+}
diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
new file mode 100644
index 0000000..9245e2c
--- /dev/null
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import android.annotation.TargetApi;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.Environment;
+import android.os.StatFs;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.PackagesMonitor;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.TiledScreenNail;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+public class GalleryUtils {
+    private static final String TAG = "GalleryUtils";
+    private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
+    private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity";
+    private static final String CAMERA_LAUNCHER_NAME = "com.android.camera.CameraLauncher";
+
+    public static final String MIME_TYPE_IMAGE = "image/*";
+    public static final String MIME_TYPE_VIDEO = "video/*";
+    public static final String MIME_TYPE_PANORAMA360 = "application/vnd.google.panorama360+jpg";
+    public static final String MIME_TYPE_ALL = "*/*";
+
+    private static final String DIR_TYPE_IMAGE = "vnd.android.cursor.dir/image";
+    private static final String DIR_TYPE_VIDEO = "vnd.android.cursor.dir/video";
+
+    private static final String PREFIX_PHOTO_EDITOR_UPDATE = "editor-update-";
+    private static final String PREFIX_HAS_PHOTO_EDITOR = "has-editor-";
+
+    private static final String KEY_CAMERA_UPDATE = "camera-update";
+    private static final String KEY_HAS_CAMERA = "has-camera";
+
+    private static float sPixelDensity = -1f;
+    private static boolean sCameraAvailableInitialized = false;
+    private static boolean sCameraAvailable;
+
+    public static void initialize(Context context) {
+        DisplayMetrics metrics = new DisplayMetrics();
+        WindowManager wm = (WindowManager)
+                context.getSystemService(Context.WINDOW_SERVICE);
+        wm.getDefaultDisplay().getMetrics(metrics);
+        sPixelDensity = metrics.density;
+        Resources r = context.getResources();
+        TiledScreenNail.setPlaceholderColor(r.getColor(
+                R.color.bitmap_screennail_placeholder));
+        initializeThumbnailSizes(metrics, r);
+    }
+
+    private static void initializeThumbnailSizes(DisplayMetrics metrics, Resources r) {
+        int maxPixels = Math.max(metrics.heightPixels, metrics.widthPixels);
+
+        // For screen-nails, we never need to completely fill the screen
+        MediaItem.setThumbnailSizes(maxPixels / 2, maxPixels / 5);
+        TiledScreenNail.setMaxSide(maxPixels / 2);
+    }
+
+    public static float[] intColorToFloatARGBArray(int from) {
+        return new float[] {
+            Color.alpha(from) / 255f,
+            Color.red(from) / 255f,
+            Color.green(from) / 255f,
+            Color.blue(from) / 255f
+        };
+    }
+
+    public static float dpToPixel(float dp) {
+        return sPixelDensity * dp;
+    }
+
+    public static int dpToPixel(int dp) {
+        return Math.round(dpToPixel((float) dp));
+    }
+
+    public static int meterToPixel(float meter) {
+        // 1 meter = 39.37 inches, 1 inch = 160 dp.
+        return Math.round(dpToPixel(meter * 39.37f * 160));
+    }
+
+    public static byte[] getBytes(String in) {
+        byte[] result = new byte[in.length() * 2];
+        int output = 0;
+        for (char ch : in.toCharArray()) {
+            result[output++] = (byte) (ch & 0xFF);
+            result[output++] = (byte) (ch >> 8);
+        }
+        return result;
+    }
+
+    // Below are used the detect using database in the render thread. It only
+    // works most of the time, but that's ok because it's for debugging only.
+
+    private static volatile Thread sCurrentThread;
+    private static volatile boolean sWarned;
+
+    public static void setRenderThread() {
+        sCurrentThread = Thread.currentThread();
+    }
+
+    public static void assertNotInRenderThread() {
+        if (!sWarned) {
+            if (Thread.currentThread() == sCurrentThread) {
+                sWarned = true;
+                Log.w(TAG, new Throwable("Should not do this in render thread"));
+            }
+        }
+    }
+
+    private static final double RAD_PER_DEG = Math.PI / 180.0;
+    private static final double EARTH_RADIUS_METERS = 6367000.0;
+
+    public static double fastDistanceMeters(double latRad1, double lngRad1,
+            double latRad2, double lngRad2) {
+       if ((Math.abs(latRad1 - latRad2) > RAD_PER_DEG)
+             || (Math.abs(lngRad1 - lngRad2) > RAD_PER_DEG)) {
+           return accurateDistanceMeters(latRad1, lngRad1, latRad2, lngRad2);
+       }
+       // Approximate sin(x) = x.
+       double sineLat = (latRad1 - latRad2);
+
+       // Approximate sin(x) = x.
+       double sineLng = (lngRad1 - lngRad2);
+
+       // Approximate cos(lat1) * cos(lat2) using
+       // cos((lat1 + lat2)/2) ^ 2
+       double cosTerms = Math.cos((latRad1 + latRad2) / 2.0);
+       cosTerms = cosTerms * cosTerms;
+       double trigTerm = sineLat * sineLat + cosTerms * sineLng * sineLng;
+       trigTerm = Math.sqrt(trigTerm);
+
+       // Approximate arcsin(x) = x
+       return EARTH_RADIUS_METERS * trigTerm;
+    }
+
+    public static double accurateDistanceMeters(double lat1, double lng1,
+            double lat2, double lng2) {
+        double dlat = Math.sin(0.5 * (lat2 - lat1));
+        double dlng = Math.sin(0.5 * (lng2 - lng1));
+        double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2);
+        return (2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0,
+                1.0 - x)))) * EARTH_RADIUS_METERS;
+    }
+
+
+    public static final double toMile(double meter) {
+        return meter / 1609;
+    }
+
+    // For debugging, it will block the caller for timeout millis.
+    public static void fakeBusy(JobContext jc, int timeout) {
+        final ConditionVariable cv = new ConditionVariable();
+        jc.setCancelListener(new CancelListener() {
+            @Override
+            public void onCancel() {
+                cv.open();
+            }
+        });
+        cv.block(timeout);
+        jc.setCancelListener(null);
+    }
+
+    public static boolean isEditorAvailable(Context context, String mimeType) {
+        int version = PackagesMonitor.getPackagesVersion(context);
+
+        String updateKey = PREFIX_PHOTO_EDITOR_UPDATE + mimeType;
+        String hasKey = PREFIX_HAS_PHOTO_EDITOR + mimeType;
+
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        if (prefs.getInt(updateKey, 0) != version) {
+            PackageManager packageManager = context.getPackageManager();
+            List<ResolveInfo> infos = packageManager.queryIntentActivities(
+                    new Intent(Intent.ACTION_EDIT).setType(mimeType), 0);
+            prefs.edit().putInt(updateKey, version)
+                        .putBoolean(hasKey, !infos.isEmpty())
+                        .commit();
+        }
+
+        return prefs.getBoolean(hasKey, true);
+    }
+
+    public static boolean isAnyCameraAvailable(Context context) {
+        int version = PackagesMonitor.getPackagesVersion(context);
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        if (prefs.getInt(KEY_CAMERA_UPDATE, 0) != version) {
+            PackageManager packageManager = context.getPackageManager();
+            List<ResolveInfo> infos = packageManager.queryIntentActivities(
+                    new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), 0);
+            prefs.edit().putInt(KEY_CAMERA_UPDATE, version)
+                        .putBoolean(KEY_HAS_CAMERA, !infos.isEmpty())
+                        .commit();
+        }
+        return prefs.getBoolean(KEY_HAS_CAMERA, true);
+    }
+
+    public static boolean isCameraAvailable(Context context) {
+        if (sCameraAvailableInitialized) return sCameraAvailable;
+        PackageManager pm = context.getPackageManager();
+        ComponentName name = new ComponentName(context, CAMERA_LAUNCHER_NAME);
+        int state = pm.getComponentEnabledSetting(name);
+        sCameraAvailableInitialized = true;
+        sCameraAvailable =
+            (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+             || (state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+        return sCameraAvailable;
+    }
+
+    public static void startCameraActivity(Context context) {
+        Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+                        | Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+    }
+
+    public static void startGalleryActivity(Context context) {
+        Intent intent = new Intent(context, Gallery.class)
+                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+                | Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+    }
+
+    public static boolean isValidLocation(double latitude, double longitude) {
+        // TODO: change || to && after we fix the default location issue
+        return (latitude != MediaItem.INVALID_LATLNG || longitude != MediaItem.INVALID_LATLNG);
+    }
+
+    public static String formatLatitudeLongitude(String format, double latitude,
+            double longitude) {
+        // We need to specify the locale otherwise it may go wrong in some language
+        // (e.g. Locale.FRENCH)
+        return String.format(Locale.ENGLISH, format, latitude, longitude);
+    }
+
+    public static void showOnMap(Context context, double latitude, double longitude) {
+        try {
+            // We don't use "geo:latitude,longitude" because it only centers
+            // the MapView to the specified location, but we need a marker
+            // for further operations (routing to/from).
+            // The q=(lat, lng) syntax is suggested by geo-team.
+            String uri = formatLatitudeLongitude("http://maps.google.com/maps?f=q&q=(%f,%f)",
+                    latitude, longitude);
+            ComponentName compName = new ComponentName(MAPS_PACKAGE_NAME,
+                    MAPS_CLASS_NAME);
+            Intent mapsIntent = new Intent(Intent.ACTION_VIEW,
+                    Uri.parse(uri)).setComponent(compName);
+            context.startActivity(mapsIntent);
+        } catch (ActivityNotFoundException e) {
+            // Use the "geo intent" if no GMM is installed
+            Log.e(TAG, "GMM activity not found!", e);
+            String url = formatLatitudeLongitude("geo:%f,%f", latitude, longitude);
+            Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+            context.startActivity(mapsIntent);
+        }
+    }
+
+    public static void setViewPointMatrix(
+            float matrix[], float x, float y, float z) {
+        // The matrix is
+        // -z,  0,  x,  0
+        //  0, -z,  y,  0
+        //  0,  0,  1,  0
+        //  0,  0,  1, -z
+        Arrays.fill(matrix, 0, 16, 0);
+        matrix[0] = matrix[5] = matrix[15] = -z;
+        matrix[8] = x;
+        matrix[9] = y;
+        matrix[10] = matrix[11] = 1;
+    }
+
+    public static int getBucketId(String path) {
+        return path.toLowerCase().hashCode();
+    }
+
+    // Return the local path that matches the given bucketId. If no match is
+    // found, return null
+    public static String searchDirForPath(File dir, int bucketId) {
+        File[] files = dir.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    String path = file.getAbsolutePath();
+                    if (GalleryUtils.getBucketId(path) == bucketId) {
+                        return path;
+                    } else {
+                        path = searchDirForPath(file, bucketId);
+                        if (path != null) return path;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    // Returns a (localized) string for the given duration (in seconds).
+    public static String formatDuration(final Context context, int duration) {
+        int h = duration / 3600;
+        int m = (duration - h * 3600) / 60;
+        int s = duration - (h * 3600 + m * 60);
+        String durationValue;
+        if (h == 0) {
+            durationValue = String.format(context.getString(R.string.details_ms), m, s);
+        } else {
+            durationValue = String.format(context.getString(R.string.details_hms), h, m, s);
+        }
+        return durationValue;
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    public static int determineTypeBits(Context context, Intent intent) {
+        int typeBits = 0;
+        String type = intent.resolveType(context);
+
+        if (MIME_TYPE_ALL.equals(type)) {
+            typeBits = DataManager.INCLUDE_ALL;
+        } else if (MIME_TYPE_IMAGE.equals(type) ||
+                DIR_TYPE_IMAGE.equals(type)) {
+            typeBits = DataManager.INCLUDE_IMAGE;
+        } else if (MIME_TYPE_VIDEO.equals(type) ||
+                DIR_TYPE_VIDEO.equals(type)) {
+            typeBits = DataManager.INCLUDE_VIDEO;
+        } else {
+            typeBits = DataManager.INCLUDE_ALL;
+        }
+
+        if (ApiHelper.HAS_INTENT_EXTRA_LOCAL_ONLY) {
+            if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) {
+                typeBits |= DataManager.INCLUDE_LOCAL_ONLY;
+            }
+        }
+
+        return typeBits;
+    }
+
+    public static int getSelectionModePrompt(int typeBits) {
+        if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) {
+            return (typeBits & DataManager.INCLUDE_IMAGE) == 0
+                    ? R.string.select_video
+                    : R.string.select_item;
+        }
+        return R.string.select_image;
+    }
+
+    public static boolean hasSpaceForSize(long size) {
+        String state = Environment.getExternalStorageState();
+        if (!Environment.MEDIA_MOUNTED.equals(state)) {
+            return false;
+        }
+
+        String path = Environment.getExternalStorageDirectory().getPath();
+        try {
+            StatFs stat = new StatFs(path);
+            return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
+        } catch (Exception e) {
+            Log.i(TAG, "Fail to access external storage", e);
+        }
+        return false;
+    }
+
+    public static boolean isPanorama(MediaItem item) {
+        if (item == null) return false;
+        int w = item.getWidth();
+        int h = item.getHeight();
+        return (h > 0 && w / h >= 2);
+    }
+}
diff --git a/src/com/android/gallery3d/util/Holder.java b/src/com/android/gallery3d/util/Holder.java
new file mode 100644
index 0000000..0ce914c
--- /dev/null
+++ b/src/com/android/gallery3d/util/Holder.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+public class Holder<T> {
+    private T mObject;
+
+    public void set(T object) {
+        mObject = object;
+    }
+
+    public T get() {
+        return mObject;
+    }
+}
diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java
new file mode 100644
index 0000000..3edc424
--- /dev/null
+++ b/src/com/android/gallery3d/util/IdentityCache.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+
+public class IdentityCache<K, V> {
+
+    private final HashMap<K, Entry<K, V>> mWeakMap =
+            new HashMap<K, Entry<K, V>>();
+    private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();
+
+    public IdentityCache() {
+    }
+
+    private static class Entry<K, V> extends WeakReference<V> {
+        K mKey;
+
+        public Entry(K key, V value, ReferenceQueue<V> queue) {
+            super(value, queue);
+            mKey = key;
+        }
+    }
+
+    private void cleanUpWeakMap() {
+        Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
+        while (entry != null) {
+            mWeakMap.remove(entry.mKey);
+            entry = (Entry<K, V>) mQueue.poll();
+        }
+    }
+
+    public synchronized V put(K key, V value) {
+        cleanUpWeakMap();
+        Entry<K, V> entry = mWeakMap.put(
+                key, new Entry<K, V>(key, value, mQueue));
+        return entry == null ? null : entry.get();
+    }
+
+    public synchronized V get(K key) {
+        cleanUpWeakMap();
+        Entry<K, V> entry = mWeakMap.get(key);
+        return entry == null ? null : entry.get();
+    }
+
+    // This is currently unused.
+    /*
+    public synchronized void clear() {
+        mWeakMap.clear();
+        mQueue = new ReferenceQueue<V>();
+    }
+    */
+
+    // This is for debugging only
+    public synchronized ArrayList<K> keys() {
+        Set<K> set = mWeakMap.keySet();
+        ArrayList<K> result = new ArrayList<K>(set);
+        return result;
+    }
+}
diff --git a/src/com/android/gallery3d/util/IntArray.java b/src/com/android/gallery3d/util/IntArray.java
new file mode 100644
index 0000000..2c4dc2c
--- /dev/null
+++ b/src/com/android/gallery3d/util/IntArray.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+public class IntArray {
+    private static final int INIT_CAPACITY = 8;
+
+    private int mData[] = new int[INIT_CAPACITY];
+    private int mSize = 0;
+
+    public void add(int value) {
+        if (mData.length == mSize) {
+            int temp[] = new int[mSize + mSize];
+            System.arraycopy(mData, 0, temp, 0, mSize);
+            mData = temp;
+        }
+        mData[mSize++] = value;
+    }
+
+    public int removeLast() {
+        mSize--;
+        return mData[mSize];
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    // For testing only
+    public int[] toArray(int[] result) {
+        if (result == null || result.length < mSize) {
+            result = new int[mSize];
+        }
+        System.arraycopy(mData, 0, result, 0, mSize);
+        return result;
+    }
+
+    public int[] getInternalArray() {
+        return mData;
+    }
+
+    public void clear() {
+        mSize = 0;
+        if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY];
+    }
+}
diff --git a/src/com/android/gallery3d/util/InterruptableOutputStream.java b/src/com/android/gallery3d/util/InterruptableOutputStream.java
new file mode 100644
index 0000000..1ab62ab
--- /dev/null
+++ b/src/com/android/gallery3d/util/InterruptableOutputStream.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+
+public class InterruptableOutputStream extends OutputStream {
+
+    private static final int MAX_WRITE_BYTES = 4096;
+
+    private OutputStream mOutputStream;
+    private volatile boolean mIsInterrupted = false;
+
+    public InterruptableOutputStream(OutputStream outputStream) {
+        mOutputStream = Utils.checkNotNull(outputStream);
+    }
+
+    @Override
+    public void write(int oneByte) throws IOException {
+        if (mIsInterrupted) throw new InterruptedIOException();
+        mOutputStream.write(oneByte);
+    }
+
+    @Override
+    public void write(byte[] buffer, int offset, int count) throws IOException {
+        int end = offset + count;
+        while (offset < end) {
+            if (mIsInterrupted) throw new InterruptedIOException();
+            int bytesCount = Math.min(MAX_WRITE_BYTES, end - offset);
+            mOutputStream.write(buffer, offset, bytesCount);
+            offset += bytesCount;
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        mOutputStream.close();
+    }
+
+    @Override
+    public void flush() throws IOException {
+        if (mIsInterrupted) throw new InterruptedIOException();
+        mOutputStream.flush();
+    }
+
+    public void interrupt() {
+        mIsInterrupted = true;
+    }
+}
diff --git a/src/com/android/gallery3d/util/JobLimiter.java b/src/com/android/gallery3d/util/JobLimiter.java
new file mode 100644
index 0000000..42b7541
--- /dev/null
+++ b/src/com/android/gallery3d/util/JobLimiter.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.util;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.LinkedList;
+
+// Limit the number of concurrent jobs that has been submitted into a ThreadPool
+@SuppressWarnings("rawtypes")
+public class JobLimiter implements FutureListener {
+    private static final String TAG = "JobLimiter";
+
+    // State Transition:
+    //      INIT -> DONE, CANCELLED
+    //      DONE -> CANCELLED
+    private static final int STATE_INIT = 0;
+    private static final int STATE_DONE = 1;
+    private static final int STATE_CANCELLED = 2;
+
+    private final LinkedList<JobWrapper<?>> mJobs = new LinkedList<JobWrapper<?>>();
+    private final ThreadPool mPool;
+    private int mLimit;
+
+    private static class JobWrapper<T> implements Future<T>, Job<T> {
+        private int mState = STATE_INIT;
+        private Job<T> mJob;
+        private Future<T> mDelegate;
+        private FutureListener<T> mListener;
+        private T mResult;
+
+        public JobWrapper(Job<T> job, FutureListener<T> listener) {
+            mJob = job;
+            mListener = listener;
+        }
+
+        public synchronized void setFuture(Future<T> future) {
+            if (mState != STATE_INIT) return;
+            mDelegate = future;
+        }
+
+        @Override
+        public void cancel() {
+            FutureListener<T> listener = null;
+            synchronized (this) {
+                if (mState != STATE_DONE) {
+                    listener = mListener;
+                    mJob = null;
+                    mListener = null;
+                    if (mDelegate != null) {
+                        mDelegate.cancel();
+                        mDelegate = null;
+                    }
+                }
+                mState = STATE_CANCELLED;
+                mResult = null;
+                notifyAll();
+            }
+            if (listener != null) listener.onFutureDone(this);
+        }
+
+        @Override
+        public synchronized boolean isCancelled() {
+            return mState == STATE_CANCELLED;
+        }
+
+        @Override
+        public boolean isDone() {
+            // Both CANCELLED AND DONE is considered as done
+            return mState !=  STATE_INIT;
+        }
+
+        @Override
+        public synchronized T get() {
+            while (mState == STATE_INIT) {
+                // handle the interrupted exception of wait()
+                Utils.waitWithoutInterrupt(this);
+            }
+            return mResult;
+        }
+
+        @Override
+        public void waitDone() {
+            get();
+        }
+
+        @Override
+        public T run(JobContext jc) {
+            Job<T> job = null;
+            synchronized (this) {
+                if (mState == STATE_CANCELLED) return null;
+                job = mJob;
+            }
+            T result  = null;
+            try {
+                result = job.run(jc);
+            } catch (Throwable t) {
+                Log.w(TAG, "error executing job: " + job, t);
+            }
+            FutureListener<T> listener = null;
+            synchronized (this) {
+                if (mState == STATE_CANCELLED) return null;
+                mState = STATE_DONE;
+                listener = mListener;
+                mListener = null;
+                mJob = null;
+                mResult = result;
+                notifyAll();
+            }
+            if (listener != null) listener.onFutureDone(this);
+            return result;
+        }
+    }
+
+    public JobLimiter(ThreadPool pool, int limit) {
+        mPool = Utils.checkNotNull(pool);
+        mLimit = limit;
+    }
+
+    public synchronized <T> Future<T> submit(Job<T> job, FutureListener<T> listener) {
+        JobWrapper<T> future = new JobWrapper<T>(Utils.checkNotNull(job), listener);
+        mJobs.addLast(future);
+        submitTasksIfAllowed();
+        return future;
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private void submitTasksIfAllowed() {
+        while (mLimit > 0 && !mJobs.isEmpty()) {
+            JobWrapper wrapper = mJobs.removeFirst();
+            if (!wrapper.isCancelled()) {
+                --mLimit;
+                wrapper.setFuture(mPool.submit(wrapper, this));
+            }
+        }
+    }
+
+    @Override
+    public synchronized void onFutureDone(Future future) {
+        ++mLimit;
+        submitTasksIfAllowed();
+    }
+}
diff --git a/src/com/android/gallery3d/util/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java
new file mode 100644
index 0000000..4cfc3cd
--- /dev/null
+++ b/src/com/android/gallery3d/util/LinkedNode.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+
+public class LinkedNode {
+    private LinkedNode mPrev;
+    private LinkedNode mNext;
+
+    public LinkedNode() {
+        mPrev = mNext = this;
+    }
+
+    public void insert(LinkedNode node) {
+        node.mNext = mNext;
+        mNext.mPrev = node;
+        node.mPrev = this;
+        mNext = node;
+    }
+
+    public void remove() {
+        if (mNext == this) throw new IllegalStateException();
+        mPrev.mNext = mNext;
+        mNext.mPrev = mPrev;
+        mPrev = mNext = null;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static class List<T extends LinkedNode> {
+        private LinkedNode mHead = new LinkedNode();
+
+        public void insertLast(T node) {
+            mHead.mPrev.insert(node);
+        }
+
+        public T getFirst() {
+            return (T) (mHead.mNext == mHead ? null : mHead.mNext);
+        }
+
+        public T getLast() {
+            return (T) (mHead.mPrev == mHead ? null : mHead.mPrev);
+        }
+
+        public T nextOf(T node) {
+            return (T) (node.mNext == mHead ? null : node.mNext);
+        }
+
+        public T previousOf(T node) {
+            return (T) (node.mPrev == mHead ? null : node.mPrev);
+        }
+
+    }
+
+    public static <T extends LinkedNode> List<T> newList() {
+        return new List<T>();
+    }
+}
diff --git a/src/com/android/gallery3d/util/Log.java b/src/com/android/gallery3d/util/Log.java
new file mode 100644
index 0000000..d7f8e85
--- /dev/null
+++ b/src/com/android/gallery3d/util/Log.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+public class Log {
+    public static int v(String tag, String msg) {
+        return android.util.Log.v(tag, msg);
+    }
+    public static int v(String tag, String msg, Throwable tr) {
+        return android.util.Log.v(tag, msg, tr);
+    }
+    public static int d(String tag, String msg) {
+        return android.util.Log.d(tag, msg);
+    }
+    public static int d(String tag, String msg, Throwable tr) {
+        return android.util.Log.d(tag, msg, tr);
+    }
+    public static int i(String tag, String msg) {
+        return android.util.Log.i(tag, msg);
+    }
+    public static int i(String tag, String msg, Throwable tr) {
+        return android.util.Log.i(tag, msg, tr);
+    }
+    public static int w(String tag, String msg) {
+        return android.util.Log.w(tag, msg);
+    }
+    public static int w(String tag, String msg, Throwable tr) {
+        return android.util.Log.w(tag, msg, tr);
+    }
+    public static int w(String tag, Throwable tr) {
+        return android.util.Log.w(tag, tr);
+    }
+    public static int e(String tag, String msg) {
+        return android.util.Log.e(tag, msg);
+    }
+    public static int e(String tag, String msg, Throwable tr) {
+        return android.util.Log.e(tag, msg, tr);
+    }
+}
diff --git a/src/com/android/gallery3d/util/MediaSetUtils.java b/src/com/android/gallery3d/util/MediaSetUtils.java
new file mode 100644
index 0000000..0438005
--- /dev/null
+++ b/src/com/android/gallery3d/util/MediaSetUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import android.os.Environment;
+
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.data.LocalMergeAlbum;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import java.util.Comparator;
+
+public class MediaSetUtils {
+    public static final Comparator<MediaSet> NAME_COMPARATOR = new NameComparator();
+
+    public static final int CAMERA_BUCKET_ID = GalleryUtils.getBucketId(
+            Environment.getExternalStorageDirectory().toString() + "/"
+            + BucketNames.CAMERA);
+    public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId(
+            Environment.getExternalStorageDirectory().toString() + "/"
+            + BucketNames.DOWNLOAD);
+    public static final int EDITED_ONLINE_PHOTOS_BUCKET_ID = GalleryUtils.getBucketId(
+            Environment.getExternalStorageDirectory().toString() + "/"
+            + BucketNames.EDITED_ONLINE_PHOTOS);
+    public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId(
+            Environment.getExternalStorageDirectory().toString() + "/"
+            + BucketNames.IMPORTED);
+    public static final int SNAPSHOT_BUCKET_ID = GalleryUtils.getBucketId(
+            Environment.getExternalStorageDirectory().toString() +
+            "/" + BucketNames.SCREENSHOTS);
+
+    private static final Path[] CAMERA_PATHS = {
+            Path.fromString("/local/all/" + CAMERA_BUCKET_ID),
+            Path.fromString("/local/image/" + CAMERA_BUCKET_ID),
+            Path.fromString("/local/video/" + CAMERA_BUCKET_ID)};
+
+    public static boolean isCameraSource(Path path) {
+        return CAMERA_PATHS[0] == path || CAMERA_PATHS[1] == path
+                || CAMERA_PATHS[2] == path;
+    }
+
+    // Sort MediaSets by name
+    public static class NameComparator implements Comparator<MediaSet> {
+        @Override
+        public int compare(MediaSet set1, MediaSet set2) {
+            int result = set1.getName().compareToIgnoreCase(set2.getName());
+            if (result != 0) return result;
+            return set1.getPath().toString().compareTo(set2.getPath().toString());
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/util/MotionEventHelper.java b/src/com/android/gallery3d/util/MotionEventHelper.java
new file mode 100644
index 0000000..715f7fa
--- /dev/null
+++ b/src/com/android/gallery3d/util/MotionEventHelper.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import android.annotation.TargetApi;
+import android.graphics.Matrix;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+
+import com.android.gallery3d.common.ApiHelper;
+
+public final class MotionEventHelper {
+    private MotionEventHelper() {}
+
+    public static MotionEvent transformEvent(MotionEvent e, Matrix m) {
+        // We try to use the new transform method if possible because it uses
+        // less memory.
+        if (ApiHelper.HAS_MOTION_EVENT_TRANSFORM) {
+            return transformEventNew(e, m);
+        } else {
+            return transformEventOld(e, m);
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static MotionEvent transformEventNew(MotionEvent e, Matrix m) {
+        MotionEvent newEvent = MotionEvent.obtain(e);
+        newEvent.transform(m);
+        return newEvent;
+    }
+
+    // This is copied from Input.cpp in the android framework.
+    private static MotionEvent transformEventOld(MotionEvent e, Matrix m) {
+        long downTime = e.getDownTime();
+        long eventTime = e.getEventTime();
+        int action = e.getAction();
+        int pointerCount = e.getPointerCount();
+        int[] pointerIds = getPointerIds(e);
+        PointerCoords[] pointerCoords = getPointerCoords(e);
+        int metaState = e.getMetaState();
+        float xPrecision = e.getXPrecision();
+        float yPrecision = e.getYPrecision();
+        int deviceId = e.getDeviceId();
+        int edgeFlags = e.getEdgeFlags();
+        int source = e.getSource();
+        int flags = e.getFlags();
+
+        // Copy the x and y coordinates into an array, map them, and copy back.
+        float[] xy = new float[pointerCoords.length * 2];
+        for (int i = 0; i < pointerCount;i++) {
+            xy[2 * i] = pointerCoords[i].x;
+            xy[2 * i + 1] = pointerCoords[i].y;
+        }
+        m.mapPoints(xy);
+        for (int i = 0; i < pointerCount;i++) {
+            pointerCoords[i].x = xy[2 * i];
+            pointerCoords[i].y = xy[2 * i + 1];
+            pointerCoords[i].orientation = transformAngle(
+                m, pointerCoords[i].orientation);
+        }
+
+        MotionEvent n = MotionEvent.obtain(downTime, eventTime, action,
+                pointerCount, pointerIds, pointerCoords, metaState, xPrecision,
+                yPrecision, deviceId, edgeFlags, source, flags);
+
+        return n;
+    }
+
+    private static int[] getPointerIds(MotionEvent e) {
+        int n = e.getPointerCount();
+        int[] r = new int[n];
+        for (int i = 0; i < n; i++) {
+            r[i] = e.getPointerId(i);
+        }
+        return r;
+    }
+
+    private static PointerCoords[] getPointerCoords(MotionEvent e) {
+        int n = e.getPointerCount();
+        PointerCoords[] r = new PointerCoords[n];
+        for (int i = 0; i < n; i++) {
+            r[i] = new PointerCoords();
+            e.getPointerCoords(i, r[i]);
+        }
+        return r;
+    }
+
+    private static float transformAngle(Matrix m, float angleRadians) {
+        // Construct and transform a vector oriented at the specified clockwise
+        // angle from vertical.  Coordinate system: down is increasing Y, right is
+        // increasing X.
+        float[] v = new float[2];
+        v[0] = FloatMath.sin(angleRadians);
+        v[1] = -FloatMath.cos(angleRadians);
+        m.mapVectors(v);
+
+        // Derive the transformed vector's clockwise angle from vertical.
+        float result = (float) Math.atan2(v[0], -v[1]);
+        if (result < -Math.PI / 2) {
+            result += Math.PI;
+        } else if (result > Math.PI / 2) {
+            result -= Math.PI;
+        }
+        return result;
+    }
+}
diff --git a/src/com/android/gallery3d/util/Profile.java b/src/com/android/gallery3d/util/Profile.java
new file mode 100644
index 0000000..7ed72c9
--- /dev/null
+++ b/src/com/android/gallery3d/util/Profile.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+// The Profile class is used to collect profiling information for a thread. It
+// samples stack traces for a thread periodically. enable() and disable() is
+// used to enable and disable profiling for the calling thread. The profiling
+// information can then be dumped to a file using the dumpToFile() method.
+//
+// The disableAll() method can be used to disable profiling for all threads and
+// can be called in onPause() to ensure all profiling is disabled when an
+// activity is paused.
+public class Profile {
+    @SuppressWarnings("unused")
+    private static final String TAG = "Profile";
+    private static final int NS_PER_MS = 1000000;
+
+    // This is a watchdog entry for one thread.
+    // For every cycleTime period, we dump the stack of the thread.
+    private static class WatchEntry {
+        Thread thread;
+
+        // Both are in milliseconds
+        int cycleTime;
+        int wakeTime;
+
+        boolean isHolding;
+        ArrayList<String[]> holdingStacks = new ArrayList<String[]>();
+    }
+
+    // This is a watchdog thread which dumps stacks of other threads periodically.
+    private static Watchdog sWatchdog = new Watchdog();
+
+    private static class Watchdog {
+        private ArrayList<WatchEntry> mList = new ArrayList<WatchEntry>();
+        private HandlerThread mHandlerThread;
+        private Handler mHandler;
+        private Runnable mProcessRunnable = new Runnable() {
+            @Override
+            public void run() {
+                synchronized (Watchdog.this) {
+                    processList();
+                }
+            }
+        };
+        private Random mRandom = new Random();
+        private ProfileData mProfileData = new ProfileData();
+
+        public Watchdog() {
+            mHandlerThread = new HandlerThread("Watchdog Handler",
+                    Process.THREAD_PRIORITY_FOREGROUND);
+            mHandlerThread.start();
+            mHandler = new Handler(mHandlerThread.getLooper());
+        }
+
+        public synchronized void addWatchEntry(Thread thread, int cycleTime) {
+            WatchEntry e = new WatchEntry();
+            e.thread = thread;
+            e.cycleTime = cycleTime;
+            int firstDelay = 1 + mRandom.nextInt(cycleTime);
+            e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay;
+            mList.add(e);
+            processList();
+        }
+
+        public synchronized void removeWatchEntry(Thread thread) {
+            for (int i = 0; i < mList.size(); i++) {
+                if (mList.get(i).thread == thread) {
+                    mList.remove(i);
+                    break;
+                }
+            }
+            processList();
+        }
+
+        public synchronized void removeAllWatchEntries() {
+            mList.clear();
+            processList();
+        }
+
+        private void processList() {
+            mHandler.removeCallbacks(mProcessRunnable);
+            if (mList.size() == 0) return;
+
+            int currentTime = (int) (System.nanoTime() / NS_PER_MS);
+            int nextWakeTime = 0;
+
+            for (WatchEntry entry : mList) {
+                if (currentTime > entry.wakeTime) {
+                    entry.wakeTime += entry.cycleTime;
+                    Thread thread = entry.thread;
+                    sampleStack(entry);
+                }
+
+                if (entry.wakeTime > nextWakeTime) {
+                    nextWakeTime = entry.wakeTime;
+                }
+            }
+
+            long delay = nextWakeTime - currentTime;
+            mHandler.postDelayed(mProcessRunnable, delay);
+        }
+
+        private void sampleStack(WatchEntry entry) {
+            Thread thread = entry.thread;
+            StackTraceElement[] stack = thread.getStackTrace();
+            String[] lines = new String[stack.length];
+            for (int i = 0; i < stack.length; i++) {
+                lines[i] = stack[i].toString();
+            }
+            if (entry.isHolding) {
+                entry.holdingStacks.add(lines);
+            } else {
+                mProfileData.addSample(lines);
+            }
+        }
+
+        private WatchEntry findEntry(Thread thread) {
+            for (int i = 0; i < mList.size(); i++) {
+                WatchEntry entry = mList.get(i);
+                if (entry.thread == thread) return entry;
+            }
+            return null;
+        }
+
+        public synchronized void dumpToFile(String filename) {
+            mProfileData.dumpToFile(filename);
+        }
+
+        public synchronized void reset() {
+            mProfileData.reset();
+        }
+
+        public synchronized void hold(Thread t) {
+            WatchEntry entry = findEntry(t);
+
+            // This can happen if the profiling is disabled (probably from
+            // another thread). Same check is applied in commit() and drop()
+            // below.
+            if (entry == null) return;
+
+            entry.isHolding = true;
+        }
+
+        public synchronized void commit(Thread t) {
+            WatchEntry entry = findEntry(t);
+            if (entry == null) return;
+            ArrayList<String[]> stacks = entry.holdingStacks;
+            for (int i = 0; i < stacks.size(); i++) {
+                mProfileData.addSample(stacks.get(i));
+            }
+            entry.isHolding = false;
+            entry.holdingStacks.clear();
+        }
+
+        public synchronized void drop(Thread t) {
+            WatchEntry entry = findEntry(t);
+            if (entry == null) return;
+            entry.isHolding = false;
+            entry.holdingStacks.clear();
+        }
+    }
+
+    // Enable profiling for the calling thread. Periodically (every cycleTimeInMs
+    // milliseconds) sample the stack trace of the calling thread.
+    public static void enable(int cycleTimeInMs) {
+        Thread t = Thread.currentThread();
+        sWatchdog.addWatchEntry(t, cycleTimeInMs);
+    }
+
+    // Disable profiling for the calling thread.
+    public static void disable() {
+        sWatchdog.removeWatchEntry(Thread.currentThread());
+    }
+
+    // Disable profiling for all threads.
+    public static void disableAll() {
+        sWatchdog.removeAllWatchEntries();
+    }
+
+    // Dump the profiling data to a file.
+    public static void dumpToFile(String filename) {
+        sWatchdog.dumpToFile(filename);
+    }
+
+    // Reset the collected profiling data.
+    public static void reset() {
+        sWatchdog.reset();
+    }
+
+    // Hold the future samples coming from current thread until commit() or
+    // drop() is called, and those samples are recorded or ignored as a result.
+    // This must called after enable() to be effective.
+    public static void hold() {
+        sWatchdog.hold(Thread.currentThread());
+    }
+
+    public static void commit() {
+        sWatchdog.commit(Thread.currentThread());
+    }
+
+    public static void drop() {
+        sWatchdog.drop(Thread.currentThread());
+    }
+}
diff --git a/src/com/android/gallery3d/util/ProfileData.java b/src/com/android/gallery3d/util/ProfileData.java
new file mode 100644
index 0000000..a1bb8e1
--- /dev/null
+++ b/src/com/android/gallery3d/util/ProfileData.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.DataOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+// ProfileData keeps profiling samples in a tree structure.
+// The addSample() method adds a sample. The dumpToFile() method saves the data
+// to a file. The reset() method clears all samples.
+public class ProfileData {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ProfileData";
+
+    private static class Node {
+        public int id;  // this is the name of this node, mapped from mNameToId
+        public Node parent;
+        public int sampleCount;
+        public ArrayList<Node> children;
+        public Node(Node parent, int id) {
+            this.parent = parent;
+            this.id = id;
+        }
+    }
+
+    private Node mRoot;
+    private int mNextId;
+    private HashMap<String, Integer> mNameToId;
+    private DataOutputStream mOut;
+    private byte mScratch[] = new byte[4];  // scratch space for writeInt()
+
+    public ProfileData() {
+        mRoot = new Node(null, -1);  // The id of the root node is unused.
+        mNameToId = new HashMap<String, Integer>();
+    }
+
+    public void reset() {
+        mRoot = new Node(null, -1);
+        mNameToId.clear();
+        mNextId = 0;
+    }
+
+    private int nameToId(String name) {
+        Integer id = mNameToId.get(name);
+        if (id == null) {
+            id = ++mNextId;  // The tool doesn't want id=0, so we start from 1.
+            mNameToId.put(name, id);
+        }
+        return id;
+    }
+
+    public void addSample(String[] stack) {
+        int[] ids = new int[stack.length];
+        for (int i = 0; i < stack.length; i++) {
+            ids[i] = nameToId(stack[i]);
+        }
+
+        Node node = mRoot;
+        for (int i = stack.length - 1; i >= 0; i--) {
+            if (node.children == null) {
+                node.children = new ArrayList<Node>();
+            }
+
+            int id = ids[i];
+            ArrayList<Node> children = node.children;
+            int j;
+            for (j = 0; j < children.size(); j++) {
+                if (children.get(j).id == id) break;
+            }
+            if (j == children.size()) {
+                children.add(new Node(node, id));
+            }
+
+            node = children.get(j);
+        }
+
+        node.sampleCount++;
+    }
+
+    public void dumpToFile(String filename) {
+        try {
+            mOut = new DataOutputStream(new FileOutputStream(filename));
+            // Start record
+            writeInt(0);
+            writeInt(3);
+            writeInt(1);
+            writeInt(20000);  // Sampling period: 20ms
+            writeInt(0);
+
+            // Samples
+            writeAllStacks(mRoot, 0);
+
+            // End record
+            writeInt(0);
+            writeInt(1);
+            writeInt(0);
+            writeAllSymbols();
+        } catch (IOException ex) {
+            Log.w("Failed to dump to file", ex);
+        } finally {
+            Utils.closeSilently(mOut);
+        }
+    }
+
+    // Writes out one stack, consisting of N+2 words:
+    // first word: sample count
+    // second word: depth of the stack (N)
+    // N words: each word is the id of one address in the stack
+    private void writeOneStack(Node node, int depth) throws IOException {
+        writeInt(node.sampleCount);
+        writeInt(depth);
+        while (depth-- > 0) {
+            writeInt(node.id);
+            node = node.parent;
+        }
+    }
+
+    private void writeAllStacks(Node node, int depth) throws IOException {
+        if (node.sampleCount > 0) {
+            writeOneStack(node, depth);
+        }
+
+        ArrayList<Node> children = node.children;
+        if (children != null) {
+            for (int i = 0; i < children.size(); i++) {
+                writeAllStacks(children.get(i), depth + 1);
+            }
+        }
+    }
+
+    // Writes out the symbol table. Each line is like:
+    // 0x17e java.util.ArrayList.isEmpty(ArrayList.java:319)
+    private void writeAllSymbols() throws IOException {
+        for (Entry<String, Integer> entry : mNameToId.entrySet()) {
+            mOut.writeBytes(String.format("0x%x %s\n", entry.getValue(), entry.getKey()));
+        }
+    }
+
+    private void writeInt(int v) throws IOException {
+        mScratch[0] = (byte) v;
+        mScratch[1] = (byte) (v >> 8);
+        mScratch[2] = (byte) (v >> 16);
+        mScratch[3] = (byte) (v >> 24);
+        mOut.write(mScratch);
+    }
+}
diff --git a/src/com/android/gallery3d/util/RangeArray.java b/src/com/android/gallery3d/util/RangeArray.java
new file mode 100644
index 0000000..8e61348
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeArray.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeArray<T> {
+    private T[] mData;
+    private int mOffset;
+
+    public RangeArray(int min, int max) {
+        mData = (T[]) new Object[max - min + 1];
+        mOffset = min;
+    }
+
+    // Wraps around an existing array
+    public RangeArray(T[] src, int min, int max) {
+        if (max - min + 1 != src.length) {
+            throw new AssertionError();
+        }
+        mData = src;
+        mOffset = min;
+    }
+
+    public void put(int i, T object) {
+        mData[i - mOffset] = object;
+    }
+
+    public T get(int i) {
+        return mData[i - mOffset];
+    }
+
+    public int indexOf(T object) {
+        for (int i = 0; i < mData.length; i++) {
+            if (mData[i] == object) return i + mOffset;
+        }
+        return Integer.MAX_VALUE;
+    }
+}
diff --git a/src/com/android/gallery3d/util/RangeBoolArray.java b/src/com/android/gallery3d/util/RangeBoolArray.java
new file mode 100644
index 0000000..035fc40
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeBoolArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeBoolArray {
+    private boolean[] mData;
+    private int mOffset;
+
+    public RangeBoolArray(int min, int max) {
+        mData = new boolean[max - min + 1];
+        mOffset = min;
+    }
+
+    // Wraps around an existing array
+    public RangeBoolArray(boolean[] src, int min, int max) {
+        mData = src;
+        mOffset = min;
+    }
+
+    public void put(int i, boolean object) {
+        mData[i - mOffset] = object;
+    }
+
+    public boolean get(int i) {
+        return mData[i - mOffset];
+    }
+
+    public int indexOf(boolean object) {
+        for (int i = 0; i < mData.length; i++) {
+            if (mData[i] == object) return i + mOffset;
+        }
+        return Integer.MAX_VALUE;
+    }
+}
diff --git a/src/com/android/gallery3d/util/RangeIntArray.java b/src/com/android/gallery3d/util/RangeIntArray.java
new file mode 100644
index 0000000..9dbb99f
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeIntArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeIntArray {
+    private int[] mData;
+    private int mOffset;
+
+    public RangeIntArray(int min, int max) {
+        mData = new int[max - min + 1];
+        mOffset = min;
+    }
+
+    // Wraps around an existing array
+    public RangeIntArray(int[] src, int min, int max) {
+        mData = src;
+        mOffset = min;
+    }
+
+    public void put(int i, int object) {
+        mData[i - mOffset] = object;
+    }
+
+    public int get(int i) {
+        return mData[i - mOffset];
+    }
+
+    public int indexOf(int object) {
+        for (int i = 0; i < mData.length; i++) {
+            if (mData[i] == object) return i + mOffset;
+        }
+        return Integer.MAX_VALUE;
+    }
+}
diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java
new file mode 100644
index 0000000..a8b26d9
--- /dev/null
+++ b/src/com/android/gallery3d/util/ReverseGeocoder.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import com.android.gallery3d.common.BlobCache;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+public class ReverseGeocoder {
+    @SuppressWarnings("unused")
+    private static final String TAG = "ReverseGeocoder";
+    public static final int EARTH_RADIUS_METERS = 6378137;
+    public static final int LAT_MIN = -90;
+    public static final int LAT_MAX = 90;
+    public static final int LON_MIN = -180;
+    public static final int LON_MAX = 180;
+    private static final int MAX_COUNTRY_NAME_LENGTH = 8;
+    // If two points are within 20 miles of each other, use
+    // "Around Palo Alto, CA" or "Around Mountain View, CA".
+    // instead of directly jumping to the next level and saying
+    // "California, US".
+    private static final int MAX_LOCALITY_MILE_RANGE = 20;
+
+    private static final String GEO_CACHE_FILE = "rev_geocoding";
+    private static final int GEO_CACHE_MAX_ENTRIES = 1000;
+    private static final int GEO_CACHE_MAX_BYTES = 500 * 1024;
+    private static final int GEO_CACHE_VERSION = 0;
+
+    public static class SetLatLong {
+        // The latitude and longitude of the min latitude point.
+        public double mMinLatLatitude = LAT_MAX;
+        public double mMinLatLongitude;
+        // The latitude and longitude of the max latitude point.
+        public double mMaxLatLatitude = LAT_MIN;
+        public double mMaxLatLongitude;
+        // The latitude and longitude of the min longitude point.
+        public double mMinLonLatitude;
+        public double mMinLonLongitude = LON_MAX;
+        // The latitude and longitude of the max longitude point.
+        public double mMaxLonLatitude;
+        public double mMaxLonLongitude = LON_MIN;
+    }
+
+    private Context mContext;
+    private Geocoder mGeocoder;
+    private BlobCache mGeoCache;
+    private ConnectivityManager mConnectivityManager;
+    private static Address sCurrentAddress; // last known address
+
+    public ReverseGeocoder(Context context) {
+        mContext = context;
+        mGeocoder = new Geocoder(mContext);
+        mGeoCache = CacheManager.getCache(context, GEO_CACHE_FILE,
+                GEO_CACHE_MAX_ENTRIES, GEO_CACHE_MAX_BYTES,
+                GEO_CACHE_VERSION);
+        mConnectivityManager = (ConnectivityManager)
+                context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    public String computeAddress(SetLatLong set) {
+        // The overall min and max latitudes and longitudes of the set.
+        double setMinLatitude = set.mMinLatLatitude;
+        double setMinLongitude = set.mMinLatLongitude;
+        double setMaxLatitude = set.mMaxLatLatitude;
+        double setMaxLongitude = set.mMaxLatLongitude;
+        if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude)
+                < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) {
+            setMinLatitude = set.mMinLonLatitude;
+            setMinLongitude = set.mMinLonLongitude;
+            setMaxLatitude = set.mMaxLonLatitude;
+            setMaxLongitude = set.mMaxLonLongitude;
+        }
+        Address addr1 = lookupAddress(setMinLatitude, setMinLongitude, true);
+        Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude, true);
+        if (addr1 == null)
+            addr1 = addr2;
+        if (addr2 == null)
+            addr2 = addr1;
+        if (addr1 == null || addr2 == null) {
+            return null;
+        }
+
+        // Get current location, we decide the granularity of the string based
+        // on this.
+        LocationManager locationManager =
+                (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+        Location location = null;
+        List<String> providers = locationManager.getAllProviders();
+        for (int i = 0; i < providers.size(); ++i) {
+            String provider = providers.get(i);
+            location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null;
+            if (location != null)
+                break;
+        }
+        String currentCity = "";
+        String currentAdminArea = "";
+        String currentCountry = Locale.getDefault().getCountry();
+        if (location != null) {
+            Address currentAddress = lookupAddress(
+                    location.getLatitude(), location.getLongitude(), true);
+            if (currentAddress == null) {
+                currentAddress = sCurrentAddress;
+            } else {
+                sCurrentAddress = currentAddress;
+            }
+            if (currentAddress != null && currentAddress.getCountryCode() != null) {
+                currentCity = checkNull(currentAddress.getLocality());
+                currentCountry = checkNull(currentAddress.getCountryCode());
+                currentAdminArea = checkNull(currentAddress.getAdminArea());
+            }
+        }
+
+        String closestCommonLocation = null;
+        String addr1Locality = checkNull(addr1.getLocality());
+        String addr2Locality = checkNull(addr2.getLocality());
+        String addr1AdminArea = checkNull(addr1.getAdminArea());
+        String addr2AdminArea = checkNull(addr2.getAdminArea());
+        String addr1CountryCode = checkNull(addr1.getCountryCode());
+        String addr2CountryCode = checkNull(addr2.getCountryCode());
+
+        if (currentCity.equals(addr1Locality) || currentCity.equals(addr2Locality)) {
+            String otherCity = currentCity;
+            if (currentCity.equals(addr1Locality)) {
+                otherCity = addr2Locality;
+                if (otherCity.length() == 0) {
+                    otherCity = addr2AdminArea;
+                    if (!currentCountry.equals(addr2CountryCode)) {
+                        otherCity += " " + addr2CountryCode;
+                    }
+                }
+                addr2Locality = addr1Locality;
+                addr2AdminArea = addr1AdminArea;
+                addr2CountryCode = addr1CountryCode;
+            } else {
+                otherCity = addr1Locality;
+                if (otherCity.length() == 0) {
+                    otherCity = addr1AdminArea;
+                    if (!currentCountry.equals(addr1CountryCode)) {
+                        otherCity += " " + addr1CountryCode;
+                    }
+                }
+                addr1Locality = addr2Locality;
+                addr1AdminArea = addr2AdminArea;
+                addr1CountryCode = addr2CountryCode;
+            }
+            closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0));
+            if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+                if (!currentCity.equals(otherCity)) {
+                    closestCommonLocation += " - " + otherCity;
+                }
+                return closestCommonLocation;
+            }
+
+            // Compare thoroughfare (street address) next.
+            closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare());
+            if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+                return closestCommonLocation;
+            }
+        }
+
+        // Compare the locality.
+        closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality);
+        if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+            String adminArea = addr1AdminArea;
+            String countryCode = addr1CountryCode;
+            if (adminArea != null && adminArea.length() > 0) {
+                if (!countryCode.equals(currentCountry)) {
+                    closestCommonLocation += ", " + adminArea + " " + countryCode;
+                } else {
+                    closestCommonLocation += ", " + adminArea;
+                }
+            }
+            return closestCommonLocation;
+        }
+
+        // If the admin area is the same as the current location, we hide it and
+        // instead show the city name.
+        if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) {
+            if ("".equals(addr1Locality)) {
+                addr1Locality = addr2Locality;
+            }
+            if ("".equals(addr2Locality)) {
+                addr2Locality = addr1Locality;
+            }
+            if (!"".equals(addr1Locality)) {
+                if (addr1Locality.equals(addr2Locality)) {
+                    closestCommonLocation = addr1Locality + ", " + currentAdminArea;
+                } else {
+                    closestCommonLocation = addr1Locality + " - " + addr2Locality;
+                }
+                return closestCommonLocation;
+            }
+        }
+
+        // Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE
+        // mile radius.
+        float[] distanceFloat = new float[1];
+        Location.distanceBetween(setMinLatitude, setMinLongitude,
+                setMaxLatitude, setMaxLongitude, distanceFloat);
+        int distance = (int) GalleryUtils.toMile(distanceFloat[0]);
+        if (distance < MAX_LOCALITY_MILE_RANGE) {
+            // Try each of the points and just return the first one to have a
+            // valid address.
+            closestCommonLocation = getLocalityAdminForAddress(addr1, true);
+            if (closestCommonLocation != null) {
+                return closestCommonLocation;
+            }
+            closestCommonLocation = getLocalityAdminForAddress(addr2, true);
+            if (closestCommonLocation != null) {
+                return closestCommonLocation;
+            }
+        }
+
+        // Check the administrative area.
+        closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea);
+        if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+            String countryCode = addr1CountryCode;
+            if (!countryCode.equals(currentCountry)) {
+                if (countryCode != null && countryCode.length() > 0) {
+                    closestCommonLocation += " " + countryCode;
+                }
+            }
+            return closestCommonLocation;
+        }
+
+        // Check the country codes.
+        closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode);
+        if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+            return closestCommonLocation;
+        }
+        // There is no intersection, let's choose a nicer name.
+        String addr1Country = addr1.getCountryName();
+        String addr2Country = addr2.getCountryName();
+        if (addr1Country == null)
+            addr1Country = addr1CountryCode;
+        if (addr2Country == null)
+            addr2Country = addr2CountryCode;
+        if (addr1Country == null || addr2Country == null)
+            return null;
+        if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) {
+            closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode;
+        } else {
+            closestCommonLocation = addr1Country + " - " + addr2Country;
+        }
+        return closestCommonLocation;
+    }
+
+    private String checkNull(String locality) {
+        if (locality == null)
+            return "";
+        if (locality.equals("null"))
+            return "";
+        return locality;
+    }
+
+    private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) {
+        if (addr == null)
+            return "";
+        String localityAdminStr = addr.getLocality();
+        if (localityAdminStr != null && !("null".equals(localityAdminStr))) {
+            if (approxLocation) {
+                // TODO: Uncomment these lines as soon as we may translations
+                // for Res.string.around.
+                // localityAdminStr =
+                // mContext.getResources().getString(Res.string.around) + " " +
+                // localityAdminStr;
+            }
+            String adminArea = addr.getAdminArea();
+            if (adminArea != null && adminArea.length() > 0) {
+                localityAdminStr += ", " + adminArea;
+            }
+            return localityAdminStr;
+        }
+        return null;
+    }
+
+    public Address lookupAddress(final double latitude, final double longitude,
+            boolean useCache) {
+        try {
+            long locationKey = (long) (((latitude + LAT_MAX) * 2 * LAT_MAX
+                    + (longitude + LON_MAX)) * EARTH_RADIUS_METERS);
+            byte[] cachedLocation = null;
+            if (useCache && mGeoCache != null) {
+                cachedLocation = mGeoCache.lookup(locationKey);
+            }
+            Address address = null;
+            NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo();
+            if (cachedLocation == null || cachedLocation.length == 0) {
+                if (networkInfo == null || !networkInfo.isConnected()) {
+                    return null;
+                }
+                List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1);
+                if (!addresses.isEmpty()) {
+                    address = addresses.get(0);
+                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+                    DataOutputStream dos = new DataOutputStream(bos);
+                    Locale locale = address.getLocale();
+                    writeUTF(dos, locale.getLanguage());
+                    writeUTF(dos, locale.getCountry());
+                    writeUTF(dos, locale.getVariant());
+
+                    writeUTF(dos, address.getThoroughfare());
+                    int numAddressLines = address.getMaxAddressLineIndex();
+                    dos.writeInt(numAddressLines);
+                    for (int i = 0; i < numAddressLines; ++i) {
+                        writeUTF(dos, address.getAddressLine(i));
+                    }
+                    writeUTF(dos, address.getFeatureName());
+                    writeUTF(dos, address.getLocality());
+                    writeUTF(dos, address.getAdminArea());
+                    writeUTF(dos, address.getSubAdminArea());
+
+                    writeUTF(dos, address.getCountryName());
+                    writeUTF(dos, address.getCountryCode());
+                    writeUTF(dos, address.getPostalCode());
+                    writeUTF(dos, address.getPhone());
+                    writeUTF(dos, address.getUrl());
+
+                    dos.flush();
+                    if (mGeoCache != null) {
+                        mGeoCache.insert(locationKey, bos.toByteArray());
+                    }
+                    dos.close();
+                }
+            } else {
+                // Parsing the address from the byte stream.
+                DataInputStream dis = new DataInputStream(
+                        new ByteArrayInputStream(cachedLocation));
+                String language = readUTF(dis);
+                String country = readUTF(dis);
+                String variant = readUTF(dis);
+                Locale locale = null;
+                if (language != null) {
+                    if (country == null) {
+                        locale = new Locale(language);
+                    } else if (variant == null) {
+                        locale = new Locale(language, country);
+                    } else {
+                        locale = new Locale(language, country, variant);
+                    }
+                }
+                if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) {
+                    dis.close();
+                    return lookupAddress(latitude, longitude, false);
+                }
+                address = new Address(locale);
+
+                address.setThoroughfare(readUTF(dis));
+                int numAddressLines = dis.readInt();
+                for (int i = 0; i < numAddressLines; ++i) {
+                    address.setAddressLine(i, readUTF(dis));
+                }
+                address.setFeatureName(readUTF(dis));
+                address.setLocality(readUTF(dis));
+                address.setAdminArea(readUTF(dis));
+                address.setSubAdminArea(readUTF(dis));
+
+                address.setCountryName(readUTF(dis));
+                address.setCountryCode(readUTF(dis));
+                address.setPostalCode(readUTF(dis));
+                address.setPhone(readUTF(dis));
+                address.setUrl(readUTF(dis));
+                dis.close();
+            }
+            return address;
+        } catch (Exception e) {
+            // Ignore.
+        }
+        return null;
+    }
+
+    private String valueIfEqual(String a, String b) {
+        return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;
+    }
+
+    public static final void writeUTF(DataOutputStream dos, String string) throws IOException {
+        if (string == null) {
+            dos.writeUTF("");
+        } else {
+            dos.writeUTF(string);
+        }
+    }
+
+    public static final String readUTF(DataInputStream dis) throws IOException {
+        String retVal = dis.readUTF();
+        if (retVal.length() == 0)
+            return null;
+        return retVal;
+    }
+}
diff --git a/src/com/android/gallery3d/util/SaveVideoFileInfo.java b/src/com/android/gallery3d/util/SaveVideoFileInfo.java
new file mode 100644
index 0000000..c7e5e85
--- /dev/null
+++ b/src/com/android/gallery3d/util/SaveVideoFileInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import java.io.File;
+
+public class SaveVideoFileInfo {
+    public File mFile = null;
+    public String mFileName = null;
+    // This the full directory path.
+    public File mDirectory = null;
+    // This is just the folder's name.
+    public String mFolderName = null;
+
+}
diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
new file mode 100644
index 0000000..10c41de
--- /dev/null
+++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.filtershow.tools.SaveImage.ContentResolverQueryCallback;
+
+import java.io.File;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+
+public class SaveVideoFileUtils {
+    // This function can decide which folder to save the video file, and generate
+    // the needed information for the video file including filename.
+    public static SaveVideoFileInfo getDstMp4FileInfo(String fileNameFormat,
+            ContentResolver contentResolver, Uri uri, String defaultFolderName) {
+        SaveVideoFileInfo dstFileInfo = new SaveVideoFileInfo();
+        // Use the default save directory if the source directory cannot be
+        // saved.
+        dstFileInfo.mDirectory = getSaveDirectory(contentResolver, uri);
+        if ((dstFileInfo.mDirectory == null) || !dstFileInfo.mDirectory.canWrite()) {
+            dstFileInfo.mDirectory = new File(Environment.getExternalStorageDirectory(),
+                    BucketNames.DOWNLOAD);
+            dstFileInfo.mFolderName = defaultFolderName;
+        } else {
+            dstFileInfo.mFolderName = dstFileInfo.mDirectory.getName();
+        }
+        dstFileInfo.mFileName = new SimpleDateFormat(fileNameFormat).format(
+                new Date(System.currentTimeMillis()));
+
+        dstFileInfo.mFile = new File(dstFileInfo.mDirectory, dstFileInfo.mFileName + ".mp4");
+        return dstFileInfo;
+    }
+
+    private static void querySource(ContentResolver contentResolver, Uri uri,
+            String[] projection, ContentResolverQueryCallback callback) {
+        Cursor cursor = null;
+        try {
+            cursor = contentResolver.query(uri, projection, null, null, null);
+            if ((cursor != null) && cursor.moveToNext()) {
+                callback.onCursorResult(cursor);
+            }
+        } catch (Exception e) {
+            // Ignore error for lacking the data column from the source.
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    private static File getSaveDirectory(ContentResolver contentResolver, Uri uri) {
+        final File[] dir = new File[1];
+        querySource(contentResolver, uri,
+                new String[] { VideoColumns.DATA },
+                new ContentResolverQueryCallback() {
+            @Override
+            public void onCursorResult(Cursor cursor) {
+                dir[0] = new File(cursor.getString(0)).getParentFile();
+            }
+        });
+        return dir[0];
+    }
+
+
+    /**
+     * Insert the content (saved file) with proper video properties.
+     */
+    public static Uri insertContent(SaveVideoFileInfo mDstFileInfo,
+            ContentResolver contentResolver, Uri uri ) {
+        long nowInMs = System.currentTimeMillis();
+        long nowInSec = nowInMs / 1000;
+        final ContentValues values = new ContentValues(13);
+        values.put(Video.Media.TITLE, mDstFileInfo.mFileName);
+        values.put(Video.Media.DISPLAY_NAME, mDstFileInfo.mFile.getName());
+        values.put(Video.Media.MIME_TYPE, "video/mp4");
+        values.put(Video.Media.DATE_TAKEN, nowInMs);
+        values.put(Video.Media.DATE_MODIFIED, nowInSec);
+        values.put(Video.Media.DATE_ADDED, nowInSec);
+        values.put(Video.Media.DATA, mDstFileInfo.mFile.getAbsolutePath());
+        values.put(Video.Media.SIZE, mDstFileInfo.mFile.length());
+        int durationMs = retriveVideoDurationMs(mDstFileInfo.mFile.getPath());
+        values.put(Video.Media.DURATION, durationMs);
+        // Copy the data taken and location info from src.
+        String[] projection = new String[] {
+                VideoColumns.DATE_TAKEN,
+                VideoColumns.LATITUDE,
+                VideoColumns.LONGITUDE,
+                VideoColumns.RESOLUTION,
+        };
+
+        // Copy some info from the source file.
+        querySource(contentResolver, uri, projection,
+                new ContentResolverQueryCallback() {
+                @Override
+                    public void onCursorResult(Cursor cursor) {
+                        long timeTaken = cursor.getLong(0);
+                        if (timeTaken > 0) {
+                            values.put(Video.Media.DATE_TAKEN, timeTaken);
+                        }
+                        double latitude = cursor.getDouble(1);
+                        double longitude = cursor.getDouble(2);
+                        // TODO: Change || to && after the default location
+                        // issue is
+                        // fixed.
+                        if ((latitude != 0f) || (longitude != 0f)) {
+                            values.put(Video.Media.LATITUDE, latitude);
+                            values.put(Video.Media.LONGITUDE, longitude);
+                        }
+                        values.put(Video.Media.RESOLUTION, cursor.getString(3));
+
+                    }
+                });
+
+        return contentResolver.insert(Video.Media.EXTERNAL_CONTENT_URI, values);
+    }
+
+    public static int retriveVideoDurationMs(String path) {
+        int durationMs = 0;
+        // Calculate the duration of the destination file.
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        retriever.setDataSource(path);
+        String duration = retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_DURATION);
+        if (duration != null) {
+            durationMs = Integer.parseInt(duration);
+        }
+        retriever.release();
+        return durationMs;
+    }
+
+}
diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java
new file mode 100644
index 0000000..f76705d
--- /dev/null
+++ b/src/com/android/gallery3d/util/UpdateHelper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import com.android.gallery3d.common.Utils;
+
+public class UpdateHelper {
+
+    private boolean mUpdated = false;
+
+    public int update(int original, int update) {
+        if (original != update) {
+            mUpdated = true;
+            original = update;
+        }
+        return original;
+    }
+
+    public long update(long original, long update) {
+        if (original != update) {
+            mUpdated = true;
+            original = update;
+        }
+        return original;
+    }
+
+    public double update(double original, double update) {
+        if (original != update) {
+            mUpdated = true;
+            original = update;
+        }
+        return original;
+    }
+
+    public <T> T update(T original, T update) {
+        if (!Utils.equals(original, update)) {
+            mUpdated = true;
+            original = update;
+        }
+        return original;
+    }
+
+    public boolean isUpdated() {
+        return mUpdated;
+    }
+}
diff --git a/src/com/android/photos/AlbumActivity.java b/src/com/android/photos/AlbumActivity.java
new file mode 100644
index 0000000..c616b99
--- /dev/null
+++ b/src/com/android/photos/AlbumActivity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class AlbumActivity extends Activity implements MultiChoiceManager.Provider {
+
+    public static final String KEY_ALBUM_URI = AlbumFragment.KEY_ALBUM_URI;
+    public static final String KEY_ALBUM_TITLE = AlbumFragment.KEY_ALBUM_TITLE;
+
+    private MultiChoiceManager mMultiChoiceManager;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Bundle intentExtras = getIntent().getExtras();
+        mMultiChoiceManager = new MultiChoiceManager(this);
+        if (savedInstanceState == null) {
+            AlbumFragment albumFragment = new AlbumFragment();
+            mMultiChoiceManager.setDelegate(albumFragment);
+            albumFragment.setArguments(intentExtras);
+            getFragmentManager().beginTransaction().add(android.R.id.content,
+                    albumFragment).commit();
+        }
+        getActionBar().setTitle(intentExtras.getString(KEY_ALBUM_TITLE));
+    }
+
+    @Override
+    public MultiChoiceManager getMultiChoiceManager() {
+        return mMultiChoiceManager;
+    }
+}
diff --git a/src/com/android/photos/AlbumFragment.java b/src/com/android/photos/AlbumFragment.java
new file mode 100644
index 0000000..406fd2a
--- /dev/null
+++ b/src/com/android/photos/AlbumFragment.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.photos.adapters.PhotoThumbnailAdapter;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.shims.MediaItemsLoader;
+import com.android.photos.views.HeaderGridView;
+
+import java.util.ArrayList;
+
+public class AlbumFragment extends MultiSelectGridFragment implements LoaderCallbacks<Cursor> {
+
+    protected static final String KEY_ALBUM_URI = "AlbumUri";
+    protected static final String KEY_ALBUM_TITLE = "AlbumTitle";
+    private static final int LOADER_ALBUM = 1;
+
+    private LoaderCompatShim<Cursor> mLoaderCompatShim;
+    private PhotoThumbnailAdapter mAdapter;
+    private String mAlbumPath;
+    private String mAlbumTitle;
+    private View mHeaderView;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Context context = getActivity();
+        mAdapter = new PhotoThumbnailAdapter(context);
+        Bundle args = getArguments();
+        if (args != null) {
+            mAlbumPath = args.getString(KEY_ALBUM_URI, null);
+            mAlbumTitle = args.getString(KEY_ALBUM_TITLE, null);
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        getLoaderManager().initLoader(LOADER_ALBUM, null, this);
+        return inflater.inflate(R.layout.album_content, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        // TODO: Remove once UI stabilizes
+        getGridView().setColumnWidth(MediaItemsLoader.getThumbnailSize());
+    }
+
+    private void updateHeaderView() {
+        if (mHeaderView == null) {
+            mHeaderView = LayoutInflater.from(getActivity())
+                    .inflate(R.layout.album_header, getGridView(), false);
+            ((HeaderGridView) getGridView()).addHeaderView(mHeaderView, null, false);
+
+            // TODO remove this when the data model stabilizes
+            mHeaderView.setMinimumHeight(200);
+        }
+        ImageView iv = (ImageView) mHeaderView.findViewById(R.id.album_header_image);
+        TextView title = (TextView) mHeaderView.findViewById(R.id.album_header_title);
+        TextView subtitle = (TextView) mHeaderView.findViewById(R.id.album_header_subtitle);
+        title.setText(mAlbumTitle);
+        int count = mAdapter.getCount();
+        subtitle.setText(getActivity().getResources().getQuantityString(
+                R.plurals.number_of_photos, count, count));
+        if (count > 0) {
+            iv.setImageDrawable(mLoaderCompatShim.drawableForItem(mAdapter.getItem(0), null));
+        }
+    }
+
+    @Override
+    public void onGridItemClick(GridView g, View v, int position, long id) {
+        if (mLoaderCompatShim == null) {
+            // Not fully initialized yet, discard
+            return;
+        }
+        Cursor item = (Cursor) getItemAtPosition(position);
+        Uri uri = mLoaderCompatShim.uriForItem(item);
+        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+        intent.setClass(getActivity(), Gallery.class);
+        startActivity(intent);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        // TODO: Switch to PhotoSetLoader
+        MediaItemsLoader loader = new MediaItemsLoader(getActivity(), mAlbumPath);
+        mLoaderCompatShim = loader;
+        mAdapter.setDrawableFactory(mLoaderCompatShim);
+        return loader;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader,
+            Cursor data) {
+        mAdapter.swapCursor(data);
+        updateHeaderView();
+        setAdapter(mAdapter);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    @Override
+    public int getItemMediaType(Object item) {
+        return ((Cursor) item).getInt(PhotoSetLoader.INDEX_MEDIA_TYPE);
+    }
+
+    @Override
+    public int getItemSupportedOperations(Object item) {
+        return ((Cursor) item).getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS);
+    }
+
+    private ArrayList<Uri> mSubItemUriTemp = new ArrayList<Uri>(1);
+    @Override
+    public ArrayList<Uri> getSubItemUrisForItem(Object item) {
+        mSubItemUriTemp.clear();
+        mSubItemUriTemp.add(mLoaderCompatShim.uriForItem((Cursor) item));
+        return mSubItemUriTemp;
+    }
+
+    @Override
+    public void deleteItemWithPath(Object itemPath) {
+        mLoaderCompatShim.deleteItemWithPath(itemPath);
+    }
+
+    @Override
+    public Uri getItemUri(Object item) {
+        return mLoaderCompatShim.uriForItem((Cursor) item);
+    }
+
+    @Override
+    public Object getPathForItem(Object item) {
+        return mLoaderCompatShim.getPathForItem((Cursor) item);
+    }
+}
diff --git a/src/com/android/photos/AlbumSetFragment.java b/src/com/android/photos/AlbumSetFragment.java
new file mode 100644
index 0000000..bc5289e
--- /dev/null
+++ b/src/com/android/photos/AlbumSetFragment.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore.Files.FileColumns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+
+import com.android.gallery3d.R;
+import com.android.photos.adapters.AlbumSetCursorAdapter;
+import com.android.photos.data.AlbumSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.shims.MediaSetLoader;
+
+import java.util.ArrayList;
+
+
+public class AlbumSetFragment extends MultiSelectGridFragment implements LoaderCallbacks<Cursor> {
+
+    private AlbumSetCursorAdapter mAdapter;
+    private LoaderCompatShim<Cursor> mLoaderCompatShim;
+
+    private static final int LOADER_ALBUMSET = 1;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Context context = getActivity();
+        mAdapter = new AlbumSetCursorAdapter(context);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View root = super.onCreateView(inflater, container, savedInstanceState);
+        getLoaderManager().initLoader(LOADER_ALBUMSET, null, this);
+        return root;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        getGridView().setColumnWidth(getActivity().getResources()
+                .getDimensionPixelSize(R.dimen.album_set_item_width));
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        // TODO: Switch to AlbumSetLoader
+        MediaSetLoader loader = new MediaSetLoader(getActivity());
+        mAdapter.setDrawableFactory(loader);
+        mLoaderCompatShim = loader;
+        return loader;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader,
+            Cursor data) {
+        mAdapter.swapCursor(data);
+        setAdapter(mAdapter);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    @Override
+    public void onGridItemClick(GridView g, View v, int position, long id) {
+        if (mLoaderCompatShim == null) {
+            // Not fully initialized yet, discard
+            return;
+        }
+        Cursor item = (Cursor) getItemAtPosition(position);
+        Context context = getActivity();
+        Intent intent = new Intent(context, AlbumActivity.class);
+        intent.putExtra(AlbumActivity.KEY_ALBUM_URI,
+                mLoaderCompatShim.getPathForItem(item).toString());
+        intent.putExtra(AlbumActivity.KEY_ALBUM_TITLE,
+                item.getString(AlbumSetLoader.INDEX_TITLE));
+        context.startActivity(intent);
+    }
+
+    @Override
+    public int getItemMediaType(Object item) {
+        return FileColumns.MEDIA_TYPE_NONE;
+    }
+
+    @Override
+    public int getItemSupportedOperations(Object item) {
+        return ((Cursor) item).getInt(AlbumSetLoader.INDEX_SUPPORTED_OPERATIONS);
+    }
+
+    @Override
+    public ArrayList<Uri> getSubItemUrisForItem(Object item) {
+        return mLoaderCompatShim.urisForSubItems((Cursor) item);
+    }
+
+    @Override
+    public void deleteItemWithPath(Object itemPath) {
+        mLoaderCompatShim.deleteItemWithPath(itemPath);
+    }
+
+    @Override
+    public Uri getItemUri(Object item) {
+        return mLoaderCompatShim.uriForItem((Cursor) item);
+    }
+
+    @Override
+    public Object getPathForItem(Object item) {
+        return mLoaderCompatShim.getPathForItem((Cursor) item);
+    }
+}
diff --git a/src/com/android/photos/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java
new file mode 100644
index 0000000..d7d52f6
--- /dev/null
+++ b/src/com/android/photos/BitmapRegionTileSource.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.photos.views.TiledImageRenderer;
+
+import java.io.IOException;
+
+/**
+ * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
+ * {@link BitmapRegionDecoder} to wrap a local file
+ */
+public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
+
+    private static final String TAG = "BitmapRegionTileSource";
+
+    private static final boolean REUSE_BITMAP =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+    private static final int MAX_PREVIEW_SIZE = 1024;
+
+    BitmapRegionDecoder mDecoder;
+    int mWidth;
+    int mHeight;
+    int mTileSize;
+    private BasicTexture mPreview;
+    private final int mRotation;
+
+    // For use only by getTile
+    private Rect mWantRegion = new Rect();
+    private Rect mOverlapRegion = new Rect();
+    private BitmapFactory.Options mOptions;
+    private Canvas mCanvas;
+
+    public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
+        mTileSize = TiledImageRenderer.suggestedTileSize(context);
+        mRotation = rotation;
+        try {
+            mDecoder = BitmapRegionDecoder.newInstance(path, true);
+            mWidth = mDecoder.getWidth();
+            mHeight = mDecoder.getHeight();
+        } catch (IOException e) {
+            Log.w("BitmapRegionTileSource", "ctor failed", e);
+        }
+        mOptions = new BitmapFactory.Options();
+        mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        mOptions.inPreferQualityOverSpeed = true;
+        mOptions.inTempStorage = new byte[16 * 1024];
+        if (previewSize != 0) {
+            previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
+            // Although this is the same size as the Bitmap that is likely already
+            // loaded, the lifecycle is different and interactions are on a different
+            // thread. Thus to simplify, this source will decode its own bitmap.
+            int sampleSize = (int) Math.ceil(Math.max(
+                    mWidth / (float) previewSize, mHeight / (float) previewSize));
+            mOptions.inSampleSize = Math.max(sampleSize, 1);
+            Bitmap preview = mDecoder.decodeRegion(
+                    new Rect(0, 0, mWidth, mHeight), mOptions);
+            if (preview.getWidth() <= MAX_PREVIEW_SIZE && preview.getHeight() <= MAX_PREVIEW_SIZE) {
+                mPreview = new BitmapTexture(preview);
+            } else {
+                Log.w(TAG, String.format(
+                        "Failed to create preview of apropriate size! "
+                        + " in: %dx%d, sample: %d, out: %dx%d",
+                        mWidth, mHeight, sampleSize,
+                        preview.getWidth(), preview.getHeight()));
+            }
+        }
+    }
+
+    @Override
+    public int getTileSize() {
+        return mTileSize;
+    }
+
+    @Override
+    public int getImageWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getImageHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public BasicTexture getPreview() {
+        return mPreview;
+    }
+
+    @Override
+    public int getRotation() {
+        return mRotation;
+    }
+
+    @Override
+    public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
+        int tileSize = getTileSize();
+        if (!REUSE_BITMAP) {
+            return getTileWithoutReusingBitmap(level, x, y, tileSize);
+        }
+
+        int t = tileSize << level;
+        mWantRegion.set(x, y, x + t, y + t);
+
+        if (bitmap == null) {
+            bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
+        }
+
+        mOptions.inSampleSize = (1 << level);
+        mOptions.inBitmap = bitmap;
+
+        try {
+            bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
+        } finally {
+            if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
+                mOptions.inBitmap = null;
+            }
+        }
+
+        if (bitmap == null) {
+            Log.w("BitmapRegionTileSource", "fail in decoding region");
+        }
+        return bitmap;
+    }
+
+    private Bitmap getTileWithoutReusingBitmap(
+            int level, int x, int y, int tileSize) {
+
+        int t = tileSize << level;
+        mWantRegion.set(x, y, x + t, y + t);
+
+        mOverlapRegion.set(0, 0, mWidth, mHeight);
+
+        mOptions.inSampleSize = (1 << level);
+        Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
+
+        if (bitmap == null) {
+            Log.w(TAG, "fail in decoding region");
+        }
+
+        if (mWantRegion.equals(mOverlapRegion)) {
+            return bitmap;
+        }
+
+        Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+        if (mCanvas == null) {
+            mCanvas = new Canvas();
+        }
+        mCanvas.setBitmap(result);
+        mCanvas.drawBitmap(bitmap,
+                (mOverlapRegion.left - mWantRegion.left) >> level,
+                (mOverlapRegion.top - mWantRegion.top) >> level, null);
+        mCanvas.setBitmap(null);
+        return result;
+    }
+}
diff --git a/src/com/android/photos/FullscreenViewer.java b/src/com/android/photos/FullscreenViewer.java
new file mode 100644
index 0000000..a376139
--- /dev/null
+++ b/src/com/android/photos/FullscreenViewer.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.os.Bundle;
+import com.android.photos.views.TiledImageView;
+
+
+public class FullscreenViewer extends Activity {
+
+    private TiledImageView mTextureView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        String path = getIntent().getData().toString();
+        mTextureView = new TiledImageView(this);
+        mTextureView.setTileSource(new BitmapRegionTileSource(this, path, 0, 0), null);
+        setContentView(mTextureView);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mTextureView.destroy();
+    }
+
+}
diff --git a/src/com/android/photos/GalleryActivity.java b/src/com/android/photos/GalleryActivity.java
new file mode 100644
index 0000000..710767d
--- /dev/null
+++ b/src/com/android/photos/GalleryActivity.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class GalleryActivity extends Activity implements MultiChoiceManager.Provider {
+
+    private MultiChoiceManager mMultiChoiceManager;
+    private ViewPager mViewPager;
+    private TabsAdapter mTabsAdapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mMultiChoiceManager = new MultiChoiceManager(this);
+        mViewPager = new ViewPager(this);
+        mViewPager.setId(R.id.viewpager);
+        setContentView(mViewPager);
+
+        ActionBar ab = getActionBar();
+        ab.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+        ab.setDisplayShowHomeEnabled(false);
+        ab.setDisplayShowTitleEnabled(false);
+
+        mTabsAdapter = new TabsAdapter(this, mViewPager);
+        mTabsAdapter.addTab(ab.newTab().setText(R.string.tab_photos),
+                PhotoSetFragment.class, null);
+        mTabsAdapter.addTab(ab.newTab().setText(R.string.tab_albums),
+                AlbumSetFragment.class, null);
+
+        if (savedInstanceState != null) {
+            ab.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.gallery, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+        case R.id.menu_camera:
+            Intent intent = new Intent(this, CameraActivity.class);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            startActivity(intent);
+            return true;
+        default:
+            return super.onOptionsItemSelected(item);
+        }
+    }
+
+    public static class TabsAdapter extends FragmentPagerAdapter implements
+            ActionBar.TabListener, ViewPager.OnPageChangeListener {
+
+        private final GalleryActivity mActivity;
+        private final ActionBar mActionBar;
+        private final ViewPager mViewPager;
+        private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+        static final class TabInfo {
+
+            private final Class<?> clss;
+            private final Bundle args;
+
+            TabInfo(Class<?> _class, Bundle _args) {
+                clss = _class;
+                args = _args;
+            }
+        }
+
+        public TabsAdapter(GalleryActivity activity, ViewPager pager) {
+            super(activity.getFragmentManager());
+            mActivity = activity;
+            mActionBar = activity.getActionBar();
+            mViewPager = pager;
+            mViewPager.setAdapter(this);
+            mViewPager.setOnPageChangeListener(this);
+        }
+
+        public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args) {
+            TabInfo info = new TabInfo(clss, args);
+            tab.setTag(info);
+            tab.setTabListener(this);
+            mTabs.add(info);
+            mActionBar.addTab(tab);
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getCount() {
+            return mTabs.size();
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            TabInfo info = mTabs.get(position);
+            return Fragment.instantiate(mActivity, info.clss.getName(),
+                    info.args);
+        }
+
+        @Override
+        public void onPageScrolled(int position, float positionOffset,
+                int positionOffsetPixels) {
+        }
+
+        @Override
+        public void onPageSelected(int position) {
+            mActionBar.setSelectedNavigationItem(position);
+        }
+
+        @Override
+        public void setPrimaryItem(ViewGroup container, int position, Object object) {
+            super.setPrimaryItem(container, position, object);
+            mActivity.mMultiChoiceManager.setDelegate((MultiChoiceManager.Delegate) object);
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) {
+        }
+
+        @Override
+        public void onTabSelected(Tab tab, FragmentTransaction ft) {
+            Object tag = tab.getTag();
+            for (int i = 0; i < mTabs.size(); i++) {
+                if (mTabs.get(i) == tag) {
+                    mViewPager.setCurrentItem(i);
+                }
+            }
+        }
+
+        @Override
+        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+        }
+
+        @Override
+        public void onTabReselected(Tab tab, FragmentTransaction ft) {
+        }
+    }
+
+    @Override
+    public MultiChoiceManager getMultiChoiceManager() {
+        return mMultiChoiceManager;
+    }
+}
diff --git a/src/com/android/photos/MultiChoiceManager.java b/src/com/android/photos/MultiChoiceManager.java
new file mode 100644
index 0000000..49519ca
--- /dev/null
+++ b/src/com/android/photos/MultiChoiceManager.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore.Files.FileColumns;
+import android.util.SparseBooleanArray;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.ShareActionProvider;
+import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.TrimVideo;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiChoiceManager implements MultiChoiceModeListener,
+    OnShareTargetSelectedListener, SelectionManager.SelectedUriSource {
+
+    public interface Provider {
+        public MultiChoiceManager getMultiChoiceManager();
+    }
+
+    public interface Delegate {
+        public SparseBooleanArray getSelectedItemPositions();
+        public int getSelectedItemCount();
+        public int getItemMediaType(Object item);
+        public int getItemSupportedOperations(Object item);
+        public ArrayList<Uri> getSubItemUrisForItem(Object item);
+        public Uri getItemUri(Object item);
+        public Object getItemAtPosition(int position);
+        public Object getPathForItemAtPosition(int position);
+        public void deleteItemWithPath(Object itemPath);
+    }
+
+    private SelectionManager mSelectionManager;
+    private ShareActionProvider mShareActionProvider;
+    private ActionMode mActionMode;
+    private Context mContext;
+    private Delegate mDelegate;
+
+    private ArrayList<Uri> mSelectedShareableUrisArray = new ArrayList<Uri>();
+
+    public MultiChoiceManager(Activity activity) {
+        mContext = activity;
+        mSelectionManager = new SelectionManager(activity);
+    }
+
+    public void setDelegate(Delegate delegate) {
+        if (mDelegate == delegate) {
+            return;
+        }
+        if (mActionMode != null) {
+            mActionMode.finish();
+        }
+        mDelegate = delegate;
+    }
+
+    @Override
+    public ArrayList<Uri> getSelectedShareableUris() {
+        return mSelectedShareableUrisArray;
+    }
+
+    private void updateSelectedTitle(ActionMode mode) {
+        int count = mDelegate.getSelectedItemCount();
+        mode.setTitle(mContext.getResources().getQuantityString(
+                R.plurals.number_of_items_selected, count, count));
+    }
+
+    private String getItemMimetype(Object item) {
+        int type = mDelegate.getItemMediaType(item);
+        if (type == FileColumns.MEDIA_TYPE_IMAGE) {
+            return GalleryUtils.MIME_TYPE_IMAGE;
+        } else if (type == FileColumns.MEDIA_TYPE_VIDEO) {
+            return GalleryUtils.MIME_TYPE_VIDEO;
+        } else {
+            return GalleryUtils.MIME_TYPE_ALL;
+        }
+    }
+
+    @Override
+    public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+            boolean checked) {
+        updateSelectedTitle(mode);
+        Object item = mDelegate.getItemAtPosition(position);
+
+        int supported = mDelegate.getItemSupportedOperations(item);
+
+        if ((supported & MediaObject.SUPPORT_SHARE) > 0) {
+            ArrayList<Uri> subItems = mDelegate.getSubItemUrisForItem(item);
+            if (checked) {
+                mSelectedShareableUrisArray.addAll(subItems);
+            } else {
+                mSelectedShareableUrisArray.removeAll(subItems);
+            }
+        }
+
+        mSelectionManager.onItemSelectedStateChanged(mShareActionProvider,
+                mDelegate.getItemMediaType(item),
+                supported,
+                checked);
+        updateActionItemVisibilities(mode.getMenu(),
+                mSelectionManager.getSupportedOperations());
+    }
+
+    private void updateActionItemVisibilities(Menu menu, int supportedOperations) {
+        MenuItem editItem = menu.findItem(R.id.menu_edit);
+        MenuItem deleteItem = menu.findItem(R.id.menu_delete);
+        MenuItem shareItem = menu.findItem(R.id.menu_share);
+        MenuItem cropItem = menu.findItem(R.id.menu_crop);
+        MenuItem trimItem = menu.findItem(R.id.menu_trim);
+        MenuItem muteItem = menu.findItem(R.id.menu_mute);
+        MenuItem setAsItem = menu.findItem(R.id.menu_set_as);
+
+        editItem.setVisible((supportedOperations & MediaObject.SUPPORT_EDIT) > 0);
+        deleteItem.setVisible((supportedOperations & MediaObject.SUPPORT_DELETE) > 0);
+        shareItem.setVisible((supportedOperations & MediaObject.SUPPORT_SHARE) > 0);
+        cropItem.setVisible((supportedOperations & MediaObject.SUPPORT_CROP) > 0);
+        trimItem.setVisible((supportedOperations & MediaObject.SUPPORT_TRIM) > 0);
+        muteItem.setVisible((supportedOperations & MediaObject.SUPPORT_MUTE) > 0);
+        setAsItem.setVisible((supportedOperations & MediaObject.SUPPORT_SETAS) > 0);
+    }
+
+    @Override
+    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+        mSelectionManager.setSelectedUriSource(this);
+        mActionMode = mode;
+        MenuInflater inflater = mode.getMenuInflater();
+        inflater.inflate(R.menu.gallery_multiselect, menu);
+        MenuItem menuItem = menu.findItem(R.id.menu_share);
+        mShareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
+        mShareActionProvider.setOnShareTargetSelectedListener(this);
+        updateSelectedTitle(mode);
+        return true;
+    }
+
+    @Override
+    public void onDestroyActionMode(ActionMode mode) {
+        // onDestroyActionMode gets called when the share target was selected,
+        // but apparently before the ArrayList is serialized in the intent
+        // so we can't clear the old one here.
+        mSelectedShareableUrisArray = new ArrayList<Uri>();
+        mSelectionManager.onClearSelection();
+        mSelectionManager.setSelectedUriSource(null);
+        mShareActionProvider = null;
+        mActionMode = null;
+    }
+
+    @Override
+    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+        updateSelectedTitle(mode);
+        return false;
+    }
+
+    @Override
+    public boolean onShareTargetSelected(ShareActionProvider provider, Intent intent) {
+        mActionMode.finish();
+        return false;
+    }
+
+    private static class BulkDeleteTask extends AsyncTask<Void, Void, Void> {
+        private Delegate mDelegate;
+        private List<Object> mPaths;
+
+        public BulkDeleteTask(Delegate delegate, List<Object> paths) {
+            mDelegate = delegate;
+            mPaths = paths;
+        }
+
+        @Override
+        protected Void doInBackground(Void... ignored) {
+            for (Object path : mPaths) {
+                mDelegate.deleteItemWithPath(path);
+            }
+            return null;
+        }
+    }
+
+    @Override
+    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+        int actionItemId = item.getItemId();
+        switch (actionItemId) {
+            case R.id.menu_delete:
+                BulkDeleteTask deleteTask = new BulkDeleteTask(mDelegate,
+                        getPathsForSelectedItems());
+                deleteTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+                mode.finish();
+                return true;
+            case R.id.menu_edit:
+            case R.id.menu_crop:
+            case R.id.menu_trim:
+            case R.id.menu_mute:
+            case R.id.menu_set_as:
+                singleItemAction(getSelectedItem(), actionItemId);
+                mode.finish();
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private void singleItemAction(Object item, int actionItemId) {
+        Intent intent = new Intent();
+        String mime = getItemMimetype(item);
+        Uri uri = mDelegate.getItemUri(item);
+        switch (actionItemId) {
+            case R.id.menu_edit:
+                intent.setDataAndType(uri, mime)
+                      .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                      .setAction(Intent.ACTION_EDIT);
+                mContext.startActivity(Intent.createChooser(intent, null));
+                return;
+            case R.id.menu_crop:
+                intent.setDataAndType(uri, mime)
+                      .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                      .setAction(CropActivity.CROP_ACTION)
+                      .setClass(mContext, FilterShowActivity.class);
+                mContext.startActivity(intent);
+                return;
+            case R.id.menu_trim:
+                intent.setData(uri)
+                      .setClass(mContext, TrimVideo.class);
+                mContext.startActivity(intent);
+                return;
+            case R.id.menu_mute:
+                /* TODO need a way to get the file path of an item
+                MuteVideo muteVideo = new MuteVideo(filePath,
+                        uri, (Activity) mContext);
+                muteVideo.muteInBackground();
+                */
+                return;
+            case R.id.menu_set_as:
+                intent.setDataAndType(uri, mime)
+                      .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                      .setAction(Intent.ACTION_ATTACH_DATA)
+                      .putExtra("mimeType", mime);
+                mContext.startActivity(Intent.createChooser(
+                        intent, mContext.getString(R.string.set_as)));
+                return;
+            default:
+                return;
+        }
+    }
+
+    private List<Object> getPathsForSelectedItems() {
+        List<Object> paths = new ArrayList<Object>();
+        SparseBooleanArray selected = mDelegate.getSelectedItemPositions();
+        for (int i = 0; i < selected.size(); i++) {
+            if (selected.valueAt(i)) {
+                paths.add(mDelegate.getPathForItemAtPosition(i));
+            }
+        }
+        return paths;
+    }
+
+    public Object getSelectedItem() {
+        if (mDelegate.getSelectedItemCount() != 1) {
+            return null;
+        }
+        SparseBooleanArray selected = mDelegate.getSelectedItemPositions();
+        for (int i = 0; i < selected.size(); i++) {
+            if (selected.valueAt(i)) {
+                return mDelegate.getItemAtPosition(selected.keyAt(i));
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/photos/MultiSelectGridFragment.java b/src/com/android/photos/MultiSelectGridFragment.java
new file mode 100644
index 0000000..dda9fe4
--- /dev/null
+++ b/src/com/android/photos/MultiSelectGridFragment.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+public abstract class MultiSelectGridFragment extends Fragment
+        implements MultiChoiceManager.Delegate, AdapterView.OnItemClickListener {
+
+    final private Handler mHandler = new Handler();
+
+    final private Runnable mRequestFocus = new Runnable() {
+        @Override
+        public void run() {
+            mGrid.focusableViewAvailable(mGrid);
+        }
+    };
+
+    ListAdapter mAdapter;
+    GridView mGrid;
+    TextView mEmptyView;
+    View mProgressContainer;
+    View mGridContainer;
+    CharSequence mEmptyText;
+    boolean mGridShown;
+    MultiChoiceManager.Provider mHost;
+
+    public MultiSelectGridFragment() {
+    }
+
+    /**
+     * Provide default implementation to return a simple grid view. Subclasses
+     * can override to replace with their own layout. If doing so, the returned
+     * view hierarchy <em>must</em> have a GridView whose id is
+     * {@link android.R.id#grid android.R.id.list} and can optionally have a
+     * sibling text view id {@link android.R.id#empty android.R.id.empty} that
+     * is to be shown when the grid is empty.
+     */
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.multigrid_content, container, false);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mHost = (MultiChoiceManager.Provider) activity;
+        if (mGrid != null) {
+            mGrid.setMultiChoiceModeListener(mHost.getMultiChoiceManager());
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mHost = null;
+    }
+
+    /**
+     * Attach to grid view once the view hierarchy has been created.
+     */
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        ensureGrid();
+    }
+
+    /**
+     * Detach from grid view.
+     */
+    @Override
+    public void onDestroyView() {
+        mHandler.removeCallbacks(mRequestFocus);
+        mGrid = null;
+        mGridShown = false;
+        mEmptyView = null;
+        mProgressContainer = mGridContainer = null;
+        super.onDestroyView();
+    }
+
+    /**
+     * This method will be called when an item in the grid is selected.
+     * Subclasses should override. Subclasses can call
+     * getGridView().getItemAtPosition(position) if they need to access the data
+     * associated with the selected item.
+     *
+     * @param g The GridView where the click happened
+     * @param v The view that was clicked within the GridView
+     * @param position The position of the view in the grid
+     * @param id The id of the item that was clicked
+     */
+    public void onGridItemClick(GridView g, View v, int position, long id) {
+    }
+
+    /**
+     * Provide the cursor for the grid view.
+     */
+    public void setAdapter(ListAdapter adapter) {
+        boolean hadAdapter = mAdapter != null;
+        mAdapter = adapter;
+        if (mGrid != null) {
+            mGrid.setAdapter(adapter);
+            if (!mGridShown && !hadAdapter) {
+                // The grid was hidden, and previously didn't have an
+                // adapter. It is now time to show it.
+                setGridShown(true, getView().getWindowToken() != null);
+            }
+        }
+    }
+
+    /**
+     * Set the currently selected grid item to the specified position with the
+     * adapter's data
+     *
+     * @param position
+     */
+    public void setSelection(int position) {
+        ensureGrid();
+        mGrid.setSelection(position);
+    }
+
+    /**
+     * Get the position of the currently selected grid item.
+     */
+    public int getSelectedItemPosition() {
+        ensureGrid();
+        return mGrid.getSelectedItemPosition();
+    }
+
+    /**
+     * Get the cursor row ID of the currently selected grid item.
+     */
+    public long getSelectedItemId() {
+        ensureGrid();
+        return mGrid.getSelectedItemId();
+    }
+
+    /**
+     * Get the activity's grid view widget.
+     */
+    public GridView getGridView() {
+        ensureGrid();
+        return mGrid;
+    }
+
+    /**
+     * The default content for a MultiSelectGridFragment has a TextView that can
+     * be shown when the grid is empty. If you would like to have it shown, call
+     * this method to supply the text it should use.
+     */
+    public void setEmptyText(CharSequence text) {
+        ensureGrid();
+        if (mEmptyView == null) {
+            return;
+        }
+        mEmptyView.setText(text);
+        if (mEmptyText == null) {
+            mGrid.setEmptyView(mEmptyView);
+        }
+        mEmptyText = text;
+    }
+
+    /**
+     * Control whether the grid is being displayed. You can make it not
+     * displayed if you are waiting for the initial data to show in it. During
+     * this time an indeterminate progress indicator will be shown instead.
+     * <p>
+     * Applications do not normally need to use this themselves. The default
+     * behavior of MultiSelectGridFragment is to start with the grid not being
+     * shown, only showing it once an adapter is given with
+     * {@link #setAdapter(ListAdapter)}. If the grid at that point had not been
+     * shown, when it does get shown it will be do without the user ever seeing
+     * the hidden state.
+     *
+     * @param shown If true, the grid view is shown; if false, the progress
+     *            indicator. The initial value is true.
+     */
+    public void setGridShown(boolean shown) {
+        setGridShown(shown, true);
+    }
+
+    /**
+     * Like {@link #setGridShown(boolean)}, but no animation is used when
+     * transitioning from the previous state.
+     */
+    public void setGridShownNoAnimation(boolean shown) {
+        setGridShown(shown, false);
+    }
+
+    /**
+     * Control whether the grid is being displayed. You can make it not
+     * displayed if you are waiting for the initial data to show in it. During
+     * this time an indeterminate progress indicator will be shown instead.
+     *
+     * @param shown If true, the grid view is shown; if false, the progress
+     *            indicator. The initial value is true.
+     * @param animate If true, an animation will be used to transition to the
+     *            new state.
+     */
+    private void setGridShown(boolean shown, boolean animate) {
+        ensureGrid();
+        if (mProgressContainer == null) {
+            throw new IllegalStateException("Can't be used with a custom content view");
+        }
+        if (mGridShown == shown) {
+            return;
+        }
+        mGridShown = shown;
+        if (shown) {
+            if (animate) {
+                mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_out));
+                mGridContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_in));
+            } else {
+                mProgressContainer.clearAnimation();
+                mGridContainer.clearAnimation();
+            }
+            mProgressContainer.setVisibility(View.GONE);
+            mGridContainer.setVisibility(View.VISIBLE);
+        } else {
+            if (animate) {
+                mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_in));
+                mGridContainer.startAnimation(AnimationUtils.loadAnimation(
+                        getActivity(), android.R.anim.fade_out));
+            } else {
+                mProgressContainer.clearAnimation();
+                mGridContainer.clearAnimation();
+            }
+            mProgressContainer.setVisibility(View.VISIBLE);
+            mGridContainer.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Get the ListAdapter associated with this activity's GridView.
+     */
+    public ListAdapter getAdapter() {
+        return mGrid.getAdapter();
+    }
+
+    private void ensureGrid() {
+        if (mGrid != null) {
+            return;
+        }
+        View root = getView();
+        if (root == null) {
+            throw new IllegalStateException("Content view not yet created");
+        }
+        if (root instanceof GridView) {
+            mGrid = (GridView) root;
+        } else {
+            View empty = root.findViewById(android.R.id.empty);
+            if (empty != null && empty instanceof TextView) {
+                mEmptyView = (TextView) empty;
+            }
+            mProgressContainer = root.findViewById(R.id.progressContainer);
+            mGridContainer = root.findViewById(R.id.gridContainer);
+            View rawGridView = root.findViewById(android.R.id.list);
+            if (!(rawGridView instanceof GridView)) {
+                throw new RuntimeException(
+                        "Content has view with id attribute 'android.R.id.list' "
+                                + "that is not a GridView class");
+            }
+            mGrid = (GridView) rawGridView;
+            if (mGrid == null) {
+                throw new RuntimeException(
+                        "Your content must have a GridView whose id attribute is " +
+                                "'android.R.id.list'");
+            }
+            if (mEmptyView != null) {
+                mGrid.setEmptyView(mEmptyView);
+            }
+        }
+        mGridShown = true;
+        mGrid.setOnItemClickListener(this);
+        mGrid.setMultiChoiceModeListener(mHost.getMultiChoiceManager());
+        if (mAdapter != null) {
+            ListAdapter adapter = mAdapter;
+            mAdapter = null;
+            setAdapter(adapter);
+        } else {
+            // We are starting without an adapter, so assume we won't
+            // have our data right away and start with the progress indicator.
+            if (mProgressContainer != null) {
+                setGridShown(false, false);
+            }
+        }
+        mHandler.post(mRequestFocus);
+    }
+
+    @Override
+    public Object getItemAtPosition(int position) {
+        return getAdapter().getItem(position);
+    }
+
+    @Override
+    public Object getPathForItemAtPosition(int position) {
+        return getPathForItem(getItemAtPosition(position));
+    }
+
+    @Override
+    public SparseBooleanArray getSelectedItemPositions() {
+        return mGrid.getCheckedItemPositions();
+    }
+
+    @Override
+    public int getSelectedItemCount() {
+        return mGrid.getCheckedItemCount();
+    }
+
+    public abstract Object getPathForItem(Object item);
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+        onGridItemClick((GridView) parent, v, position, id);
+    }
+}
diff --git a/src/com/android/photos/PhotoFragment.java b/src/com/android/photos/PhotoFragment.java
new file mode 100644
index 0000000..3be6313
--- /dev/null
+++ b/src/com/android/photos/PhotoFragment.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Fragment;
+
+
+public class PhotoFragment extends Fragment {
+
+}
diff --git a/src/com/android/photos/PhotoSetFragment.java b/src/com/android/photos/PhotoSetFragment.java
new file mode 100644
index 0000000..961fd0b
--- /dev/null
+++ b/src/com/android/photos/PhotoSetFragment.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+
+import com.android.gallery3d.app.Gallery;
+import com.android.photos.adapters.PhotoThumbnailAdapter;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.shims.MediaItemsLoader;
+
+import java.util.ArrayList;
+
+public class PhotoSetFragment extends MultiSelectGridFragment implements LoaderCallbacks<Cursor> {
+
+    private static final int LOADER_PHOTOSET = 1;
+
+    private LoaderCompatShim<Cursor> mLoaderCompatShim;
+    private PhotoThumbnailAdapter mAdapter;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Context context = getActivity();
+        mAdapter = new PhotoThumbnailAdapter(context);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View root = super.onCreateView(inflater, container, savedInstanceState);
+        getLoaderManager().initLoader(LOADER_PHOTOSET, null, this);
+        return root;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        // TODO: Remove once UI stabilizes
+        getGridView().setColumnWidth(MediaItemsLoader.getThumbnailSize());
+    }
+
+    @Override
+    public void onGridItemClick(GridView g, View v, int position, long id) {
+        if (mLoaderCompatShim == null) {
+            // Not fully initialized yet, discard
+            return;
+        }
+        Cursor item = (Cursor) getItemAtPosition(position);
+        Uri uri = mLoaderCompatShim.uriForItem(item);
+        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+        intent.setClass(getActivity(), Gallery.class);
+        startActivity(intent);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        // TODO: Switch to PhotoSetLoader
+        MediaItemsLoader loader = new MediaItemsLoader(getActivity());
+        mLoaderCompatShim = loader;
+        mAdapter.setDrawableFactory(mLoaderCompatShim);
+        return loader;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader,
+            Cursor data) {
+        mAdapter.swapCursor(data);
+        setAdapter(mAdapter);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    @Override
+    public int getItemMediaType(Object item) {
+        return ((Cursor) item).getInt(PhotoSetLoader.INDEX_MEDIA_TYPE);
+    }
+
+    @Override
+    public int getItemSupportedOperations(Object item) {
+        return ((Cursor) item).getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS);
+    }
+
+    private ArrayList<Uri> mSubItemUriTemp = new ArrayList<Uri>(1);
+    @Override
+    public ArrayList<Uri> getSubItemUrisForItem(Object item) {
+        mSubItemUriTemp.clear();
+        mSubItemUriTemp.add(mLoaderCompatShim.uriForItem((Cursor) item));
+        return mSubItemUriTemp;
+    }
+
+    @Override
+    public void deleteItemWithPath(Object itemPath) {
+        mLoaderCompatShim.deleteItemWithPath(itemPath);
+    }
+
+    @Override
+    public Uri getItemUri(Object item) {
+        return mLoaderCompatShim.uriForItem((Cursor) item);
+    }
+
+    @Override
+    public Object getPathForItem(Object item) {
+        return mLoaderCompatShim.getPathForItem((Cursor) item);
+    }
+}
diff --git a/src/com/android/photos/SelectionManager.java b/src/com/android/photos/SelectionManager.java
new file mode 100644
index 0000000..9bfb9be
--- /dev/null
+++ b/src/com/android/photos/SelectionManager.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateBeamUrisCallback;
+import android.nfc.NfcEvent;
+import android.provider.MediaStore.Files.FileColumns;
+import android.widget.ShareActionProvider;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+
+public class SelectionManager {
+    private Activity mActivity;
+    private NfcAdapter mNfcAdapter;
+    private SelectedUriSource mUriSource;
+    private Intent mShareIntent = new Intent();
+
+    public interface SelectedUriSource {
+        public ArrayList<Uri> getSelectedShareableUris();
+    }
+
+    public SelectionManager(Activity activity) {
+        mActivity = activity;
+        if (ApiHelper.AT_LEAST_16) {
+            mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity);
+            mNfcAdapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() {
+                @Override
+                public Uri[] createBeamUris(NfcEvent arg0) {
+                 // This will have been preceded by a call to onItemSelectedStateChange
+                    if (mCachedShareableUris == null) return null;
+                    return mCachedShareableUris.toArray(
+                            new Uri[mCachedShareableUris.size()]);
+                }
+            }, mActivity);
+        }
+    }
+
+    public void setSelectedUriSource(SelectedUriSource source) {
+        mUriSource = source;
+    }
+
+    private int mSelectedTotalCount = 0;
+    private int mSelectedShareableCount = 0;
+    private int mSelectedShareableImageCount = 0;
+    private int mSelectedShareableVideoCount = 0;
+    private int mSelectedDeletableCount = 0;
+    private int mSelectedEditableCount = 0;
+    private int mSelectedCroppableCount = 0;
+    private int mSelectedSetableCount = 0;
+    private int mSelectedTrimmableCount = 0;
+    private int mSelectedMuteableCount = 0;
+
+    private ArrayList<Uri> mCachedShareableUris = null;
+
+    public void onItemSelectedStateChanged(ShareActionProvider share,
+            int itemType, int itemSupportedOperations, boolean selected) {
+        int increment = selected ? 1 : -1;
+
+        mSelectedTotalCount += increment;
+        mCachedShareableUris = null;
+
+        if ((itemSupportedOperations & MediaObject.SUPPORT_DELETE) > 0) {
+            mSelectedDeletableCount += increment;
+        }
+        if ((itemSupportedOperations & MediaObject.SUPPORT_EDIT) > 0) {
+            mSelectedEditableCount += increment;
+        }
+        if ((itemSupportedOperations & MediaObject.SUPPORT_CROP) > 0) {
+            mSelectedCroppableCount += increment;
+        }
+        if ((itemSupportedOperations & MediaObject.SUPPORT_SETAS) > 0) {
+            mSelectedSetableCount += increment;
+        }
+        if ((itemSupportedOperations & MediaObject.SUPPORT_TRIM) > 0) {
+            mSelectedTrimmableCount += increment;
+        }
+        if ((itemSupportedOperations & MediaObject.SUPPORT_MUTE) > 0) {
+            mSelectedMuteableCount += increment;
+        }
+        if ((itemSupportedOperations & MediaObject.SUPPORT_SHARE) > 0) {
+            mSelectedShareableCount += increment;
+            if (itemType == FileColumns.MEDIA_TYPE_IMAGE) {
+                mSelectedShareableImageCount += increment;
+            } else if (itemType == FileColumns.MEDIA_TYPE_VIDEO) {
+                mSelectedShareableVideoCount += increment;
+            }
+        }
+
+        mShareIntent.removeExtra(Intent.EXTRA_STREAM);
+        if (mSelectedShareableCount == 0) {
+            mShareIntent.setAction(null).setType(null);
+        } else if (mSelectedShareableCount >= 1) {
+            mCachedShareableUris = mUriSource.getSelectedShareableUris();
+            if (mCachedShareableUris.size() == 0) {
+                mShareIntent.setAction(null).setType(null);
+            } else {
+                if (mSelectedShareableImageCount == mSelectedShareableCount) {
+                    mShareIntent.setType(GalleryUtils.MIME_TYPE_IMAGE);
+                } else if (mSelectedShareableVideoCount == mSelectedShareableCount) {
+                    mShareIntent.setType(GalleryUtils.MIME_TYPE_VIDEO);
+                } else {
+                    mShareIntent.setType(GalleryUtils.MIME_TYPE_ALL);
+                }
+                if (mCachedShareableUris.size() == 1) {
+                    mShareIntent.setAction(Intent.ACTION_SEND);
+                    mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris.get(0));
+                } else {
+                    mShareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
+                    mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris);
+                }
+            }
+        }
+        share.setShareIntent(mShareIntent);
+    }
+
+    public int getSupportedOperations() {
+        if (mSelectedTotalCount == 0) {
+            return 0;
+        }
+        int supported = 0;
+        if (mSelectedTotalCount == 1) {
+            if (mSelectedCroppableCount == 1) {
+                supported |= MediaObject.SUPPORT_CROP;
+            }
+            if (mSelectedEditableCount == 1) {
+                supported |= MediaObject.SUPPORT_EDIT;
+            }
+            if (mSelectedSetableCount == 1) {
+                supported |= MediaObject.SUPPORT_SETAS;
+            }
+            if (mSelectedTrimmableCount == 1) {
+                supported |= MediaObject.SUPPORT_TRIM;
+            }
+            if (mSelectedMuteableCount == 1) {
+                supported |= MediaObject.SUPPORT_MUTE;
+            }
+        }
+        if (mSelectedDeletableCount == mSelectedTotalCount) {
+            supported |= MediaObject.SUPPORT_DELETE;
+        }
+        if (mSelectedShareableCount > 0) {
+            supported |= MediaObject.SUPPORT_SHARE;
+        }
+        return supported;
+    }
+
+    public void onClearSelection() {
+        mSelectedTotalCount = 0;
+        mSelectedShareableCount = 0;
+        mSelectedShareableImageCount = 0;
+        mSelectedShareableVideoCount = 0;
+        mSelectedDeletableCount = 0;
+        mSelectedEditableCount = 0;
+        mSelectedCroppableCount = 0;
+        mSelectedSetableCount = 0;
+        mSelectedTrimmableCount = 0;
+        mSelectedMuteableCount = 0;
+        mCachedShareableUris = null;
+        mShareIntent.removeExtra(Intent.EXTRA_STREAM);
+        mShareIntent.setAction(null).setType(null);
+    }
+}
diff --git a/src/com/android/photos/adapters/AlbumSetCursorAdapter.java b/src/com/android/photos/adapters/AlbumSetCursorAdapter.java
new file mode 100644
index 0000000..ab99cde
--- /dev/null
+++ b/src/com/android/photos/adapters/AlbumSetCursorAdapter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 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.photos.adapters;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.photos.data.AlbumSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+
+import java.util.Date;
+
+public class AlbumSetCursorAdapter extends CursorAdapter {
+
+    private LoaderCompatShim<Cursor> mDrawableFactory;
+
+    public void setDrawableFactory(LoaderCompatShim<Cursor> factory) {
+        mDrawableFactory = factory;
+    }
+
+    public AlbumSetCursorAdapter(Context context) {
+        super(context, null, false);
+    }
+
+    @Override
+    public void bindView(View v, Context context, Cursor cursor) {
+        TextView titleTextView = (TextView) v.findViewById(
+                R.id.album_set_item_title);
+        titleTextView.setText(cursor.getString(AlbumSetLoader.INDEX_TITLE));
+
+        TextView countTextView = (TextView) v.findViewById(
+                R.id.album_set_item_count);
+        int count = cursor.getInt(AlbumSetLoader.INDEX_COUNT);
+        countTextView.setText(context.getResources().getQuantityString(
+                R.plurals.number_of_photos, count, count));
+
+        ImageView thumbImageView = (ImageView) v.findViewById(
+                R.id.album_set_item_image);
+        Drawable recycle = thumbImageView.getDrawable();
+        Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle);
+        if (recycle != drawable) {
+            thumbImageView.setImageDrawable(drawable);
+        }
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        return LayoutInflater.from(context).inflate(
+                R.layout.album_set_item, parent, false);
+    }
+}
diff --git a/src/com/android/photos/adapters/PhotoThumbnailAdapter.java b/src/com/android/photos/adapters/PhotoThumbnailAdapter.java
new file mode 100644
index 0000000..1190b8c
--- /dev/null
+++ b/src/com/android/photos/adapters/PhotoThumbnailAdapter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 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.photos.adapters;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter;
+
+
+public class PhotoThumbnailAdapter extends CursorAdapter implements GalleryThumbnailAdapter {
+    private LayoutInflater mInflater;
+    private LoaderCompatShim<Cursor> mDrawableFactory;
+
+    public PhotoThumbnailAdapter(Context context) {
+        super(context, null, false);
+        mInflater = LayoutInflater.from(context);
+    }
+
+    public void setDrawableFactory(LoaderCompatShim<Cursor> factory) {
+        mDrawableFactory = factory;
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        ImageView iv = (ImageView) view.findViewById(R.id.thumbnail);
+        Drawable recycle = iv.getDrawable();
+        Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle);
+        if (recycle != drawable) {
+            iv.setImageDrawable(drawable);
+        }
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        View view = mInflater.inflate(R.layout.photo_set_item, parent, false);
+        return view;
+    }
+
+    @Override
+    public float getIntrinsicAspectRatio(int position) {
+        Cursor cursor = getItem(position);
+        float width = cursor.getInt(PhotoSetLoader.INDEX_WIDTH);
+        float height = cursor.getInt(PhotoSetLoader.INDEX_HEIGHT);
+        return width / height;
+    }
+
+    @Override
+    public Cursor getItem(int position) {
+        return (Cursor) super.getItem(position);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/photos/data/AlbumSetLoader.java b/src/com/android/photos/data/AlbumSetLoader.java
new file mode 100644
index 0000000..9404732
--- /dev/null
+++ b/src/com/android/photos/data/AlbumSetLoader.java
@@ -0,0 +1,54 @@
+package com.android.photos.data;
+
+import android.database.MatrixCursor;
+
+
+public class AlbumSetLoader {
+    public static final int INDEX_ID = 0;
+    public static final int INDEX_TITLE = 1;
+    public static final int INDEX_TIMESTAMP = 2;
+    public static final int INDEX_THUMBNAIL_URI = 3;
+    public static final int INDEX_THUMBNAIL_WIDTH = 4;
+    public static final int INDEX_THUMBNAIL_HEIGHT = 5;
+    public static final int INDEX_COUNT_PENDING_UPLOAD = 6;
+    public static final int INDEX_COUNT = 7;
+    public static final int INDEX_SUPPORTED_OPERATIONS = 8;
+
+    public static final String[] PROJECTION = {
+        "_id",
+        "title",
+        "timestamp",
+        "thumb_uri",
+        "thumb_width",
+        "thumb_height",
+        "count_pending_upload",
+        "_count",
+        "supported_operations"
+    };
+    public static final MatrixCursor MOCK = createRandomCursor(30);
+
+    private static MatrixCursor createRandomCursor(int count) {
+        MatrixCursor c = new MatrixCursor(PROJECTION, count);
+        for (int i = 0; i < count; i++) {
+            c.addRow(createRandomRow());
+        }
+        return c;
+    }
+
+    private static Object[] createRandomRow() {
+        double random = Math.random();
+        int id = (int) (500 * random);
+        Object[] row = {
+            id,
+            "Fun times " + id,
+            (long) (System.currentTimeMillis() * random),
+            null,
+            0,
+            0,
+            (random < .3 ? 1 : 0),
+            1,
+            0
+        };
+        return row;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/photos/data/BitmapDecoder.java b/src/com/android/photos/data/BitmapDecoder.java
new file mode 100644
index 0000000..0671e73
--- /dev/null
+++ b/src/com/android/photos/data/BitmapDecoder.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.util.Log;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * BitmapDecoder keeps a pool of temporary storage to reuse for decoding
+ * bitmaps. It also simplifies the multi-stage decoding required to efficiently
+ * use GalleryBitmapPool. The static methods decode and decodeFile can be used
+ * to decode a bitmap from GalleryBitmapPool. The bitmap may be returned
+ * directly to GalleryBitmapPool or use the put method here when the bitmap is
+ * ready to be recycled.
+ */
+public class BitmapDecoder {
+    private static final String TAG = BitmapDecoder.class.getSimpleName();
+    private static final int POOL_SIZE = 4;
+    private static final int TEMP_STORAGE_SIZE_BYTES = 16 * 1024;
+    private static final int HEADER_MAX_SIZE = 128 * 1024;
+    private static final int NO_SCALING = -1;
+
+    private static final Pool<BitmapFactory.Options> sOptions =
+            new SynchronizedPool<BitmapFactory.Options>(POOL_SIZE);
+
+    private interface Decoder<T> {
+        Bitmap decode(T input, BitmapFactory.Options options);
+
+        boolean decodeBounds(T input, BitmapFactory.Options options);
+    }
+
+    private static abstract class OnlyDecode<T> implements Decoder<T> {
+        @Override
+        public boolean decodeBounds(T input, BitmapFactory.Options options) {
+            decode(input, options);
+            return true;
+        }
+    }
+
+    private static final Decoder<InputStream> sStreamDecoder = new Decoder<InputStream>() {
+        @Override
+        public Bitmap decode(InputStream is, Options options) {
+            return BitmapFactory.decodeStream(is, null, options);
+        }
+
+        @Override
+        public boolean decodeBounds(InputStream is, Options options) {
+            is.mark(HEADER_MAX_SIZE);
+            BitmapFactory.decodeStream(is, null, options);
+            try {
+                is.reset();
+                return true;
+            } catch (IOException e) {
+                Log.e(TAG, "Could not decode stream to bitmap", e);
+                return false;
+            }
+        }
+    };
+
+    private static final Decoder<String> sFileDecoder = new OnlyDecode<String>() {
+        @Override
+        public Bitmap decode(String filePath, Options options) {
+            return BitmapFactory.decodeFile(filePath, options);
+        }
+    };
+
+    private static final Decoder<byte[]> sByteArrayDecoder = new OnlyDecode<byte[]>() {
+        @Override
+        public Bitmap decode(byte[] data, Options options) {
+            return BitmapFactory.decodeByteArray(data, 0, data.length, options);
+        }
+    };
+
+    private static <T> Bitmap delegateDecode(Decoder<T> decoder, T input, int width, int height) {
+        BitmapFactory.Options options = getOptions();
+        GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
+        try {
+            options.inJustDecodeBounds = true;
+            if (!decoder.decodeBounds(input, options)) {
+                return null;
+            }
+            options.inJustDecodeBounds = false;
+            Bitmap reuseBitmap = null;
+            if (width != NO_SCALING && options.outWidth >= width && options.outHeight >= height) {
+                setScaling(options, width, height);
+            } else {
+                reuseBitmap = pool.get(options.outWidth, options.outHeight);
+            }
+            options.inBitmap = reuseBitmap;
+            Bitmap decodedBitmap = decoder.decode(input, options);
+            if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
+                pool.put(reuseBitmap);
+            }
+            return decodedBitmap;
+        } catch (IllegalArgumentException e) {
+            if (options.inBitmap == null) {
+                throw e;
+            }
+            pool.put(options.inBitmap);
+            options.inBitmap = null;
+            return decoder.decode(input, options);
+        } finally {
+            options.inBitmap = null;
+            options.inJustDecodeBounds = false;
+            sOptions.release(options);
+        }
+    }
+
+    public static Bitmap decode(InputStream in) {
+        try {
+            if (!in.markSupported()) {
+                in = new BufferedInputStream(in);
+            }
+            return delegateDecode(sStreamDecoder, in, NO_SCALING, NO_SCALING);
+        } finally {
+            Utils.closeSilently(in);
+        }
+    }
+
+    public static Bitmap decode(File file) {
+        return decodeFile(file.getPath());
+    }
+
+    public static Bitmap decodeFile(String path) {
+        return delegateDecode(sFileDecoder, path, NO_SCALING, NO_SCALING);
+    }
+
+    public static Bitmap decodeByteArray(byte[] data) {
+        return delegateDecode(sByteArrayDecoder, data, NO_SCALING, NO_SCALING);
+    }
+
+    public static void put(Bitmap bitmap) {
+        GalleryBitmapPool.getInstance().put(bitmap);
+    }
+
+    /**
+     * Decodes to a specific size. If the dimensions of the image don't match
+     * width x height, the resulting image will be in the proportions of the
+     * decoded image, but will be scaled to fill the dimensions. For example, if
+     * width and height are 10x10 and the image is 200x100, the resulting image
+     * will be scaled/sampled to 20x10.
+     */
+    public static Bitmap decodeFile(String path, int width, int height) {
+        return delegateDecode(sFileDecoder, path, width, height);
+    }
+
+    /** @see #decodeFile(String, int, int) */
+    public static Bitmap decodeByteArray(byte[] data, int width, int height) {
+        return delegateDecode(sByteArrayDecoder, data, width, height);
+    }
+
+    /** @see #decodeFile(String, int, int) */
+    public static Bitmap decode(InputStream in, int width, int height) {
+        try {
+            if (!in.markSupported()) {
+                in = new BufferedInputStream(in);
+            }
+            return delegateDecode(sStreamDecoder, in, width, height);
+        } finally {
+            Utils.closeSilently(in);
+        }
+    }
+
+    private static BitmapFactory.Options getOptions() {
+        BitmapFactory.Options opts = sOptions.acquire();
+        if (opts == null) {
+            opts = new BitmapFactory.Options();
+            opts.inMutable = true;
+            opts.inPreferredConfig = Config.ARGB_8888;
+            opts.inTempStorage = new byte[TEMP_STORAGE_SIZE_BYTES];
+        }
+        opts.inSampleSize = 1;
+        opts.inDensity = 1;
+        opts.inTargetDensity = 1;
+
+        return opts;
+    }
+
+    // Sets the options to sample then scale the image so that the image's
+    // minimum dimension will match side.
+    private static void setScaling(BitmapFactory.Options options, int width, int height) {
+        float widthScale = ((float)options.outWidth)/ width;
+        float heightScale = ((float) options.outHeight)/height;
+        int side = (widthScale < heightScale) ? width : height;
+        options.inSampleSize = BitmapUtils.computeSampleSize(options.outWidth, options.outHeight,
+                side, BitmapUtils.UNCONSTRAINED);
+        int constraint;
+        if (options.outWidth < options.outHeight) {
+            // Width is the constraint. Scale so that width = side.
+            constraint = options.outWidth;
+        } else {
+            // Height is the constraint. Scale so that height = side.
+            constraint = options.outHeight;
+        }
+        options.inDensity = constraint / options.inSampleSize;
+        options.inTargetDensity = side;
+    }
+}
diff --git a/src/com/android/photos/data/FileRetriever.java b/src/com/android/photos/data/FileRetriever.java
new file mode 100644
index 0000000..eb7686e
--- /dev/null
+++ b/src/com/android/photos/data/FileRetriever.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.common.BitmapUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileRetriever implements MediaRetriever {
+    private static final String TAG = FileRetriever.class.getSimpleName();
+
+    @Override
+    public File getLocalFile(Uri contentUri) {
+        return new File(contentUri.getPath());
+    }
+
+    @Override
+    public MediaSize getFastImageSize(Uri contentUri, MediaSize size) {
+        if (isVideo(contentUri)) {
+            return null;
+        }
+        return MediaSize.TemporaryThumbnail;
+    }
+
+    @Override
+    public byte[] getTemporaryImage(Uri contentUri, MediaSize fastImageSize) {
+
+        try {
+            ExifInterface exif = new ExifInterface(contentUri.getPath());
+            if (exif.hasThumbnail()) {
+                return exif.getThumbnail();
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to load exif for " + contentUri);
+        }
+        return null;
+    }
+
+    @Override
+    public boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile) {
+        if (imageSize == MediaSize.Original) {
+            return false; // getLocalFile should always return the original.
+        }
+        if (imageSize == MediaSize.Thumbnail) {
+            File preview = MediaCache.getInstance().getCachedFile(contentUri, MediaSize.Preview);
+            if (preview != null) {
+                // Just downsample the preview, it is faster.
+                return MediaCacheUtils.downsample(preview, imageSize, tempFile);
+            }
+        }
+        File highRes = new File(contentUri.getPath());
+        boolean success;
+        if (!isVideo(contentUri)) {
+            success = MediaCacheUtils.downsample(highRes, imageSize, tempFile);
+        } else {
+            // Video needs to extract the bitmap.
+            Bitmap bitmap = BitmapUtils.createVideoThumbnail(highRes.getPath());
+            if (bitmap == null) {
+                return false;
+            } else if (imageSize == MediaSize.Thumbnail
+                    && !MediaCacheUtils.needsDownsample(bitmap, MediaSize.Preview)
+                    && MediaCacheUtils.writeToFile(bitmap, tempFile)) {
+                // Opportunistically save preview
+                MediaCache mediaCache = MediaCache.getInstance();
+                mediaCache.insertIntoCache(contentUri, MediaSize.Preview, tempFile);
+            }
+            // Now scale the image
+            success = MediaCacheUtils.downsample(bitmap, imageSize, tempFile);
+        }
+        return success;
+    }
+
+    @Override
+    public Uri normalizeUri(Uri contentUri, MediaSize size) {
+        return contentUri;
+    }
+
+    @Override
+    public MediaSize normalizeMediaSize(Uri contentUri, MediaSize size) {
+        return size;
+    }
+
+    private static boolean isVideo(Uri uri) {
+        MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+        String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+        String mimeType = mimeTypeMap.getMimeTypeFromExtension(extension);
+        return (mimeType != null && mimeType.startsWith("video/"));
+    }
+}
diff --git a/src/com/android/photos/data/GalleryBitmapPool.java b/src/com/android/photos/data/GalleryBitmapPool.java
new file mode 100644
index 0000000..390a0d4
--- /dev/null
+++ b/src/com/android/photos/data/GalleryBitmapPool.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
+
+import com.android.photos.data.SparseArrayBitmapPool.Node;
+
+/**
+ * Pool allowing the efficient reuse of bitmaps in order to avoid long
+ * garbage collection pauses.
+ */
+public class GalleryBitmapPool {
+
+    private static final int CAPACITY_BYTES = 20971520;
+
+    // We found that Gallery uses bitmaps that are either square (for example,
+    // tiles of large images or square thumbnails), match one of the common
+    // photo aspect ratios (4x3, 3x2, or 16x9), or, less commonly, are of some
+    // other aspect ratio. Taking advantage of this information, we use 3
+    // SparseArrayBitmapPool instances to back the GalleryBitmapPool, which affords
+    // O(1) lookups for square bitmaps, and average-case - but *not* asymptotically -
+    // O(1) lookups for common photo aspect ratios and other miscellaneous aspect
+    // ratios. Beware of the pathological case where there are many bitmaps added
+    // to the pool with different non-square aspect ratios but the same width, as
+    // performance will degrade and the average case lookup will approach
+    // O(# of different aspect ratios).
+    private static final int POOL_INDEX_NONE = -1;
+    private static final int POOL_INDEX_SQUARE = 0;
+    private static final int POOL_INDEX_PHOTO = 1;
+    private static final int POOL_INDEX_MISC = 2;
+
+    private static final Point[] COMMON_PHOTO_ASPECT_RATIOS =
+        { new Point(4, 3), new Point(3, 2), new Point(16, 9) };
+
+    private int mCapacityBytes;
+    private SparseArrayBitmapPool [] mPools;
+    private Pool<Node> mSharedNodePool = new SynchronizedPool<Node>(128);
+
+    private GalleryBitmapPool(int capacityBytes) {
+        mPools = new SparseArrayBitmapPool[3];
+        mPools[POOL_INDEX_SQUARE] = new SparseArrayBitmapPool(capacityBytes / 3, mSharedNodePool);
+        mPools[POOL_INDEX_PHOTO] = new SparseArrayBitmapPool(capacityBytes / 3, mSharedNodePool);
+        mPools[POOL_INDEX_MISC] = new SparseArrayBitmapPool(capacityBytes / 3, mSharedNodePool);
+        mCapacityBytes = capacityBytes;
+    }
+
+    private static GalleryBitmapPool sInstance = new GalleryBitmapPool(CAPACITY_BYTES);
+
+    public static GalleryBitmapPool getInstance() {
+        return sInstance;
+    }
+
+    private SparseArrayBitmapPool getPoolForDimensions(int width, int height) {
+        int index = getPoolIndexForDimensions(width, height);
+        if (index == POOL_INDEX_NONE) {
+            return null;
+        } else {
+            return mPools[index];
+        }
+    }
+
+    private int getPoolIndexForDimensions(int width, int height) {
+        if (width <= 0 || height <= 0) {
+            return POOL_INDEX_NONE;
+        }
+        if (width == height) {
+            return POOL_INDEX_SQUARE;
+        }
+        int min, max;
+        if (width > height) {
+            min = height;
+            max = width;
+        } else {
+            min = width;
+            max = height;
+        }
+        for (Point ar : COMMON_PHOTO_ASPECT_RATIOS) {
+            if (min * ar.x == max * ar.y) {
+                return POOL_INDEX_PHOTO;
+            }
+        }
+        return POOL_INDEX_MISC;
+    }
+
+    /**
+     * @return Capacity of the pool in bytes.
+     */
+    public synchronized int getCapacity() {
+        return mCapacityBytes;
+    }
+
+    /**
+     * @return Approximate total size in bytes of the bitmaps stored in the pool.
+     */
+    public int getSize() {
+        // Note that this only returns an approximate size, since multiple threads
+        // might be getting and putting Bitmaps from the pool and we lock at the
+        // sub-pool level to avoid unnecessary blocking.
+        int total = 0;
+        for (SparseArrayBitmapPool p : mPools) {
+            total += p.getSize();
+        }
+        return total;
+    }
+
+    /**
+     * @return Bitmap from the pool with the desired height/width or null if none available.
+     */
+    public Bitmap get(int width, int height) {
+        SparseArrayBitmapPool pool = getPoolForDimensions(width, height);
+        if (pool == null) {
+            return null;
+        } else {
+            return pool.get(width, height);
+        }
+    }
+
+    /**
+     * Adds the given bitmap to the pool.
+     * @return Whether the bitmap was added to the pool.
+     */
+    public boolean put(Bitmap b) {
+        if (b == null || b.getConfig() != Bitmap.Config.ARGB_8888) {
+            return false;
+        }
+        SparseArrayBitmapPool pool = getPoolForDimensions(b.getWidth(), b.getHeight());
+        if (pool == null) {
+            b.recycle();
+            return false;
+        } else {
+            return pool.put(b);
+        }
+    }
+
+    /**
+     * Empty the pool, recycling all the bitmaps currently in it.
+     */
+    public void clear() {
+        for (SparseArrayBitmapPool p : mPools) {
+            p.clear();
+        }
+    }
+}
diff --git a/src/com/android/photos/data/MediaCache.java b/src/com/android/photos/data/MediaCache.java
new file mode 100644
index 0000000..0952a40
--- /dev/null
+++ b/src/com/android/photos/data/MediaCache.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.photos.data.MediaCacheDatabase.Action;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * MediaCache keeps a cache of images, videos, thumbnails and previews. Calls to
+ * retrieve a specific media item are executed asynchronously. The caller has an
+ * option to receive a notification for lower resolution images that happen to
+ * be available prior to the one requested.
+ * <p>
+ * When an media item has been retrieved, the notification for it is called on a
+ * separate notifier thread. This thread should not be held for a long time so
+ * that other notifications may happen.
+ * </p>
+ * <p>
+ * Media items are uniquely identified by their content URIs. Each
+ * scheme/authority can offer its own MediaRetriever, running in its own thread.
+ * </p>
+ * <p>
+ * The MediaCache is an LRU cache, but does not allow the thumbnail cache to
+ * drop below a minimum size. This prevents browsing through original images to
+ * wipe out the thumbnails.
+ * </p>
+ */
+public class MediaCache {
+    static final String TAG = MediaCache.class.getSimpleName();
+    /** Subdirectory containing the image cache. */
+    static final String IMAGE_CACHE_SUBDIR = "image_cache";
+    /** File name extension to use for cached images. */
+    static final String IMAGE_EXTENSION = ".cache";
+    /** File name extension to use for temporary cached images while retrieving. */
+    static final String TEMP_IMAGE_EXTENSION = ".temp";
+
+    public static interface ImageReady {
+        void imageReady(InputStream bitmapInputStream);
+    }
+
+    public static interface OriginalReady {
+        void originalReady(File originalFile);
+    }
+
+    /** A Thread for each MediaRetriever */
+    private class ProcessQueue extends Thread {
+        private Queue<ProcessingJob> mQueue;
+
+        public ProcessQueue(Queue<ProcessingJob> queue) {
+            mQueue = queue;
+        }
+
+        @Override
+        public void run() {
+            while (mRunning) {
+                ProcessingJob status;
+                synchronized (mQueue) {
+                    while (mQueue.isEmpty()) {
+                        try {
+                            mQueue.wait();
+                        } catch (InterruptedException e) {
+                            if (!mRunning) {
+                                return;
+                            }
+                            Log.w(TAG, "Unexpected interruption", e);
+                        }
+                    }
+                    status = mQueue.remove();
+                }
+                processTask(status);
+            }
+        }
+    };
+
+    private interface NotifyReady {
+        void notifyReady();
+
+        void setFile(File file) throws FileNotFoundException;
+
+        boolean isPrefetch();
+    }
+
+    private static class NotifyOriginalReady implements NotifyReady {
+        private final OriginalReady mCallback;
+        private File mFile;
+
+        public NotifyOriginalReady(OriginalReady callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void notifyReady() {
+            if (mCallback != null) {
+                mCallback.originalReady(mFile);
+            }
+        }
+
+        @Override
+        public void setFile(File file) {
+            mFile = file;
+        }
+
+        @Override
+        public boolean isPrefetch() {
+            return mCallback == null;
+        }
+    }
+
+    private static class NotifyImageReady implements NotifyReady {
+        private final ImageReady mCallback;
+        private InputStream mInputStream;
+
+        public NotifyImageReady(ImageReady callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void notifyReady() {
+            if (mCallback != null) {
+                mCallback.imageReady(mInputStream);
+            }
+        }
+
+        @Override
+        public void setFile(File file) throws FileNotFoundException {
+            mInputStream = new FileInputStream(file);
+        }
+
+        public void setBytes(byte[] bytes) {
+            mInputStream = new ByteArrayInputStream(bytes);
+        }
+
+        @Override
+        public boolean isPrefetch() {
+            return mCallback == null;
+        }
+    }
+
+    /** A media item to be retrieved and its notifications. */
+    private static class ProcessingJob {
+        public ProcessingJob(Uri uri, MediaSize size, NotifyReady complete,
+                NotifyImageReady lowResolution) {
+            this.contentUri = uri;
+            this.size = size;
+            this.complete = complete;
+            this.lowResolution = lowResolution;
+        }
+        public Uri contentUri;
+        public MediaSize size;
+        public NotifyImageReady lowResolution;
+        public NotifyReady complete;
+    }
+
+    private boolean mRunning = true;
+    private static MediaCache sInstance;
+    private File mCacheDir;
+    private Context mContext;
+    private Queue<NotifyReady> mCallbacks = new LinkedList<NotifyReady>();
+    private Map<String, MediaRetriever> mRetrievers = new HashMap<String, MediaRetriever>();
+    private Map<String, List<ProcessingJob>> mTasks = new HashMap<String, List<ProcessingJob>>();
+    private List<ProcessQueue> mProcessingThreads = new ArrayList<ProcessQueue>();
+    private MediaCacheDatabase mDatabaseHelper;
+    private long mTempImageNumber = 1;
+    private Object mTempImageNumberLock = new Object();
+
+    private long mMaxCacheSize = 40 * 1024 * 1024; // 40 MB
+    private long mMinThumbCacheSize = 4 * 1024 * 1024; // 4 MB
+    private long mCacheSize = -1;
+    private long mThumbCacheSize = -1;
+    private Object mCacheSizeLock = new Object();
+
+    private Action mNotifyCachedLowResolution = new Action() {
+        @Override
+        public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+            ProcessingJob job = (ProcessingJob) parameter;
+            File file = createCacheImagePath(id);
+            addNotification(job.lowResolution, file);
+        }
+    };
+
+    private Action mMoveTempToCache = new Action() {
+        @Override
+        public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+            File tempFile = (File) parameter;
+            File cacheFile = createCacheImagePath(id);
+            tempFile.renameTo(cacheFile);
+        }
+    };
+
+    private Action mDeleteFile = new Action() {
+        @Override
+        public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+            File file = createCacheImagePath(id);
+            file.delete();
+            synchronized (mCacheSizeLock) {
+                if (mCacheSize != -1) {
+                    long length = (Long) parameter;
+                    mCacheSize -= length;
+                    if (size == MediaSize.Thumbnail) {
+                        mThumbCacheSize -= length;
+                    }
+                }
+            }
+        }
+    };
+
+    /** The thread used to make ImageReady and OriginalReady callbacks. */
+    private Thread mProcessNotifications = new Thread() {
+        @Override
+        public void run() {
+            while (mRunning) {
+                NotifyReady notifyImage;
+                synchronized (mCallbacks) {
+                    while (mCallbacks.isEmpty()) {
+                        try {
+                            mCallbacks.wait();
+                        } catch (InterruptedException e) {
+                            if (!mRunning) {
+                                return;
+                            }
+                            Log.w(TAG, "Unexpected Interruption, continuing");
+                        }
+                    }
+                    notifyImage = mCallbacks.remove();
+                }
+
+                notifyImage.notifyReady();
+            }
+        }
+    };
+
+    public static synchronized void initialize(Context context) {
+        if (sInstance == null) {
+            sInstance = new MediaCache(context);
+            MediaCacheUtils.initialize(context);
+        }
+    }
+
+    public static MediaCache getInstance() {
+        return sInstance;
+    }
+
+    public static synchronized void shutdown() {
+        sInstance.mRunning = false;
+        sInstance.mProcessNotifications.interrupt();
+        for (ProcessQueue processingThread : sInstance.mProcessingThreads) {
+            processingThread.interrupt();
+        }
+        sInstance = null;
+    }
+
+    private MediaCache(Context context) {
+        mDatabaseHelper = new MediaCacheDatabase(context);
+        mProcessNotifications.start();
+        mContext = context;
+    }
+
+    // This is used for testing.
+    public void setCacheDir(File cacheDir) {
+        cacheDir.mkdirs();
+        mCacheDir = cacheDir;
+    }
+
+    public File getCacheDir() {
+        synchronized (mContext) {
+            if (mCacheDir == null) {
+                String state = Environment.getExternalStorageState();
+                File baseDir;
+                if (Environment.MEDIA_MOUNTED.equals(state)) {
+                    baseDir = mContext.getExternalCacheDir();
+                } else {
+                    // Stored in internal cache
+                    baseDir = mContext.getCacheDir();
+                }
+                mCacheDir = new File(baseDir, IMAGE_CACHE_SUBDIR);
+                mCacheDir.mkdirs();
+            }
+            return mCacheDir;
+        }
+    }
+
+    /**
+     * Invalidates all cached images related to a given contentUri. This call
+     * doesn't complete until the images have been removed from the cache.
+     */
+    public void invalidate(Uri contentUri) {
+        mDatabaseHelper.delete(contentUri, mDeleteFile);
+    }
+
+    public void clearCacheDir() {
+        File[] cachedFiles = getCacheDir().listFiles();
+        if (cachedFiles != null) {
+            for (File cachedFile : cachedFiles) {
+                cachedFile.delete();
+            }
+        }
+    }
+
+    /**
+     * Add a MediaRetriever for a Uri scheme and authority. This MediaRetriever
+     * will be granted its own thread for retrieving images.
+     */
+    public void addRetriever(String scheme, String authority, MediaRetriever retriever) {
+        String differentiator = getDifferentiator(scheme, authority);
+        synchronized (mRetrievers) {
+            mRetrievers.put(differentiator, retriever);
+        }
+        synchronized (mTasks) {
+            LinkedList<ProcessingJob> queue = new LinkedList<ProcessingJob>();
+            mTasks.put(differentiator, queue);
+            new ProcessQueue(queue).start();
+        }
+    }
+
+    /**
+     * Retrieves a thumbnail. complete will be called when the thumbnail is
+     * available. If lowResolution is not null and a lower resolution thumbnail
+     * is available before the thumbnail, lowResolution will be called prior to
+     * complete. All callbacks will be made on a thread other than the calling
+     * thread.
+     *
+     * @param contentUri The URI for the full resolution image to search for.
+     * @param complete Callback for when the image has been retrieved.
+     * @param lowResolution If not null and a lower resolution image is
+     *            available prior to retrieving the thumbnail, this will be
+     *            called with the low resolution bitmap.
+     */
+    public void retrieveThumbnail(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+        addTask(contentUri, complete, lowResolution, MediaSize.Thumbnail);
+    }
+
+    /**
+     * Retrieves a preview. complete will be called when the preview is
+     * available. If lowResolution is not null and a lower resolution preview is
+     * available before the preview, lowResolution will be called prior to
+     * complete. All callbacks will be made on a thread other than the calling
+     * thread.
+     *
+     * @param contentUri The URI for the full resolution image to search for.
+     * @param complete Callback for when the image has been retrieved.
+     * @param lowResolution If not null and a lower resolution image is
+     *            available prior to retrieving the preview, this will be called
+     *            with the low resolution bitmap.
+     */
+    public void retrievePreview(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+        addTask(contentUri, complete, lowResolution, MediaSize.Preview);
+    }
+
+    /**
+     * Retrieves the original image or video. complete will be called when the
+     * media is available on the local file system. If lowResolution is not null
+     * and a lower resolution preview is available before the original,
+     * lowResolution will be called prior to complete. All callbacks will be
+     * made on a thread other than the calling thread.
+     *
+     * @param contentUri The URI for the full resolution image to search for.
+     * @param complete Callback for when the image has been retrieved.
+     * @param lowResolution If not null and a lower resolution image is
+     *            available prior to retrieving the preview, this will be called
+     *            with the low resolution bitmap.
+     */
+    public void retrieveOriginal(Uri contentUri, OriginalReady complete, ImageReady lowResolution) {
+        File localFile = getLocalFile(contentUri);
+        if (localFile != null) {
+            addNotification(new NotifyOriginalReady(complete), localFile);
+        } else {
+            NotifyImageReady notifyLowResolution = (lowResolution == null) ? null
+                    : new NotifyImageReady(lowResolution);
+            addTask(contentUri, new NotifyOriginalReady(complete), notifyLowResolution,
+                    MediaSize.Original);
+        }
+    }
+
+    /**
+     * Looks for an already cached media at a specific size.
+     *
+     * @param contentUri The original media item content URI
+     * @param size The target size to search for in the cache
+     * @return The cached file location or null if it is not cached.
+     */
+    public File getCachedFile(Uri contentUri, MediaSize size) {
+        Long cachedId = mDatabaseHelper.getCached(contentUri, size);
+        File file = null;
+        if (cachedId != null) {
+            file = createCacheImagePath(cachedId);
+            if (!file.exists()) {
+                mDatabaseHelper.delete(contentUri, size, mDeleteFile);
+                file = null;
+            }
+        }
+        return file;
+    }
+
+    /**
+     * Inserts a media item into the cache.
+     *
+     * @param contentUri The original media item URI.
+     * @param size The size of the media item to store in the cache.
+     * @param tempFile The temporary file where the image is stored. This file
+     *            will no longer exist after executing this method.
+     * @return The new location, in the cache, of the media item or null if it
+     *         wasn't possible to move into the cache.
+     */
+    public File insertIntoCache(Uri contentUri, MediaSize size, File tempFile) {
+        long fileSize = tempFile.length();
+        if (fileSize == 0) {
+            return null;
+        }
+        File cacheFile = null;
+        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+        // Ensure that this step is atomic
+        db.beginTransaction();
+        try {
+            Long id = mDatabaseHelper.getCached(contentUri, size);
+            if (id != null) {
+                cacheFile = createCacheImagePath(id);
+                if (tempFile.renameTo(cacheFile)) {
+                    mDatabaseHelper.updateLength(id, fileSize);
+                } else {
+                    Log.w(TAG, "Could not update cached file with " + tempFile);
+                    tempFile.delete();
+                    cacheFile = null;
+                }
+            } else {
+                ensureFreeCacheSpace(tempFile.length(), size);
+                id = mDatabaseHelper.insert(contentUri, size, mMoveTempToCache, tempFile);
+                cacheFile = createCacheImagePath(id);
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+        return cacheFile;
+    }
+
+    /**
+     * For testing purposes.
+     */
+    public void setMaxCacheSize(long maxCacheSize) {
+        synchronized (mCacheSizeLock) {
+            mMaxCacheSize = maxCacheSize;
+            mMinThumbCacheSize = mMaxCacheSize / 10;
+            mCacheSize = -1;
+            mThumbCacheSize = -1;
+        }
+    }
+
+    private File createCacheImagePath(long id) {
+        return new File(getCacheDir(), String.valueOf(id) + IMAGE_EXTENSION);
+    }
+
+    private void addTask(Uri contentUri, ImageReady complete, ImageReady lowResolution,
+            MediaSize size) {
+        NotifyReady notifyComplete = new NotifyImageReady(complete);
+        NotifyImageReady notifyLowResolution = null;
+        if (lowResolution != null) {
+            notifyLowResolution = new NotifyImageReady(lowResolution);
+        }
+        addTask(contentUri, notifyComplete, notifyLowResolution, size);
+    }
+
+    private void addTask(Uri contentUri, NotifyReady complete, NotifyImageReady lowResolution,
+            MediaSize size) {
+        MediaRetriever retriever = getMediaRetriever(contentUri);
+        Uri uri = retriever.normalizeUri(contentUri, size);
+        if (uri == null) {
+            throw new IllegalArgumentException("No MediaRetriever for " + contentUri);
+        }
+        size = retriever.normalizeMediaSize(uri, size);
+
+        File cachedFile = getCachedFile(uri, size);
+        if (cachedFile != null) {
+            addNotification(complete, cachedFile);
+            return;
+        }
+        String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+        synchronized (mTasks) {
+            List<ProcessingJob> tasks = mTasks.get(differentiator);
+            if (tasks == null) {
+                throw new IllegalArgumentException("Cannot find retriever for: " + uri);
+            }
+            synchronized (tasks) {
+                ProcessingJob job = new ProcessingJob(uri, size, complete, lowResolution);
+                if (complete.isPrefetch()) {
+                    tasks.add(job);
+                } else {
+                    int index = tasks.size() - 1;
+                    while (index >= 0 && tasks.get(index).complete.isPrefetch()) {
+                        index--;
+                    }
+                    tasks.add(index + 1, job);
+                }
+                tasks.notifyAll();
+            }
+        }
+    }
+
+    private MediaRetriever getMediaRetriever(Uri uri) {
+        String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+        MediaRetriever retriever;
+        synchronized (mRetrievers) {
+            retriever = mRetrievers.get(differentiator);
+        }
+        if (retriever == null) {
+            throw new IllegalArgumentException("No MediaRetriever for " + uri);
+        }
+        return retriever;
+    }
+
+    private File getLocalFile(Uri uri) {
+        MediaRetriever retriever = getMediaRetriever(uri);
+        File localFile = null;
+        if (retriever != null) {
+            localFile = retriever.getLocalFile(uri);
+        }
+        return localFile;
+    }
+
+    private MediaSize getFastImageSize(Uri uri, MediaSize size) {
+        MediaRetriever retriever = getMediaRetriever(uri);
+        return retriever.getFastImageSize(uri, size);
+    }
+
+    private boolean isFastImageBetter(MediaSize fastImageType, MediaSize size) {
+        if (fastImageType == null) {
+            return false;
+        }
+        if (size == null) {
+            return true;
+        }
+        return fastImageType.isBetterThan(size);
+    }
+
+    private byte[] getTemporaryImage(Uri uri, MediaSize fastImageType) {
+        MediaRetriever retriever = getMediaRetriever(uri);
+        return retriever.getTemporaryImage(uri, fastImageType);
+    }
+
+    private void processTask(ProcessingJob job) {
+        File cachedFile = getCachedFile(job.contentUri, job.size);
+        if (cachedFile != null) {
+            addNotification(job.complete, cachedFile);
+            return;
+        }
+
+        boolean hasLowResolution = job.lowResolution != null;
+        if (hasLowResolution) {
+            MediaSize cachedSize = mDatabaseHelper.executeOnBestCached(job.contentUri, job.size,
+                    mNotifyCachedLowResolution);
+            MediaSize fastImageSize = getFastImageSize(job.contentUri, job.size);
+            if (isFastImageBetter(fastImageSize, cachedSize)) {
+                if (fastImageSize.isTemporary()) {
+                    byte[] bytes = getTemporaryImage(job.contentUri, fastImageSize);
+                    if (bytes != null) {
+                        addNotification(job.lowResolution, bytes);
+                    }
+                } else {
+                    File lowFile = getMedia(job.contentUri, fastImageSize);
+                    if (lowFile != null) {
+                        addNotification(job.lowResolution, lowFile);
+                    }
+                }
+            }
+        }
+
+        // Now get the full size desired
+        File fullSizeFile = getMedia(job.contentUri, job.size);
+        if (fullSizeFile != null) {
+            addNotification(job.complete, fullSizeFile);
+        }
+    }
+
+    private void addNotification(NotifyReady callback, File file) {
+        try {
+            callback.setFile(file);
+            synchronized (mCallbacks) {
+                mCallbacks.add(callback);
+                mCallbacks.notifyAll();
+            }
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, "Unable to read file " + file, e);
+        }
+    }
+
+    private void addNotification(NotifyImageReady callback, byte[] bytes) {
+        callback.setBytes(bytes);
+        synchronized (mCallbacks) {
+            mCallbacks.add(callback);
+            mCallbacks.notifyAll();
+        }
+    }
+
+    private File getMedia(Uri uri, MediaSize size) {
+        long imageNumber;
+        synchronized (mTempImageNumberLock) {
+            imageNumber = mTempImageNumber++;
+        }
+        File tempFile = new File(getCacheDir(), String.valueOf(imageNumber) + TEMP_IMAGE_EXTENSION);
+        MediaRetriever retriever = getMediaRetriever(uri);
+        boolean retrieved = retriever.getMedia(uri, size, tempFile);
+        File cachedFile = null;
+        if (retrieved) {
+            ensureFreeCacheSpace(tempFile.length(), size);
+            long id = mDatabaseHelper.insert(uri, size, mMoveTempToCache, tempFile);
+            cachedFile = createCacheImagePath(id);
+        }
+        return cachedFile;
+    }
+
+    private static String getDifferentiator(String scheme, String authority) {
+        if (authority == null) {
+            return scheme;
+        }
+        StringBuilder differentiator = new StringBuilder(scheme);
+        differentiator.append(':');
+        differentiator.append(authority);
+        return differentiator.toString();
+    }
+
+    private void ensureFreeCacheSpace(long size, MediaSize mediaSize) {
+        synchronized (mCacheSizeLock) {
+            if (mCacheSize == -1 || mThumbCacheSize == -1) {
+                mCacheSize = mDatabaseHelper.getCacheSize();
+                mThumbCacheSize = mDatabaseHelper.getThumbnailCacheSize();
+                if (mCacheSize == -1 || mThumbCacheSize == -1) {
+                    Log.e(TAG, "Can't determine size of the image cache");
+                    return;
+                }
+            }
+            mCacheSize += size;
+            if (mediaSize == MediaSize.Thumbnail) {
+                mThumbCacheSize += size;
+            }
+            if (mCacheSize > mMaxCacheSize) {
+                shrinkCacheLocked();
+            }
+        }
+    }
+
+    private void shrinkCacheLocked() {
+        long deleteSize = mMinThumbCacheSize;
+        boolean includeThumbnails = (mThumbCacheSize - deleteSize) > mMinThumbCacheSize;
+        mDatabaseHelper.deleteOldCached(includeThumbnails, deleteSize, mDeleteFile);
+    }
+}
diff --git a/src/com/android/photos/data/MediaCacheDatabase.java b/src/com/android/photos/data/MediaCacheDatabase.java
new file mode 100644
index 0000000..c92ac0f
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheDatabase.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+
+class MediaCacheDatabase extends SQLiteOpenHelper {
+    public static final int DB_VERSION = 1;
+    public static final String DB_NAME = "mediacache.db";
+
+    /** Internal database table used for the media cache */
+    public static final String TABLE = "media_cache";
+
+    private static interface Columns extends BaseColumns {
+        /** The Content URI of the original image. */
+        public static final String URI = "uri";
+        /** MediaSize.getValue() values. */
+        public static final String MEDIA_SIZE = "media_size";
+        /** The last time this image was queried. */
+        public static final String LAST_ACCESS = "last_access";
+        /** The image size in bytes. */
+        public static final String SIZE_IN_BYTES = "size";
+    }
+
+    static interface Action {
+        void execute(Uri uri, long id, MediaSize size, Object parameter);
+    }
+
+    private static final String[] PROJECTION_ID = {
+        Columns._ID,
+    };
+
+    private static final String[] PROJECTION_CACHED = {
+        Columns._ID, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES,
+    };
+
+    private static final String[] PROJECTION_CACHE_SIZE = {
+        "SUM(" + Columns.SIZE_IN_BYTES + ")"
+    };
+
+    private static final String[] PROJECTION_DELETE_OLD = {
+        Columns._ID, Columns.URI, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES, Columns.LAST_ACCESS,
+    };
+
+    public static final String CREATE_TABLE = "CREATE TABLE " + TABLE + "("
+            + Columns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+            + Columns.URI + " TEXT NOT NULL,"
+            + Columns.MEDIA_SIZE + " INTEGER NOT NULL,"
+            + Columns.LAST_ACCESS + " INTEGER NOT NULL,"
+            + Columns.SIZE_IN_BYTES + " INTEGER NOT NULL,"
+            + "UNIQUE(" + Columns.URI + ", " + Columns.MEDIA_SIZE + "))";
+
+    public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE;
+
+    public static final String WHERE_THUMBNAIL = Columns.MEDIA_SIZE + " = "
+            + MediaSize.Thumbnail.getValue();
+
+    public static final String WHERE_NOT_THUMBNAIL = Columns.MEDIA_SIZE + " <> "
+            + MediaSize.Thumbnail.getValue();
+
+    public static final String WHERE_CLEAR_CACHE = Columns.LAST_ACCESS + " <= ?";
+
+    public static final String WHERE_CLEAR_CACHE_LARGE = WHERE_CLEAR_CACHE + " AND "
+            + WHERE_NOT_THUMBNAIL;
+
+    static class QueryCacheResults {
+        public QueryCacheResults(long id, int sizeVal) {
+            this.id = id;
+            this.size = MediaSize.fromInteger(sizeVal);
+        }
+        public long id;
+        public MediaSize size;
+    }
+
+    public MediaCacheDatabase(Context context) {
+        super(context, DB_NAME, null, DB_VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(CREATE_TABLE);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        db.execSQL(DROP_TABLE);
+        onCreate(db);
+        MediaCache.getInstance().clearCacheDir();
+    }
+
+    public Long getCached(Uri uri, MediaSize size) {
+        String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?";
+        SQLiteDatabase db = getWritableDatabase();
+        String[] whereArgs = {
+                uri.toString(), String.valueOf(size.getValue()),
+        };
+        Cursor cursor = db.query(TABLE, PROJECTION_ID, where, whereArgs, null, null, null);
+        Long id = null;
+        if (cursor.moveToNext()) {
+            id = cursor.getLong(0);
+        }
+        cursor.close();
+        if (id != null) {
+            String[] updateArgs = {
+                id.toString()
+            };
+            ContentValues values = new ContentValues();
+            values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+            db.beginTransaction();
+            try {
+                db.update(TABLE, values, Columns._ID + " = ?", updateArgs);
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+        }
+        return id;
+    }
+
+    public MediaSize executeOnBestCached(Uri uri, MediaSize size, Action action) {
+        String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " < ?";
+        String orderBy = Columns.MEDIA_SIZE + " DESC";
+        SQLiteDatabase db = getReadableDatabase();
+        String[] whereArgs = {
+                uri.toString(), String.valueOf(size.getValue()),
+        };
+        Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, orderBy);
+        MediaSize bestSize = null;
+        if (cursor.moveToNext()) {
+            long id = cursor.getLong(0);
+            bestSize = MediaSize.fromInteger(cursor.getInt(1));
+            long fileSize = cursor.getLong(2);
+            action.execute(uri, id, bestSize, fileSize);
+        }
+        cursor.close();
+        return bestSize;
+    }
+
+    public long insert(Uri uri, MediaSize size, Action action, File tempFile) {
+        SQLiteDatabase db = getWritableDatabase();
+        db.beginTransaction();
+        try {
+            ContentValues values = new ContentValues();
+            values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+            values.put(Columns.MEDIA_SIZE, size.getValue());
+            values.put(Columns.URI, uri.toString());
+            values.put(Columns.SIZE_IN_BYTES, tempFile.length());
+            long id = db.insert(TABLE, null, values);
+            if (id != -1) {
+                action.execute(uri, id, size, tempFile);
+                db.setTransactionSuccessful();
+            }
+            return id;
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public void updateLength(long id, long fileSize) {
+        ContentValues values = new ContentValues();
+        values.put(Columns.SIZE_IN_BYTES, fileSize);
+        String[] whereArgs = {
+            String.valueOf(id)
+        };
+        SQLiteDatabase db = getWritableDatabase();
+        db.beginTransaction();
+        try {
+            db.update(TABLE, values, Columns._ID + " = ?", whereArgs);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public void delete(Uri uri, MediaSize size, Action action) {
+        String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?";
+        String[] whereArgs = {
+                uri.toString(), String.valueOf(size.getValue()),
+        };
+        deleteRows(uri, where, whereArgs, action);
+    }
+
+    public void delete(Uri uri, Action action) {
+        String where = Columns.URI + " = ?";
+        String[] whereArgs = {
+            uri.toString()
+        };
+        deleteRows(uri, where, whereArgs, action);
+    }
+
+    private void deleteRows(Uri uri, String where, String[] whereArgs, Action action) {
+        SQLiteDatabase db = getWritableDatabase();
+        // Make this an atomic operation
+        db.beginTransaction();
+        Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, null);
+        while (cursor.moveToNext()) {
+            long id = cursor.getLong(0);
+            MediaSize size = MediaSize.fromInteger(cursor.getInt(1));
+            long length = cursor.getLong(2);
+            action.execute(uri, id, size, length);
+        }
+        cursor.close();
+        try {
+            db.delete(TABLE, where, whereArgs);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public void deleteOldCached(boolean includeThumbnails, long deleteSize, Action action) {
+        String where = includeThumbnails ? null : WHERE_NOT_THUMBNAIL;
+        long lastAccess = 0;
+        SQLiteDatabase db = getWritableDatabase();
+        db.beginTransaction();
+        try {
+            Cursor cursor = db.query(TABLE, PROJECTION_DELETE_OLD, where, null, null, null,
+                    Columns.LAST_ACCESS);
+            while (cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+                String uri = cursor.getString(1);
+                MediaSize size = MediaSize.fromInteger(cursor.getInt(2));
+                long length = cursor.getLong(3);
+                long imageLastAccess = cursor.getLong(4);
+
+                if (imageLastAccess != lastAccess && deleteSize < 0) {
+                    break; // We've deleted enough.
+                }
+                lastAccess = imageLastAccess;
+                action.execute(Uri.parse(uri), id, size, length);
+                deleteSize -= length;
+            }
+            cursor.close();
+            String[] whereArgs = {
+                String.valueOf(lastAccess),
+            };
+            String whereDelete = includeThumbnails ? WHERE_CLEAR_CACHE : WHERE_CLEAR_CACHE_LARGE;
+            db.delete(TABLE, whereDelete, whereArgs);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public long getCacheSize() {
+        return getCacheSize(null);
+    }
+
+    public long getThumbnailCacheSize() {
+        return getCacheSize(WHERE_THUMBNAIL);
+    }
+
+    private long getCacheSize(String where) {
+        SQLiteDatabase db = getReadableDatabase();
+        Cursor cursor = db.query(TABLE, PROJECTION_CACHE_SIZE, where, null, null, null, null);
+        long size = -1;
+        if (cursor.moveToNext()) {
+            size = cursor.getLong(0);
+        }
+        cursor.close();
+        return size;
+    }
+}
diff --git a/src/com/android/photos/data/MediaCacheUtils.java b/src/com/android/photos/data/MediaCacheUtils.java
new file mode 100644
index 0000000..e3ccd14
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheUtils.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+import android.util.Pools.SimplePool;
+import android.util.Pools.SynchronizedPool;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DecodeUtils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class MediaCacheUtils {
+    private static final String TAG = MediaCacheUtils.class.getSimpleName();
+    private static int QUALITY = 80;
+    private static final int BUFFER_SIZE = 4096;
+    private static final SimplePool<byte[]> mBufferPool = new SynchronizedPool<byte[]>(5);
+
+    private static final JobContext sJobStub = new JobContext() {
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public void setCancelListener(CancelListener listener) {
+        }
+
+        @Override
+        public boolean setMode(int mode) {
+            return true;
+        }
+    };
+
+    private static int mTargetThumbnailSize;
+    private static int mTargetPreviewSize;
+
+    public static void initialize(Context context) {
+        Resources resources = context.getResources();
+        mTargetThumbnailSize = resources.getDimensionPixelSize(R.dimen.size_thumbnail);
+        mTargetPreviewSize = resources.getDimensionPixelSize(R.dimen.size_preview);
+    }
+
+    public static int getTargetSize(MediaSize size) {
+        return (size == MediaSize.Thumbnail) ? mTargetThumbnailSize : mTargetPreviewSize;
+    }
+
+    public static boolean downsample(File inBitmap, MediaSize targetSize, File outBitmap) {
+        if (MediaSize.Original == targetSize) {
+            return false; // MediaCache should use the local path for this.
+        }
+        int size = getTargetSize(targetSize);
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        // TODO: remove unnecessary job context from DecodeUtils.
+        Bitmap bitmap = DecodeUtils.decodeThumbnail(sJobStub, inBitmap.getPath(), options, size,
+                MediaItem.TYPE_THUMBNAIL);
+        boolean success = (bitmap != null);
+        if (success) {
+            success = writeAndRecycle(bitmap, outBitmap);
+        }
+        return success;
+    }
+
+    public static boolean downsample(Bitmap inBitmap, MediaSize size, File outBitmap) {
+        if (MediaSize.Original == size) {
+            return false; // MediaCache should use the local path for this.
+        }
+        int targetSize = getTargetSize(size);
+        boolean success;
+        if (!needsDownsample(inBitmap, size)) {
+            success = writeAndRecycle(inBitmap, outBitmap);
+        } else {
+            float maxDimension = Math.max(inBitmap.getWidth(), inBitmap.getHeight());
+            float scale = targetSize / maxDimension;
+            int targetWidth = Math.round(scale * inBitmap.getWidth());
+            int targetHeight = Math.round(scale * inBitmap.getHeight());
+            Bitmap scaled = Bitmap.createScaledBitmap(inBitmap, targetWidth, targetHeight, false);
+            success = writeAndRecycle(scaled, outBitmap);
+            inBitmap.recycle();
+        }
+        return success;
+    }
+
+    public static boolean extractImageFromVideo(File inVideo, File outBitmap) {
+        Bitmap bitmap = BitmapUtils.createVideoThumbnail(inVideo.getPath());
+        return writeAndRecycle(bitmap, outBitmap);
+    }
+
+    public static boolean needsDownsample(Bitmap bitmap, MediaSize size) {
+        if (size == MediaSize.Original) {
+            return false;
+        }
+        int targetSize = getTargetSize(size);
+        int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+        return maxDimension > (targetSize * 4 / 3);
+    }
+
+    public static boolean writeAndRecycle(Bitmap bitmap, File outBitmap) {
+        boolean success = writeToFile(bitmap, outBitmap);
+        bitmap.recycle();
+        return success;
+    }
+
+    public static boolean writeToFile(Bitmap bitmap, File outBitmap) {
+        boolean success = false;
+        try {
+            FileOutputStream out = new FileOutputStream(outBitmap);
+            success = bitmap.compress(CompressFormat.JPEG, QUALITY, out);
+            out.close();
+        } catch (IOException e) {
+            Log.w(TAG, "Couldn't write bitmap to cache", e);
+            // success is already false
+        }
+        return success;
+    }
+
+    public static int copyStream(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = mBufferPool.acquire();
+        if (buffer == null) {
+            buffer = new byte[BUFFER_SIZE];
+        }
+        try {
+            int totalWritten = 0;
+            int bytesRead;
+            while ((bytesRead = in.read(buffer)) >= 0) {
+                out.write(buffer, 0, bytesRead);
+                totalWritten += bytesRead;
+            }
+            return totalWritten;
+        } finally {
+            Utils.closeSilently(in);
+            Utils.closeSilently(out);
+            mBufferPool.release(buffer);
+        }
+    }
+}
diff --git a/src/com/android/photos/data/MediaRetriever.java b/src/com/android/photos/data/MediaRetriever.java
new file mode 100644
index 0000000..f383e5f
--- /dev/null
+++ b/src/com/android/photos/data/MediaRetriever.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.net.Uri;
+
+import java.io.File;
+
+public interface MediaRetriever {
+    public enum MediaSize {
+        TemporaryThumbnail(5), Thumbnail(10), TemporaryPreview(15), Preview(20), Original(30);
+
+        private final int mValue;
+
+        private MediaSize(int value) {
+            mValue = value;
+        }
+
+        public int getValue() {
+            return mValue;
+        }
+
+        static MediaSize fromInteger(int value) {
+            switch (value) {
+                case 10:
+                    return MediaSize.Thumbnail;
+                case 20:
+                    return MediaSize.Preview;
+                case 30:
+                    return MediaSize.Original;
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+
+        public boolean isBetterThan(MediaSize that) {
+            return mValue > that.mValue;
+        }
+
+        public boolean isTemporary() {
+            return this == TemporaryThumbnail || this == TemporaryPreview;
+        }
+    }
+
+    /**
+     * Returns the local File for the given Uri. If the image is not stored
+     * locally, null should be returned. The image should not be retrieved if it
+     * isn't already available.
+     *
+     * @param contentUri The media URI to search for.
+     * @return The local File of the image if it is available or null if it
+     *         isn't.
+     */
+    File getLocalFile(Uri contentUri);
+
+    /**
+     * Returns the fast access image type for a given image size, if supported.
+     * This image should be smaller than size and should be quick to retrieve.
+     * It does not have to obey the expected aspect ratio.
+     *
+     * @param contentUri The original media Uri.
+     * @param size The target size to search for a fast-access image.
+     * @return The fast image type supported for the given image size or null of
+     *         no fast image is supported.
+     */
+    MediaSize getFastImageSize(Uri contentUri, MediaSize size);
+
+    /**
+     * Returns a byte array containing the contents of the fast temporary image
+     * for a given image size. For example, a thumbnail may be smaller or of a
+     * different aspect ratio than the generated thumbnail.
+     *
+     * @param contentUri The original media Uri.
+     * @param temporarySize The target media size. Guaranteed to be a MediaSize
+     *            for which isTemporary() returns true.
+     * @return A byte array of contents for for the given contentUri and
+     *         fastImageType. null can be retrieved if the quick retrieval
+     *         fails.
+     */
+    byte[] getTemporaryImage(Uri contentUri, MediaSize temporarySize);
+
+    /**
+     * Retrieves an image and saves it to a file.
+     *
+     * @param contentUri The original media Uri.
+     * @param size The target media size.
+     * @param tempFile The file to write the bitmap to.
+     * @return <code>true</code> on success.
+     */
+    boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile);
+
+    /**
+     * Normalizes a URI that may have additional parameters. It is fine to
+     * return contentUri. This is executed on the calling thread, so it must be
+     * a fast access operation and cannot depend, for example, on I/O.
+     *
+     * @param contentUri The URI to normalize
+     * @param size The size of the image being requested
+     * @return The normalized URI representation of contentUri.
+     */
+    Uri normalizeUri(Uri contentUri, MediaSize size);
+
+    /**
+     * Normalize the MediaSize for a given URI. Typically the size returned
+     * would be the passed-in size. Some URIs may only have one size used and
+     * should be treaded as Thumbnails, for example. This is executed on the
+     * calling thread, so it must be a fast access operation and cannot depend,
+     * for example, on I/O.
+     *
+     * @param contentUri The URI for the size being normalized.
+     * @param size The size to be normalized.
+     * @return The normalized size of the given URI.
+     */
+    MediaSize normalizeMediaSize(Uri contentUri, MediaSize size);
+}
diff --git a/src/com/android/photos/data/NotificationWatcher.java b/src/com/android/photos/data/NotificationWatcher.java
new file mode 100644
index 0000000..9041c23
--- /dev/null
+++ b/src/com/android/photos/data/NotificationWatcher.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.net.Uri;
+
+import com.android.photos.data.PhotoProvider.ChangeNotification;
+
+import java.util.ArrayList;
+
+/**
+ * Used for capturing notifications from PhotoProvider without relying on
+ * ContentResolver. MockContentResolver does not allow sending notification to
+ * ContentObservers, so PhotoProvider allows this alternative for testing.
+ */
+public class NotificationWatcher implements ChangeNotification {
+    private ArrayList<Uri> mUris = new ArrayList<Uri>();
+    private boolean mSyncToNetwork = false;
+
+    @Override
+    public void notifyChange(Uri uri, boolean syncToNetwork) {
+        mUris.add(uri);
+        mSyncToNetwork = mSyncToNetwork || syncToNetwork;
+    }
+
+    public boolean isNotified(Uri uri) {
+        return mUris.contains(uri);
+    }
+
+    public int notificationCount() {
+        return mUris.size();
+    }
+
+    public boolean syncToNetwork() {
+        return mSyncToNetwork;
+    }
+
+    public void reset() {
+        mUris.clear();
+        mSyncToNetwork = false;
+    }
+}
diff --git a/src/com/android/photos/data/PhotoDatabase.java b/src/com/android/photos/data/PhotoDatabase.java
new file mode 100644
index 0000000..0c7b227
--- /dev/null
+++ b/src/com/android/photos/data/PhotoDatabase.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.photos.data.PhotoProvider.Accounts;
+import com.android.photos.data.PhotoProvider.Albums;
+import com.android.photos.data.PhotoProvider.Metadata;
+import com.android.photos.data.PhotoProvider.Photos;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used in PhotoProvider to create and access the database containing
+ * information about photo and video information stored on the server.
+ */
+public class PhotoDatabase extends SQLiteOpenHelper {
+    @SuppressWarnings("unused")
+    private static final String TAG = PhotoDatabase.class.getSimpleName();
+    static final int DB_VERSION = 3;
+
+    private static final String SQL_CREATE_TABLE = "CREATE TABLE ";
+
+    private static final String[][] CREATE_PHOTO = {
+        { Photos._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+        // Photos.ACCOUNT_ID is a foreign key to Accounts._ID
+        { Photos.ACCOUNT_ID, "INTEGER NOT NULL" },
+        { Photos.WIDTH, "INTEGER NOT NULL" },
+        { Photos.HEIGHT, "INTEGER NOT NULL" },
+        { Photos.DATE_TAKEN, "INTEGER NOT NULL" },
+        // Photos.ALBUM_ID is a foreign key to Albums._ID
+        { Photos.ALBUM_ID, "INTEGER" },
+        { Photos.MIME_TYPE, "TEXT NOT NULL" },
+        { Photos.TITLE, "TEXT" },
+        { Photos.DATE_MODIFIED, "INTEGER" },
+        { Photos.ROTATION, "INTEGER" },
+    };
+
+    private static final String[][] CREATE_ALBUM = {
+        { Albums._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+        // Albums.ACCOUNT_ID is a foreign key to Accounts._ID
+        { Albums.ACCOUNT_ID, "INTEGER NOT NULL" },
+        // Albums.PARENT_ID is a foreign key to Albums._ID
+        { Albums.PARENT_ID, "INTEGER" },
+        { Albums.ALBUM_TYPE, "TEXT" },
+        { Albums.VISIBILITY, "INTEGER NOT NULL" },
+        { Albums.LOCATION_STRING, "TEXT" },
+        { Albums.TITLE, "TEXT NOT NULL" },
+        { Albums.SUMMARY, "TEXT" },
+        { Albums.DATE_PUBLISHED, "INTEGER" },
+        { Albums.DATE_MODIFIED, "INTEGER" },
+        createUniqueConstraint(Albums.PARENT_ID, Albums.TITLE),
+    };
+
+    private static final String[][] CREATE_METADATA = {
+        { Metadata._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+        // Metadata.PHOTO_ID is a foreign key to Photos._ID
+        { Metadata.PHOTO_ID, "INTEGER NOT NULL" },
+        { Metadata.KEY, "TEXT NOT NULL" },
+        { Metadata.VALUE, "TEXT NOT NULL" },
+        createUniqueConstraint(Metadata.PHOTO_ID, Metadata.KEY),
+    };
+
+    private static final String[][] CREATE_ACCOUNT = {
+        { Accounts._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+        { Accounts.ACCOUNT_NAME, "TEXT UNIQUE NOT NULL" },
+    };
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        createTable(db, Accounts.TABLE, getAccountTableDefinition());
+        createTable(db, Albums.TABLE, getAlbumTableDefinition());
+        createTable(db, Photos.TABLE, getPhotoTableDefinition());
+        createTable(db, Metadata.TABLE, getMetadataTableDefinition());
+    }
+
+    public PhotoDatabase(Context context, String dbName, int dbVersion) {
+        super(context, dbName, null, dbVersion);
+    }
+
+    public PhotoDatabase(Context context, String dbName) {
+        super(context, dbName, null, DB_VERSION);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        recreate(db);
+    }
+
+    @Override
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        recreate(db);
+    }
+
+    private void recreate(SQLiteDatabase db) {
+        dropTable(db, Metadata.TABLE);
+        dropTable(db, Photos.TABLE);
+        dropTable(db, Albums.TABLE);
+        dropTable(db, Accounts.TABLE);
+        onCreate(db);
+    }
+
+    protected List<String[]> getAlbumTableDefinition() {
+        return tableCreationStrings(CREATE_ALBUM);
+    }
+
+    protected List<String[]> getPhotoTableDefinition() {
+        return tableCreationStrings(CREATE_PHOTO);
+    }
+
+    protected List<String[]> getMetadataTableDefinition() {
+        return tableCreationStrings(CREATE_METADATA);
+    }
+
+    protected List<String[]> getAccountTableDefinition() {
+        return tableCreationStrings(CREATE_ACCOUNT);
+    }
+
+    protected static void createTable(SQLiteDatabase db, String table, List<String[]> columns) {
+        StringBuilder create = new StringBuilder(SQL_CREATE_TABLE);
+        create.append(table).append('(');
+        boolean first = true;
+        for (String[] column : columns) {
+            if (!first) {
+                create.append(',');
+            }
+            first = false;
+            for (String val: column) {
+                create.append(val).append(' ');
+            }
+        }
+        create.append(')');
+        db.beginTransaction();
+        try {
+            db.execSQL(create.toString());
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    protected static String[] createUniqueConstraint(String column1, String column2) {
+        return new String[] {
+                "UNIQUE(", column1, ",", column2, ")"
+        };
+    }
+
+    protected static List<String[]> tableCreationStrings(String[][] createTable) {
+        ArrayList<String[]> create = new ArrayList<String[]>(createTable.length);
+        for (String[] line: createTable) {
+            create.add(line);
+        }
+        return create;
+    }
+
+    protected static void addToTable(List<String[]> createTable, String[][] columns, String[][] constraints) {
+        if (columns != null) {
+            for (String[] column: columns) {
+                createTable.add(0, column);
+            }
+        }
+        if (constraints != null) {
+            for (String[] constraint: constraints) {
+                createTable.add(constraint);
+            }
+        }
+    }
+
+    protected static void dropTable(SQLiteDatabase db, String table) {
+        db.beginTransaction();
+        try {
+            db.execSQL("drop table if exists " + table);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+}
diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java
new file mode 100644
index 0000000..d4310ca
--- /dev/null
+++ b/src/com/android/photos/data/PhotoProvider.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.provider.BaseColumns;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.List;
+
+/**
+ * A provider that gives access to photo and video information for media stored
+ * on the server. Only media that is or will be put on the server will be
+ * accessed by this provider. Use Photos.CONTENT_URI to query all photos and
+ * videos. Use Albums.CONTENT_URI to query all albums. Use Metadata.CONTENT_URI
+ * to query metadata about a photo or video, based on the ID of the media. Use
+ * ImageCache.THUMBNAIL_CONTENT_URI, ImageCache.PREVIEW_CONTENT_URI, or
+ * ImageCache.ORIGINAL_CONTENT_URI to query the path of the thumbnail, preview,
+ * or original-sized image respectfully. <br/>
+ * To add or update metadata, use the update function rather than insert. All
+ * values for the metadata must be in the ContentValues, even if they are also
+ * in the selection. The selection and selectionArgs are not used when updating
+ * metadata. If the metadata values are null, the row will be deleted.
+ */
+public class PhotoProvider extends SQLiteContentProvider {
+    @SuppressWarnings("unused")
+    private static final String TAG = PhotoProvider.class.getSimpleName();
+
+    protected static final String DB_NAME = "photo.db";
+    public static final String AUTHORITY = PhotoProviderAuthority.AUTHORITY;
+    static final Uri BASE_CONTENT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY)
+            .build();
+
+    // Used to allow mocking out the change notification because
+    // MockContextResolver disallows system-wide notification.
+    public static interface ChangeNotification {
+        void notifyChange(Uri uri, boolean syncToNetwork);
+    }
+
+    /**
+     * Contains columns that can be accessed via Accounts.CONTENT_URI
+     */
+    public static interface Accounts extends BaseColumns {
+        /**
+         * Internal database table used for account information
+         */
+        public static final String TABLE = "accounts";
+        /**
+         * Content URI for account information
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+        /**
+         * User name for this account.
+         */
+        public static final String ACCOUNT_NAME = "name";
+    }
+
+    /**
+     * Contains columns that can be accessed via Photos.CONTENT_URI.
+     */
+    public static interface Photos extends BaseColumns {
+        /**
+         * The image_type query parameter required for requesting a specific
+         * size of image.
+         */
+        public static final String MEDIA_SIZE_QUERY_PARAMETER = "media_size";
+
+        /** Internal database table used for basic photo information. */
+        public static final String TABLE = "photos";
+        /** Content URI for basic photo and video information. */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+
+        /** Long foreign key to Accounts._ID */
+        public static final String ACCOUNT_ID = "account_id";
+        /** Column name for the width of the original image. Integer value. */
+        public static final String WIDTH = "width";
+        /** Column name for the height of the original image. Integer value. */
+        public static final String HEIGHT = "height";
+        /**
+         * Column name for the date that the original image was taken. Long
+         * value indicating the milliseconds since epoch in the GMT time zone.
+         */
+        public static final String DATE_TAKEN = "date_taken";
+        /**
+         * Column name indicating the long value of the album id that this image
+         * resides in. Will be NULL if it it has not been uploaded to the
+         * server.
+         */
+        public static final String ALBUM_ID = "album_id";
+        /** The column name for the mime-type String. */
+        public static final String MIME_TYPE = "mime_type";
+        /** The title of the photo. String value. */
+        public static final String TITLE = "title";
+        /** The date the photo entry was last updated. Long value. */
+        public static final String DATE_MODIFIED = "date_modified";
+        /**
+         * The rotation of the photo in degrees, if rotation has not already
+         * been applied. Integer value.
+         */
+        public static final String ROTATION = "rotation";
+    }
+
+    /**
+     * Contains columns and Uri for accessing album information.
+     */
+    public static interface Albums extends BaseColumns {
+        /** Internal database table used album information. */
+        public static final String TABLE = "albums";
+        /** Content URI for album information. */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+
+        /** Long foreign key to Accounts._ID */
+        public static final String ACCOUNT_ID = "account_id";
+        /** Parent directory or null if this is in the root. */
+        public static final String PARENT_ID = "parent_id";
+        /** The type of album. Non-null, if album is auto-generated. String value. */
+        public static final String ALBUM_TYPE = "album_type";
+        /**
+         * Column name for the visibility level of the album. Can be any of the
+         * VISIBILITY_* values.
+         */
+        public static final String VISIBILITY = "visibility";
+        /** The user-specified location associated with the album. String value. */
+        public static final String LOCATION_STRING = "location_string";
+        /** The title of the album. String value. */
+        public static final String TITLE = "title";
+        /** A short summary of the contents of the album. String value. */
+        public static final String SUMMARY = "summary";
+        /** The date the album was created. Long value */
+        public static final String DATE_PUBLISHED = "date_published";
+        /** The date the album entry was last updated. Long value. */
+        public static final String DATE_MODIFIED = "date_modified";
+
+        // Privacy values for Albums.VISIBILITY
+        public static final int VISIBILITY_PRIVATE = 1;
+        public static final int VISIBILITY_SHARED = 2;
+        public static final int VISIBILITY_PUBLIC = 3;
+    }
+
+    /**
+     * Contains columns and Uri for accessing photo and video metadata
+     */
+    public static interface Metadata extends BaseColumns {
+        /** Internal database table used metadata information. */
+        public static final String TABLE = "metadata";
+        /** Content URI for photo and video metadata. */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+        /** Foreign key to photo_id. Long value. */
+        public static final String PHOTO_ID = "photo_id";
+        /** Metadata key. String value */
+        public static final String KEY = "key";
+        /**
+         * Metadata value. Type is based on key.
+         */
+        public static final String VALUE = "value";
+
+        /** A short summary of the photo. String value. */
+        public static final String KEY_SUMMARY = "summary";
+        /** The date the photo was added. Long value. */
+        public static final String KEY_PUBLISHED = "date_published";
+        /** The date the photo was last updated. Long value. */
+        public static final String KEY_DATE_UPDATED = "date_updated";
+        /** The size of the photo is bytes. Integer value. */
+        public static final String KEY_SIZE_IN_BTYES = "size";
+        /** The latitude associated with the photo. Double value. */
+        public static final String KEY_LATITUDE = "latitude";
+        /** The longitude associated with the photo. Double value. */
+        public static final String KEY_LONGITUDE = "longitude";
+
+        /** The make of the camera used. String value. */
+        public static final String KEY_EXIF_MAKE = ExifInterface.TAG_MAKE;
+        /** The model of the camera used. String value. */
+        public static final String KEY_EXIF_MODEL = ExifInterface.TAG_MODEL;;
+        /** The exposure time used. Float value. */
+        public static final String KEY_EXIF_EXPOSURE = ExifInterface.TAG_EXPOSURE_TIME;
+        /** Whether the flash was used. Boolean value. */
+        public static final String KEY_EXIF_FLASH = ExifInterface.TAG_FLASH;
+        /** The focal length used. Float value. */
+        public static final String KEY_EXIF_FOCAL_LENGTH = ExifInterface.TAG_FOCAL_LENGTH;
+        /** The fstop value used. Float value. */
+        public static final String KEY_EXIF_FSTOP = ExifInterface.TAG_APERTURE;
+        /** The ISO equivalent value used. Integer value. */
+        public static final String KEY_EXIF_ISO = ExifInterface.TAG_ISO;
+    }
+
+    // SQL used within this class.
+    protected static final String WHERE_ID = BaseColumns._ID + " = ?";
+    protected static final String WHERE_METADATA_ID = Metadata.PHOTO_ID + " = ? AND "
+            + Metadata.KEY + " = ?";
+
+    protected static final String SELECT_ALBUM_ID = "SELECT " + Albums._ID + " FROM "
+            + Albums.TABLE;
+    protected static final String SELECT_PHOTO_ID = "SELECT " + Photos._ID + " FROM "
+            + Photos.TABLE;
+    protected static final String SELECT_PHOTO_COUNT = "SELECT COUNT(*) FROM " + Photos.TABLE;
+    protected static final String DELETE_PHOTOS = "DELETE FROM " + Photos.TABLE;
+    protected static final String DELETE_METADATA = "DELETE FROM " + Metadata.TABLE;
+    protected static final String SELECT_METADATA_COUNT = "SELECT COUNT(*) FROM " + Metadata.TABLE;
+    protected static final String WHERE = " WHERE ";
+    protected static final String IN = " IN ";
+    protected static final String NESTED_SELECT_START = "(";
+    protected static final String NESTED_SELECT_END = ")";
+    protected static final String[] PROJECTION_COUNT = {
+        "COUNT(*)"
+    };
+
+    /**
+     * For selecting the mime-type for an image.
+     */
+    private static final String[] PROJECTION_MIME_TYPE = {
+        Photos.MIME_TYPE,
+    };
+
+    protected static final String[] BASE_COLUMNS_ID = {
+        BaseColumns._ID,
+    };
+
+    protected ChangeNotification mNotifier = null;
+    protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    protected static final int MATCH_PHOTO = 1;
+    protected static final int MATCH_PHOTO_ID = 2;
+    protected static final int MATCH_ALBUM = 3;
+    protected static final int MATCH_ALBUM_ID = 4;
+    protected static final int MATCH_METADATA = 5;
+    protected static final int MATCH_METADATA_ID = 6;
+    protected static final int MATCH_ACCOUNT = 7;
+    protected static final int MATCH_ACCOUNT_ID = 8;
+
+    static {
+        sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO);
+        // match against Photos._ID
+        sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID);
+        sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM);
+        // match against Albums._ID
+        sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID);
+        sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA);
+        // match against metadata/<Metadata._ID>
+        sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID);
+        sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT);
+        // match against Accounts._ID
+        sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID);
+    }
+
+    @Override
+    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
+            boolean callerIsSyncAdapter) {
+        int match = matchUri(uri);
+        selection = addIdToSelection(match, selection);
+        selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
+        return deleteCascade(uri, match, selection, selectionArgs);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        Cursor cursor = query(uri, PROJECTION_MIME_TYPE, null, null, null);
+        String mimeType = null;
+        if (cursor.moveToNext()) {
+            mimeType = cursor.getString(0);
+        }
+        cursor.close();
+        return mimeType;
+    }
+
+    @Override
+    public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+        int match = matchUri(uri);
+        validateMatchTable(match);
+        String table = getTableFromMatch(match, uri);
+        SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+        Uri insertedUri = null;
+        long id = db.insert(table, null, values);
+        if (id != -1) {
+            // uri already matches the table.
+            insertedUri = ContentUris.withAppendedId(uri, id);
+            postNotifyUri(insertedUri);
+        }
+        return insertedUri;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return query(uri, projection, selection, selectionArgs, sortOrder, null);
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder, CancellationSignal cancellationSignal) {
+        projection = replaceCount(projection);
+        int match = matchUri(uri);
+        selection = addIdToSelection(match, selection);
+        selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
+        String table = getTableFromMatch(match, uri);
+        Cursor c = query(table, projection, selection, selectionArgs, sortOrder, cancellationSignal);
+        if (c != null) {
+            c.setNotificationUri(getContext().getContentResolver(), uri);
+        }
+        return c;
+    }
+
+    @Override
+    public int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs, boolean callerIsSyncAdapter) {
+        int match = matchUri(uri);
+        int rowsUpdated = 0;
+        SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+        if (match == MATCH_METADATA) {
+            rowsUpdated = modifyMetadata(db, values);
+        } else {
+            selection = addIdToSelection(match, selection);
+            selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
+            String table = getTableFromMatch(match, uri);
+            rowsUpdated = db.update(table, values, selection, selectionArgs);
+        }
+        postNotifyUri(uri);
+        return rowsUpdated;
+    }
+
+    public void setMockNotification(ChangeNotification notification) {
+        mNotifier = notification;
+    }
+
+    protected static String addIdToSelection(int match, String selection) {
+        String where;
+        switch (match) {
+            case MATCH_PHOTO_ID:
+            case MATCH_ALBUM_ID:
+            case MATCH_METADATA_ID:
+                where = WHERE_ID;
+                break;
+            default:
+                return selection;
+        }
+        return DatabaseUtils.concatenateWhere(selection, where);
+    }
+
+    protected static String[] addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs) {
+        String[] whereArgs;
+        switch (match) {
+            case MATCH_PHOTO_ID:
+            case MATCH_ALBUM_ID:
+            case MATCH_METADATA_ID:
+                whereArgs = new String[] {
+                    uri.getPathSegments().get(1),
+                };
+                break;
+            default:
+                return selectionArgs;
+        }
+        return DatabaseUtils.appendSelectionArgs(selectionArgs, whereArgs);
+    }
+
+    protected static String[] addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri) {
+        List<String> segments = uri.getPathSegments();
+        String[] additionalArgs = {
+                segments.get(1),
+                segments.get(2),
+        };
+
+        return DatabaseUtils.appendSelectionArgs(selectionArgs, additionalArgs);
+    }
+
+    protected static String getTableFromMatch(int match, Uri uri) {
+        String table;
+        switch (match) {
+            case MATCH_PHOTO:
+            case MATCH_PHOTO_ID:
+                table = Photos.TABLE;
+                break;
+            case MATCH_ALBUM:
+            case MATCH_ALBUM_ID:
+                table = Albums.TABLE;
+                break;
+            case MATCH_METADATA:
+            case MATCH_METADATA_ID:
+                table = Metadata.TABLE;
+                break;
+            case MATCH_ACCOUNT:
+            case MATCH_ACCOUNT_ID:
+                table = Accounts.TABLE;
+                break;
+            default:
+                throw unknownUri(uri);
+        }
+        return table;
+    }
+
+    @Override
+    public SQLiteOpenHelper getDatabaseHelper(Context context) {
+        return new PhotoDatabase(context, DB_NAME);
+    }
+
+    private int modifyMetadata(SQLiteDatabase db, ContentValues values) {
+        int rowCount;
+        if (values.get(Metadata.VALUE) == null) {
+            String[] selectionArgs = {
+                    values.getAsString(Metadata.PHOTO_ID), values.getAsString(Metadata.KEY),
+            };
+            rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs);
+        } else {
+            long rowId = db.replace(Metadata.TABLE, null, values);
+            rowCount = (rowId == -1) ? 0 : 1;
+        }
+        return rowCount;
+    }
+
+    private int matchUri(Uri uri) {
+        int match = sUriMatcher.match(uri);
+        if (match == UriMatcher.NO_MATCH) {
+            throw unknownUri(uri);
+        }
+        return match;
+    }
+
+    @Override
+    protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) {
+        if (mNotifier != null) {
+            mNotifier.notifyChange(uri, syncToNetwork);
+        } else {
+            super.notifyChange(resolver, uri, syncToNetwork);
+        }
+    }
+
+    protected static IllegalArgumentException unknownUri(Uri uri) {
+        return new IllegalArgumentException("Unknown Uri format: " + uri);
+    }
+
+    protected static String nestWhere(String matchColumn, String table, String nestedWhere) {
+        String query = SQLiteQueryBuilder.buildQueryString(false, table, BASE_COLUMNS_ID,
+                nestedWhere, null, null, null, null);
+        return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END;
+    }
+
+    protected static String metadataSelectionFromPhotos(String where) {
+        return nestWhere(Metadata.PHOTO_ID, Photos.TABLE, where);
+    }
+
+    protected static String photoSelectionFromAlbums(String where) {
+        return nestWhere(Photos.ALBUM_ID, Albums.TABLE, where);
+    }
+
+    protected static String photoSelectionFromAccounts(String where) {
+        return nestWhere(Photos.ACCOUNT_ID, Accounts.TABLE, where);
+    }
+
+    protected static String albumSelectionFromAccounts(String where) {
+        return nestWhere(Albums.ACCOUNT_ID, Accounts.TABLE, where);
+    }
+
+    protected int deleteCascade(Uri uri, int match, String selection, String[] selectionArgs) {
+        switch (match) {
+            case MATCH_PHOTO:
+            case MATCH_PHOTO_ID:
+                deleteCascade(Metadata.CONTENT_URI, MATCH_METADATA,
+                        metadataSelectionFromPhotos(selection), selectionArgs);
+                break;
+            case MATCH_ALBUM:
+            case MATCH_ALBUM_ID:
+                deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO,
+                        photoSelectionFromAlbums(selection), selectionArgs);
+                break;
+            case MATCH_ACCOUNT:
+            case MATCH_ACCOUNT_ID:
+                deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO,
+                        photoSelectionFromAccounts(selection), selectionArgs);
+                deleteCascade(Albums.CONTENT_URI, MATCH_ALBUM,
+                        albumSelectionFromAccounts(selection), selectionArgs);
+                break;
+        }
+        SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+        String table = getTableFromMatch(match, uri);
+        int deleted = db.delete(table, selection, selectionArgs);
+        if (deleted > 0) {
+            postNotifyUri(uri);
+        }
+        return deleted;
+    }
+
+    private static void validateMatchTable(int match) {
+        switch (match) {
+            case MATCH_PHOTO:
+            case MATCH_ALBUM:
+            case MATCH_METADATA:
+            case MATCH_ACCOUNT:
+                break;
+            default:
+                throw new IllegalArgumentException("Operation not allowed on an existing row.");
+        }
+    }
+
+    protected Cursor query(String table, String[] columns, String selection,
+            String[] selectionArgs, String orderBy, CancellationSignal cancellationSignal) {
+        SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
+        if (ApiHelper.HAS_CANCELLATION_SIGNAL) {
+            return db.query(false, table, columns, selection, selectionArgs, null, null,
+                    orderBy, null, cancellationSignal);
+        } else {
+            return db.query(table, columns, selection, selectionArgs, null, null, orderBy);
+        }
+    }
+
+    protected static String[] replaceCount(String[] projection) {
+        if (projection != null && projection.length == 1
+                && BaseColumns._COUNT.equals(projection[0])) {
+            return PROJECTION_COUNT;
+        }
+        return projection;
+    }
+}
diff --git a/src/com/android/photos/data/PhotoSetLoader.java b/src/com/android/photos/data/PhotoSetLoader.java
new file mode 100644
index 0000000..56c82c4
--- /dev/null
+++ b/src/com/android/photos/data/PhotoSetLoader.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Files;
+import android.provider.MediaStore.Files.FileColumns;
+
+import com.android.photos.drawables.DataUriThumbnailDrawable;
+import com.android.photos.shims.LoaderCompatShim;
+
+import java.util.ArrayList;
+
+public class PhotoSetLoader extends CursorLoader implements LoaderCompatShim<Cursor> {
+
+    public static final String SUPPORTED_OPERATIONS = "supported_operations";
+
+    private static final Uri CONTENT_URI = Files.getContentUri("external");
+    public static final String[] PROJECTION = new String[] {
+        FileColumns._ID,
+        FileColumns.DATA,
+        FileColumns.WIDTH,
+        FileColumns.HEIGHT,
+        FileColumns.DATE_ADDED,
+        FileColumns.MEDIA_TYPE,
+        SUPPORTED_OPERATIONS,
+    };
+
+    private static final String SORT_ORDER = FileColumns.DATE_ADDED + " DESC";
+    private static final String SELECTION =
+            FileColumns.MEDIA_TYPE + " == " + FileColumns.MEDIA_TYPE_IMAGE
+            + " OR "
+            + FileColumns.MEDIA_TYPE + " == " + FileColumns.MEDIA_TYPE_VIDEO;
+
+    public static final int INDEX_ID = 0;
+    public static final int INDEX_DATA = 1;
+    public static final int INDEX_WIDTH = 2;
+    public static final int INDEX_HEIGHT = 3;
+    public static final int INDEX_DATE_ADDED = 4;
+    public static final int INDEX_MEDIA_TYPE = 5;
+    public static final int INDEX_SUPPORTED_OPERATIONS = 6;
+
+    private static final Uri GLOBAL_CONTENT_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/external/");
+    private final ContentObserver mGlobalObserver = new ForceLoadContentObserver();
+
+    public PhotoSetLoader(Context context) {
+        super(context, CONTENT_URI, PROJECTION, SELECTION, null, SORT_ORDER);
+    }
+
+    @Override
+    protected void onStartLoading() {
+        super.onStartLoading();
+        getContext().getContentResolver().registerContentObserver(GLOBAL_CONTENT_URI,
+                true, mGlobalObserver);
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+        getContext().getContentResolver().unregisterContentObserver(mGlobalObserver);
+    }
+
+    @Override
+    public Drawable drawableForItem(Cursor item, Drawable recycle) {
+        DataUriThumbnailDrawable drawable = null;
+        if (recycle == null || !(recycle instanceof DataUriThumbnailDrawable)) {
+            drawable = new DataUriThumbnailDrawable();
+        } else {
+            drawable = (DataUriThumbnailDrawable) recycle;
+        }
+        drawable.setImage(item.getString(INDEX_DATA),
+                item.getInt(INDEX_WIDTH), item.getInt(INDEX_HEIGHT));
+        return drawable;
+    }
+
+    @Override
+    public Uri uriForItem(Cursor item) {
+        return null;
+    }
+
+    @Override
+    public ArrayList<Uri> urisForSubItems(Cursor item) {
+        return null;
+    }
+
+    @Override
+    public void deleteItemWithPath(Object path) {
+
+    }
+
+    @Override
+    public Object getPathForItem(Cursor item) {
+        return null;
+    }
+}
diff --git a/src/com/android/photos/data/SQLiteContentProvider.java b/src/com/android/photos/data/SQLiteContentProvider.java
new file mode 100644
index 0000000..daffa6e
--- /dev/null
+++ b/src/com/android/photos/data/SQLiteContentProvider.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * General purpose {@link ContentProvider} base class that uses SQLiteDatabase
+ * for storage.
+ */
+public abstract class SQLiteContentProvider extends ContentProvider {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "SQLiteContentProvider";
+
+    private SQLiteOpenHelper mOpenHelper;
+    private Set<Uri> mChangedUris;
+
+    private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
+    private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
+
+    /**
+     * Maximum number of operations allowed in a batch between yield points.
+     */
+    private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
+
+    @Override
+    public boolean onCreate() {
+        Context context = getContext();
+        mOpenHelper = getDatabaseHelper(context);
+        mChangedUris = new HashSet<Uri>();
+        return true;
+    }
+
+    @Override
+    public void shutdown() {
+        getDatabaseHelper().close();
+    }
+
+    /**
+     * Returns a {@link SQLiteOpenHelper} that can open the database.
+     */
+    public abstract SQLiteOpenHelper getDatabaseHelper(Context context);
+
+    /**
+     * The equivalent of the {@link #insert} method, but invoked within a
+     * transaction.
+     */
+    public abstract Uri insertInTransaction(Uri uri, ContentValues values,
+            boolean callerIsSyncAdapter);
+
+    /**
+     * The equivalent of the {@link #update} method, but invoked within a
+     * transaction.
+     */
+    public abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs, boolean callerIsSyncAdapter);
+
+    /**
+     * The equivalent of the {@link #delete} method, but invoked within a
+     * transaction.
+     */
+    public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
+            boolean callerIsSyncAdapter);
+
+    /**
+     * Call this to add a URI to the list of URIs to be notified when the
+     * transaction is committed.
+     */
+    protected void postNotifyUri(Uri uri) {
+        synchronized (mChangedUris) {
+            mChangedUris.add(uri);
+        }
+    }
+
+    public boolean isCallerSyncAdapter(Uri uri) {
+        return false;
+    }
+
+    public SQLiteOpenHelper getDatabaseHelper() {
+        return mOpenHelper;
+    }
+
+    private boolean applyingBatch() {
+        return mApplyingBatch.get() != null && mApplyingBatch.get();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        Uri result = null;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                result = insertInTransaction(uri, values, callerIsSyncAdapter);
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+
+            onEndTransaction(callerIsSyncAdapter);
+        } else {
+            result = insertInTransaction(uri, values, callerIsSyncAdapter);
+        }
+        return result;
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        int numValues = values.length;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            for (int i = 0; i < numValues; i++) {
+                @SuppressWarnings("unused")
+                Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter);
+                db.yieldIfContendedSafely();
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+
+        onEndTransaction(callerIsSyncAdapter);
+        return numValues;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        int count = 0;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                count = updateInTransaction(uri, values, selection, selectionArgs,
+                        callerIsSyncAdapter);
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+
+            onEndTransaction(callerIsSyncAdapter);
+        } else {
+            count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter);
+        }
+
+        return count;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        int count = 0;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+
+            onEndTransaction(callerIsSyncAdapter);
+        } else {
+            count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+        }
+        return count;
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+            throws OperationApplicationException {
+        int ypCount = 0;
+        int opCount = 0;
+        boolean callerIsSyncAdapter = false;
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            mApplyingBatch.set(true);
+            final int numOperations = operations.size();
+            final ContentProviderResult[] results = new ContentProviderResult[numOperations];
+            for (int i = 0; i < numOperations; i++) {
+                if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+                    throw new OperationApplicationException(
+                            "Too many content provider operations between yield points. "
+                                    + "The maximum number of operations per yield point is "
+                                    + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
+                }
+                final ContentProviderOperation operation = operations.get(i);
+                if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) {
+                    callerIsSyncAdapter = true;
+                }
+                if (i > 0 && operation.isYieldAllowed()) {
+                    opCount = 0;
+                    if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
+                        ypCount++;
+                    }
+                }
+                results[i] = operation.apply(this, results, i);
+            }
+            db.setTransactionSuccessful();
+            return results;
+        } finally {
+            mApplyingBatch.set(false);
+            db.endTransaction();
+            onEndTransaction(callerIsSyncAdapter);
+        }
+    }
+
+    protected Set<Uri> onEndTransaction(boolean callerIsSyncAdapter) {
+        Set<Uri> changed;
+        synchronized (mChangedUris) {
+            changed = new HashSet<Uri>(mChangedUris);
+            mChangedUris.clear();
+        }
+        ContentResolver resolver = getContext().getContentResolver();
+        for (Uri uri : changed) {
+            boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri);
+            notifyChange(resolver, uri, syncToNetwork);
+        }
+        return changed;
+    }
+
+    protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) {
+        resolver.notifyChange(uri, null, syncToNetwork);
+    }
+
+    protected boolean syncToNetwork(Uri uri) {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/photos/data/SparseArrayBitmapPool.java b/src/com/android/photos/data/SparseArrayBitmapPool.java
new file mode 100644
index 0000000..95e1026
--- /dev/null
+++ b/src/com/android/photos/data/SparseArrayBitmapPool.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.util.SparseArray;
+
+import android.util.Pools.Pool;
+import android.util.Pools.SimplePool;
+
+/**
+ * Bitmap pool backed by a sparse array indexing linked lists of bitmaps
+ * sharing the same width. Performance will degrade if using this to store
+ * many bitmaps with the same width but many different heights.
+ */
+public class SparseArrayBitmapPool {
+
+    private int mCapacityBytes;
+    private SparseArray<Node> mStore = new SparseArray<Node>();
+    private int mSizeBytes = 0;
+
+    private Pool<Node> mNodePool;
+    private Node mPoolNodesHead = null;
+    private Node mPoolNodesTail = null;
+
+    protected static class Node {
+        Bitmap bitmap;
+
+        // Each node is part of two doubly linked lists:
+        // - A pool-level list (accessed by mPoolNodesHead and mPoolNodesTail)
+        //   that is used for FIFO eviction of nodes when the pool gets full.
+        // - A bucket-level list for each index of the sparse array, so that
+        //   each index can store more than one item.
+        Node prevInBucket;
+        Node nextInBucket;
+        Node nextInPool;
+        Node prevInPool;
+    }
+
+    /**
+     * @param capacityBytes Maximum capacity of the pool in bytes.
+     * @param nodePool Shared pool to use for recycling linked list nodes, or null.
+     */
+    public SparseArrayBitmapPool(int capacityBytes, Pool<Node> nodePool) {
+        mCapacityBytes = capacityBytes;
+        if (nodePool == null) {
+            mNodePool = new SimplePool<Node>(32);
+        } else {
+            mNodePool = nodePool;
+        }
+    }
+
+    /**
+     * Set the maximum capacity of the pool, and if necessary trim it down to size.
+     */
+    public synchronized void setCapacity(int capacityBytes) {
+        mCapacityBytes = capacityBytes;
+
+        // No-op unless current size exceeds the new capacity.
+        freeUpCapacity(0);
+    }
+
+    private void freeUpCapacity(int bytesNeeded) {
+        int targetSize = mCapacityBytes - bytesNeeded;
+        // Repeatedly remove the oldest node until we have freed up at least bytesNeeded.
+        while (mPoolNodesTail != null && mSizeBytes > targetSize) {
+            unlinkAndRecycleNode(mPoolNodesTail, true);
+        }
+    }
+
+    private void unlinkAndRecycleNode(Node n, boolean recycleBitmap) {
+        // Unlink the node from its sparse array bucket list.
+        if (n.prevInBucket != null) {
+            // This wasn't the head, update the previous node.
+            n.prevInBucket.nextInBucket = n.nextInBucket;
+        } else {
+            // This was the head of the bucket, replace it with the next node.
+            mStore.put(n.bitmap.getWidth(), n.nextInBucket);
+        }
+        if (n.nextInBucket != null) {
+            // This wasn't the tail, update the next node.
+            n.nextInBucket.prevInBucket = n.prevInBucket;
+        }
+
+        // Unlink the node from the pool-wide list.
+        if (n.prevInPool != null) {
+            // This wasn't the head, update the previous node.
+            n.prevInPool.nextInPool = n.nextInPool;
+        } else {
+            // This was the head of the pool-wide list, update the head pointer.
+            mPoolNodesHead = n.nextInPool;
+        }
+        if (n.nextInPool != null) {
+            // This wasn't the tail, update the next node.
+            n.nextInPool.prevInPool = n.prevInPool;
+        } else {
+            // This was the tail, update the tail pointer.
+            mPoolNodesTail = n.prevInPool;
+        }
+
+        // Recycle the node.
+        n.nextInBucket = null;
+        n.nextInPool = null;
+        n.prevInBucket = null;
+        n.prevInPool = null;
+        mSizeBytes -= n.bitmap.getByteCount();
+        if (recycleBitmap) n.bitmap.recycle();
+        n.bitmap = null;
+        mNodePool.release(n);
+    }
+
+    /**
+     * @return Capacity of the pool in bytes.
+     */
+    public synchronized int getCapacity() {
+        return mCapacityBytes;
+    }
+
+    /**
+     * @return Total size in bytes of the bitmaps stored in the pool.
+     */
+    public synchronized int getSize() {
+        return mSizeBytes;
+    }
+
+    /**
+     * @return Bitmap from the pool with the desired height/width or null if none available.
+     */
+    public synchronized Bitmap get(int width, int height) {
+        Node cur = mStore.get(width);
+
+        // Traverse the list corresponding to the width bucket in the
+        // sparse array, and unlink and return the first bitmap that
+        // also has the correct height.
+        while (cur != null) {
+            if (cur.bitmap.getHeight() == height) {
+                Bitmap b = cur.bitmap;
+                unlinkAndRecycleNode(cur, false);
+                return b;
+            }
+            cur = cur.nextInBucket;
+        }
+        return null;
+    }
+
+    /**
+     * Adds the given bitmap to the pool.
+     * @return Whether the bitmap was added to the pool.
+     */
+    public synchronized boolean put(Bitmap b) {
+        if (b == null) {
+            return false;
+        }
+
+        // Ensure there is enough room to contain the new bitmap.
+        int bytes = b.getByteCount();
+        freeUpCapacity(bytes);
+
+        Node newNode = mNodePool.acquire();
+        if (newNode == null) {
+            newNode = new Node();
+        }
+        newNode.bitmap = b;
+
+        // We append to the head, and freeUpCapacity clears from the tail,
+        // resulting in FIFO eviction.
+        newNode.prevInBucket = null;
+        newNode.prevInPool = null;
+        newNode.nextInPool = mPoolNodesHead;
+        mPoolNodesHead = newNode;
+
+        // Insert the node into its appropriate bucket based on width.
+        int key = b.getWidth();
+        newNode.nextInBucket = mStore.get(key);
+        if (newNode.nextInBucket != null) {
+            // The bucket already had nodes, update the old head.
+            newNode.nextInBucket.prevInBucket = newNode;
+        }
+        mStore.put(key, newNode);
+
+        if (newNode.nextInPool == null) {
+            // This is the only node in the list, update the tail pointer.
+            mPoolNodesTail = newNode;
+        } else {
+            newNode.nextInPool.prevInPool = newNode;
+        }
+        mSizeBytes += bytes;
+        return true;
+    }
+
+    /**
+     * Empty the pool, recycling all the bitmaps currently in it.
+     */
+    public synchronized void clear() {
+        // Clearing is equivalent to ensuring all the capacity is available.
+        freeUpCapacity(mCapacityBytes);
+    }
+}
diff --git a/src/com/android/photos/drawables/AutoThumbnailDrawable.java b/src/com/android/photos/drawables/AutoThumbnailDrawable.java
new file mode 100644
index 0000000..b51b670
--- /dev/null
+++ b/src/com/android/photos/drawables/AutoThumbnailDrawable.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2013 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.photos.drawables;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.photos.data.GalleryBitmapPool;
+
+import java.io.InputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public abstract class AutoThumbnailDrawable<T> extends Drawable {
+
+    private static final String TAG = "AutoThumbnailDrawable";
+
+    private static ExecutorService sThreadPool = Executors.newSingleThreadExecutor();
+    private static GalleryBitmapPool sBitmapPool = GalleryBitmapPool.getInstance();
+    private static byte[] sTempStorage = new byte[64 * 1024];
+
+    // UI thread only
+    private Paint mPaint = new Paint();
+    private Matrix mDrawMatrix = new Matrix();
+
+    // Decoder thread only
+    private BitmapFactory.Options mOptions = new BitmapFactory.Options();
+
+    // Shared, guarded by mLock
+    private Object mLock = new Object();
+    private Bitmap mBitmap;
+    protected T mData;
+    private boolean mIsQueued;
+    private int mImageWidth, mImageHeight;
+    private Rect mBounds = new Rect();
+    private int mSampleSize = 1;
+
+    public AutoThumbnailDrawable() {
+        mPaint.setAntiAlias(true);
+        mPaint.setFilterBitmap(true);
+        mDrawMatrix.reset();
+        mOptions.inTempStorage = sTempStorage;
+    }
+
+    protected abstract byte[] getPreferredImageBytes(T data);
+    protected abstract InputStream getFallbackImageStream(T data);
+    protected abstract boolean dataChangedLocked(T data);
+
+    public void setImage(T data, int width, int height) {
+        if (!dataChangedLocked(data)) return;
+        synchronized (mLock) {
+            mImageWidth = width;
+            mImageHeight = height;
+            mData = data;
+            setBitmapLocked(null);
+            refreshSampleSizeLocked();
+        }
+        invalidateSelf();
+    }
+
+    private void setBitmapLocked(Bitmap b) {
+        if (b == mBitmap) {
+            return;
+        }
+        if (mBitmap != null) {
+            sBitmapPool.put(mBitmap);
+        }
+        mBitmap = b;
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        synchronized (mLock) {
+            mBounds.set(bounds);
+            if (mBounds.isEmpty()) {
+                mBitmap = null;
+            } else {
+                refreshSampleSizeLocked();
+                updateDrawMatrixLocked();
+            }
+        }
+        invalidateSelf();
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (mBitmap != null) {
+            canvas.save();
+            canvas.clipRect(mBounds);
+            canvas.concat(mDrawMatrix);
+            canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+            canvas.restore();
+        } else {
+            // TODO: Draw placeholder...?
+        }
+    }
+
+    private void updateDrawMatrixLocked() {
+        if (mBitmap == null || mBounds.isEmpty()) {
+            mDrawMatrix.reset();
+            return;
+        }
+
+        float scale;
+        float dx = 0, dy = 0;
+
+        int dwidth = mBitmap.getWidth();
+        int dheight = mBitmap.getHeight();
+        int vwidth = mBounds.width();
+        int vheight = mBounds.height();
+
+        // Calculates a matrix similar to ScaleType.CENTER_CROP
+        if (dwidth * vheight > vwidth * dheight) {
+            scale = (float) vheight / (float) dheight;
+            dx = (vwidth - dwidth * scale) * 0.5f;
+        } else {
+            scale = (float) vwidth / (float) dwidth;
+            dy = (vheight - dheight * scale) * 0.5f;
+        }
+        if (scale < .8f) {
+            Log.w(TAG, "sample size was too small! Overdrawing! " + scale + ", " + mSampleSize);
+        } else if (scale > 1.5f) {
+            Log.w(TAG, "Potential quality loss! " + scale + ", " + mSampleSize);
+        }
+
+        mDrawMatrix.setScale(scale, scale);
+        mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
+    }
+
+    private int calculateSampleSizeLocked(int dwidth, int dheight) {
+        float scale;
+
+        int vwidth = mBounds.width();
+        int vheight = mBounds.height();
+
+        // Inverse of updateDrawMatrixLocked
+        if (dwidth * vheight > vwidth * dheight) {
+            scale = (float) dheight / (float) vheight;
+        } else {
+            scale = (float) dwidth / (float) vwidth;
+        }
+        int result = Math.round(scale);
+        return result > 0 ? result : 1;
+    }
+
+    private void refreshSampleSizeLocked() {
+        if (mBounds.isEmpty() || mImageWidth == 0 || mImageHeight == 0) {
+            return;
+        }
+
+        int sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight);
+        if (sampleSize != mSampleSize || mBitmap == null) {
+            mSampleSize = sampleSize;
+            loadBitmapLocked();
+        }
+    }
+
+    private void loadBitmapLocked() {
+        if (!mIsQueued && !mBounds.isEmpty()) {
+            unscheduleSelf(mUpdateBitmap);
+            sThreadPool.execute(mLoadBitmap);
+            mIsQueued = true;
+        }
+    }
+
+    public float getAspectRatio() {
+        return (float) mImageWidth / (float) mImageHeight;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return -1;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return -1;
+    }
+
+    @Override
+    public int getOpacity() {
+        Bitmap bm = mBitmap;
+        return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ?
+                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        int oldAlpha = mPaint.getAlpha();
+        if (alpha != oldAlpha) {
+            mPaint.setAlpha(alpha);
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        mPaint.setColorFilter(cf);
+        invalidateSelf();
+    }
+
+    private final Runnable mLoadBitmap = new Runnable() {
+        @Override
+        public void run() {
+            T data;
+            synchronized (mLock) {
+                data = mData;
+            }
+            int preferredSampleSize = 1;
+            byte[] preferred = getPreferredImageBytes(data);
+            boolean hasPreferred = (preferred != null && preferred.length > 0);
+            if (hasPreferred) {
+                mOptions.inJustDecodeBounds = true;
+                BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions);
+                mOptions.inJustDecodeBounds = false;
+            }
+            int sampleSize, width, height;
+            synchronized (mLock) {
+                if (dataChangedLocked(data)) {
+                    return;
+                }
+                width = mImageWidth;
+                height = mImageHeight;
+                if (hasPreferred) {
+                    preferredSampleSize = calculateSampleSizeLocked(
+                            mOptions.outWidth, mOptions.outHeight);
+                }
+                sampleSize = calculateSampleSizeLocked(width, height);
+                mIsQueued = false;
+            }
+            Bitmap b = null;
+            InputStream is = null;
+            try {
+                if (hasPreferred) {
+                    mOptions.inSampleSize = preferredSampleSize;
+                    mOptions.inBitmap = sBitmapPool.get(
+                            mOptions.outWidth / preferredSampleSize,
+                            mOptions.outHeight / preferredSampleSize);
+                    b = BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions);
+                    if (mOptions.inBitmap != null && b != mOptions.inBitmap) {
+                        sBitmapPool.put(mOptions.inBitmap);
+                        mOptions.inBitmap = null;
+                    }
+                }
+                if (b == null) {
+                    is = getFallbackImageStream(data);
+                    mOptions.inSampleSize = sampleSize;
+                    mOptions.inBitmap = sBitmapPool.get(width / sampleSize, height / sampleSize);
+                    b = BitmapFactory.decodeStream(is, null, mOptions);
+                    if (mOptions.inBitmap != null && b != mOptions.inBitmap) {
+                        sBitmapPool.put(mOptions.inBitmap);
+                        mOptions.inBitmap = null;
+                    }
+                }
+            } catch (Exception e) {
+                Log.d(TAG, "Failed to fetch bitmap", e);
+                return;
+            } finally {
+                try {
+                    if (is != null) {
+                        is.close();
+                    }
+                } catch (Exception e) {}
+                if (b != null) {
+                    synchronized (mLock) {
+                        if (!dataChangedLocked(data)) {
+                            setBitmapLocked(b);
+                            scheduleSelf(mUpdateBitmap, 0);
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    private final Runnable mUpdateBitmap = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (AutoThumbnailDrawable.this) {
+                updateDrawMatrixLocked();
+                invalidateSelf();
+            }
+        }
+    };
+
+}
diff --git a/src/com/android/photos/drawables/DataUriThumbnailDrawable.java b/src/com/android/photos/drawables/DataUriThumbnailDrawable.java
new file mode 100644
index 0000000..c83b0c8
--- /dev/null
+++ b/src/com/android/photos/drawables/DataUriThumbnailDrawable.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 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.photos.drawables;
+
+import android.media.ExifInterface;
+import android.text.TextUtils;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class DataUriThumbnailDrawable extends AutoThumbnailDrawable<String> {
+
+    @Override
+    protected byte[] getPreferredImageBytes(String data) {
+        byte[] thumbnail = null;
+        try {
+            ExifInterface exif = new ExifInterface(data);
+            if (exif.hasThumbnail()) {
+                thumbnail = exif.getThumbnail();
+             }
+        } catch (IOException e) { }
+        return thumbnail;
+    }
+
+    @Override
+    protected InputStream getFallbackImageStream(String data) {
+        try {
+            return new FileInputStream(data);
+        } catch (FileNotFoundException e) {
+            return null;
+        }
+    }
+
+    @Override
+    protected boolean dataChangedLocked(String data) {
+        return !TextUtils.equals(mData, data);
+    }
+}
diff --git a/src/com/android/photos/shims/BitmapJobDrawable.java b/src/com/android/photos/shims/BitmapJobDrawable.java
new file mode 100644
index 0000000..32dbc80
--- /dev/null
+++ b/src/com/android/photos/shims/BitmapJobDrawable.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.BitmapLoader;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.photos.data.GalleryBitmapPool;
+
+
+public class BitmapJobDrawable extends Drawable implements Runnable {
+
+    private ThumbnailLoader mLoader;
+    private MediaItem mItem;
+    private Bitmap mBitmap;
+    private Paint mPaint = new Paint();
+    private Matrix mDrawMatrix = new Matrix();
+    private int mRotation = 0;
+
+    public BitmapJobDrawable() {
+    }
+
+    public void setMediaItem(MediaItem item) {
+        if (mItem == item) return;
+
+        if (mLoader != null) {
+            mLoader.cancelLoad();
+        }
+        mItem = item;
+        if (mBitmap != null) {
+            GalleryBitmapPool.getInstance().put(mBitmap);
+            mBitmap = null;
+        }
+        if (mItem != null) {
+            // TODO: Figure out why ThumbnailLoader doesn't like to be re-used
+            mLoader = new ThumbnailLoader(this);
+            mLoader.startLoad();
+            mRotation = mItem.getRotation();
+        }
+        invalidateSelf();
+    }
+
+    @Override
+    public void run() {
+        Bitmap bitmap = mLoader.getBitmap();
+        if (bitmap != null) {
+            mBitmap = bitmap;
+            updateDrawMatrix();
+        }
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        updateDrawMatrix();
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        Rect bounds = getBounds();
+        if (mBitmap != null) {
+            canvas.save();
+            canvas.clipRect(bounds);
+            canvas.concat(mDrawMatrix);
+            canvas.rotate(mRotation, bounds.centerX(), bounds.centerY());
+            canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+            canvas.restore();
+        } else {
+            mPaint.setColor(0xFFCCCCCC);
+            canvas.drawRect(bounds, mPaint);
+        }
+    }
+
+    private void updateDrawMatrix() {
+        Rect bounds = getBounds();
+        if (mBitmap == null || bounds.isEmpty()) {
+            mDrawMatrix.reset();
+            return;
+        }
+
+        float scale;
+        float dx = 0, dy = 0;
+
+        int dwidth = mBitmap.getWidth();
+        int dheight = mBitmap.getHeight();
+        int vwidth = bounds.width();
+        int vheight = bounds.height();
+
+        // Calculates a matrix similar to ScaleType.CENTER_CROP
+        if (dwidth * vheight > vwidth * dheight) {
+            scale = (float) vheight / (float) dheight;
+            dx = (vwidth - dwidth * scale) * 0.5f;
+        } else {
+            scale = (float) vwidth / (float) dwidth;
+            dy = (vheight - dheight * scale) * 0.5f;
+        }
+
+        mDrawMatrix.setScale(scale, scale);
+        mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
+        invalidateSelf();
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+    }
+
+    @Override
+    public int getOpacity() {
+        Bitmap bm = mBitmap;
+        return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ?
+                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        int oldAlpha = mPaint.getAlpha();
+        if (alpha != oldAlpha) {
+            mPaint.setAlpha(alpha);
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        mPaint.setColorFilter(cf);
+        invalidateSelf();
+    }
+
+    private static class ThumbnailLoader extends BitmapLoader {
+        private static final ThreadPool sThreadPool = new ThreadPool(0, 2);
+        private BitmapJobDrawable mParent;
+
+        public ThumbnailLoader(BitmapJobDrawable parent) {
+            mParent = parent;
+        }
+
+        @Override
+        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+            return sThreadPool.submit(
+                    mParent.mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this);
+        }
+
+        @Override
+        protected void onLoadComplete(Bitmap bitmap) {
+            mParent.scheduleSelf(mParent, 0);
+        }
+    }
+
+}
diff --git a/src/com/android/photos/shims/LoaderCompatShim.java b/src/com/android/photos/shims/LoaderCompatShim.java
new file mode 100644
index 0000000..d5bf710
--- /dev/null
+++ b/src/com/android/photos/shims/LoaderCompatShim.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+import java.util.ArrayList;
+
+
+public interface LoaderCompatShim<T> {
+    Drawable drawableForItem(T item, Drawable recycle);
+    Uri uriForItem(T item);
+    ArrayList<Uri> urisForSubItems(T item);
+    void deleteItemWithPath(Object path);
+    Object getPathForItem(T item);
+}
diff --git a/src/com/android/photos/shims/MediaItemsLoader.java b/src/com/android/photos/shims/MediaItemsLoader.java
new file mode 100644
index 0000000..6142355
--- /dev/null
+++ b/src/com/android/photos/shims/MediaItemsLoader.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.MediaStore.Files.FileColumns;
+import android.util.SparseArray;
+
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSet.SyncListener;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.Future;
+import com.android.photos.data.PhotoSetLoader;
+
+import java.util.ArrayList;
+
+/**
+ * Returns all MediaItems in a MediaSet, wrapping them in a cursor to appear
+ * like a PhotoSetLoader
+ */
+public class MediaItemsLoader extends AsyncTaskLoader<Cursor> implements LoaderCompatShim<Cursor> {
+
+    private static final SyncListener sNullListener = new SyncListener() {
+        @Override
+        public void onSyncDone(MediaSet mediaSet, int resultCode) {
+        }
+    };
+
+    private final MediaSet mMediaSet;
+    private final DataManager mDataManager;
+    private Future<Integer> mSyncTask = null;
+    private ContentListener mObserver = new ContentListener() {
+        @Override
+        public void onContentDirty() {
+            onContentChanged();
+        }
+    };
+    private SparseArray<MediaItem> mMediaItems;
+
+    public MediaItemsLoader(Context context) {
+        super(context);
+        mDataManager = DataManager.from(context);
+        String path = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL);
+        mMediaSet = mDataManager.getMediaSet(path);
+    }
+
+    public MediaItemsLoader(Context context, String parentPath) {
+        super(context);
+        mDataManager = DataManager.from(getContext());
+        mMediaSet = mDataManager.getMediaSet(parentPath);
+    }
+
+    @Override
+    protected void onStartLoading() {
+        super.onStartLoading();
+        mMediaSet.addContentListener(mObserver);
+        mSyncTask = mMediaSet.requestSync(sNullListener);
+        forceLoad();
+    }
+
+    @Override
+    protected boolean onCancelLoad() {
+        if (mSyncTask != null) {
+            mSyncTask.cancel();
+            mSyncTask = null;
+        }
+        return super.onCancelLoad();
+    }
+
+    @Override
+    protected void onStopLoading() {
+        super.onStopLoading();
+        cancelLoad();
+        mMediaSet.removeContentListener(mObserver);
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+        onStopLoading();
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        // TODO: This probably doesn't work
+        mMediaSet.reload();
+        final MatrixCursor cursor = new MatrixCursor(PhotoSetLoader.PROJECTION);
+        final Object[] row = new Object[PhotoSetLoader.PROJECTION.length];
+        final SparseArray<MediaItem> mediaItems = new SparseArray<MediaItem>();
+        mMediaSet.enumerateTotalMediaItems(new ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                row[PhotoSetLoader.INDEX_ID] = index;
+                row[PhotoSetLoader.INDEX_DATA] = item.getContentUri().toString();
+                row[PhotoSetLoader.INDEX_DATE_ADDED] = item.getDateInMs();
+                row[PhotoSetLoader.INDEX_HEIGHT] = item.getHeight();
+                row[PhotoSetLoader.INDEX_WIDTH] = item.getWidth();
+                row[PhotoSetLoader.INDEX_WIDTH] = item.getWidth();
+                int rawMediaType = item.getMediaType();
+                int mappedMediaType = FileColumns.MEDIA_TYPE_NONE;
+                if (rawMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
+                    mappedMediaType = FileColumns.MEDIA_TYPE_IMAGE;
+                } else if (rawMediaType == MediaItem.MEDIA_TYPE_VIDEO) {
+                    mappedMediaType = FileColumns.MEDIA_TYPE_VIDEO;
+                }
+                row[PhotoSetLoader.INDEX_MEDIA_TYPE] = mappedMediaType;
+                row[PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS] =
+                        item.getSupportedOperations();
+                cursor.addRow(row);
+                mediaItems.append(index, item);
+            }
+        });
+        synchronized (mMediaSet) {
+            mMediaItems = mediaItems;
+        }
+        return cursor;
+    }
+
+    @Override
+    public Drawable drawableForItem(Cursor item, Drawable recycle) {
+        BitmapJobDrawable drawable = null;
+        if (recycle == null || !(recycle instanceof BitmapJobDrawable)) {
+            drawable = new BitmapJobDrawable();
+        } else {
+            drawable = (BitmapJobDrawable) recycle;
+        }
+        int index = item.getInt(PhotoSetLoader.INDEX_ID);
+        drawable.setMediaItem(mMediaItems.get(index));
+        return drawable;
+    }
+
+    public static int getThumbnailSize() {
+        return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+    }
+
+    @Override
+    public Uri uriForItem(Cursor item) {
+        int index = item.getInt(PhotoSetLoader.INDEX_ID);
+        MediaItem mi = mMediaItems.get(index);
+        return mi == null ? null : mi.getContentUri();
+    }
+
+    @Override
+    public ArrayList<Uri> urisForSubItems(Cursor item) {
+        return null;
+    }
+
+    @Override
+    public void deleteItemWithPath(Object path) {
+        MediaObject o = mDataManager.getMediaObject((Path) path);
+        if (o != null) {
+            o.delete();
+        }
+    }
+
+    @Override
+    public Object getPathForItem(Cursor item) {
+        int index = item.getInt(PhotoSetLoader.INDEX_ID);
+        MediaItem mi = mMediaItems.get(index);
+        if (mi != null) {
+            return mi.getPath();
+        }
+        return null;
+    }
+
+}
diff --git a/src/com/android/photos/shims/MediaSetLoader.java b/src/com/android/photos/shims/MediaSetLoader.java
new file mode 100644
index 0000000..9093bc1
--- /dev/null
+++ b/src/com/android/photos/shims/MediaSetLoader.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.data.MediaSet.SyncListener;
+import com.android.gallery3d.util.Future;
+import com.android.photos.data.AlbumSetLoader;
+
+import java.util.ArrayList;
+
+/**
+ * Returns all MediaSets in a MediaSet, wrapping them in a cursor to appear
+ * like a AlbumSetLoader.
+ */
+public class MediaSetLoader extends AsyncTaskLoader<Cursor> implements LoaderCompatShim<Cursor>{
+
+    private static final SyncListener sNullListener = new SyncListener() {
+        @Override
+        public void onSyncDone(MediaSet mediaSet, int resultCode) {
+        }
+    };
+
+    private final MediaSet mMediaSet;
+    private final DataManager mDataManager;
+    private Future<Integer> mSyncTask = null;
+    private ContentListener mObserver = new ContentListener() {
+        @Override
+        public void onContentDirty() {
+            onContentChanged();
+        }
+    };
+
+    private ArrayList<MediaItem> mCoverItems;
+
+    public MediaSetLoader(Context context) {
+        super(context);
+        mDataManager = DataManager.from(context);
+        String path = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL);
+        mMediaSet = mDataManager.getMediaSet(path);
+    }
+
+    public MediaSetLoader(Context context, String path) {
+        super(context);
+        mDataManager = DataManager.from(getContext());
+        mMediaSet = mDataManager.getMediaSet(path);
+    }
+
+    @Override
+    protected void onStartLoading() {
+        super.onStartLoading();
+        mMediaSet.addContentListener(mObserver);
+        mSyncTask = mMediaSet.requestSync(sNullListener);
+        forceLoad();
+    }
+
+    @Override
+    protected boolean onCancelLoad() {
+        if (mSyncTask != null) {
+            mSyncTask.cancel();
+            mSyncTask = null;
+        }
+        return super.onCancelLoad();
+    }
+
+    @Override
+    protected void onStopLoading() {
+        super.onStopLoading();
+        cancelLoad();
+        mMediaSet.removeContentListener(mObserver);
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+        onStopLoading();
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        // TODO: This probably doesn't work
+        mMediaSet.reload();
+        final MatrixCursor cursor = new MatrixCursor(AlbumSetLoader.PROJECTION);
+        final Object[] row = new Object[AlbumSetLoader.PROJECTION.length];
+        int count = mMediaSet.getSubMediaSetCount();
+        ArrayList<MediaItem> coverItems = new ArrayList<MediaItem>(count);
+        for (int i = 0; i < count; i++) {
+            MediaSet m = mMediaSet.getSubMediaSet(i);
+            m.reload();
+            row[AlbumSetLoader.INDEX_ID] = i;
+            row[AlbumSetLoader.INDEX_TITLE] = m.getName();
+            row[AlbumSetLoader.INDEX_COUNT] = m.getMediaItemCount();
+            row[AlbumSetLoader.INDEX_SUPPORTED_OPERATIONS] = m.getSupportedOperations();
+            MediaItem coverItem = m.getCoverMediaItem();
+            if (coverItem != null) {
+                row[AlbumSetLoader.INDEX_TIMESTAMP] = coverItem.getDateInMs();
+            }
+            coverItems.add(coverItem);
+            cursor.addRow(row);
+        }
+        synchronized (mMediaSet) {
+            mCoverItems = coverItems;
+        }
+        return cursor;
+    }
+
+    @Override
+    public Drawable drawableForItem(Cursor item, Drawable recycle) {
+        BitmapJobDrawable drawable = null;
+        if (recycle == null || !(recycle instanceof BitmapJobDrawable)) {
+            drawable = new BitmapJobDrawable();
+        } else {
+            drawable = (BitmapJobDrawable) recycle;
+        }
+        int index = item.getInt(AlbumSetLoader.INDEX_ID);
+        drawable.setMediaItem(mCoverItems.get(index));
+        return drawable;
+    }
+
+    public static int getThumbnailSize() {
+        return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+    }
+
+    @Override
+    public Uri uriForItem(Cursor item) {
+        int index = item.getInt(AlbumSetLoader.INDEX_ID);
+        MediaSet ms = mMediaSet.getSubMediaSet(index);
+        return ms == null ? null : ms.getContentUri();
+    }
+
+    @Override
+    public ArrayList<Uri> urisForSubItems(Cursor item) {
+        int index = item.getInt(AlbumSetLoader.INDEX_ID);
+        MediaSet ms = mMediaSet.getSubMediaSet(index);
+        if (ms == null) return null;
+        final ArrayList<Uri> result = new ArrayList<Uri>();
+        ms.enumerateMediaItems(new MediaSet.ItemConsumer() {
+            @Override
+            public void consume(int index, MediaItem item) {
+                if (item != null) {
+                    result.add(item.getContentUri());
+                }
+            }
+        });
+        return result;
+    }
+
+    @Override
+    public void deleteItemWithPath(Object path) {
+        MediaObject o = mDataManager.getMediaObject((Path) path);
+        if (o != null) {
+            o.delete();
+        }
+    }
+
+    @Override
+    public Object getPathForItem(Cursor item) {
+        int index = item.getInt(AlbumSetLoader.INDEX_ID);
+        MediaSet ms = mMediaSet.getSubMediaSet(index);
+        if (ms != null) {
+            return ms.getPath();
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/photos/views/BlockingGLTextureView.java b/src/com/android/photos/views/BlockingGLTextureView.java
new file mode 100644
index 0000000..8a05051
--- /dev/null
+++ b/src/com/android/photos/views/BlockingGLTextureView.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.opengl.GLSurfaceView.Renderer;
+import android.opengl.GLUtils;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * A TextureView that supports blocking rendering for synchronous drawing
+ */
+public class BlockingGLTextureView extends TextureView
+        implements SurfaceTextureListener {
+
+    private RenderThread mRenderThread;
+
+    public BlockingGLTextureView(Context context) {
+        super(context);
+        setSurfaceTextureListener(this);
+    }
+
+    public void setRenderer(Renderer renderer) {
+        if (mRenderThread != null) {
+            throw new IllegalArgumentException("Renderer already set");
+        }
+        mRenderThread = new RenderThread(renderer);
+    }
+
+    public void render() {
+        mRenderThread.render();
+    }
+
+    public void destroy() {
+        if (mRenderThread != null) {
+            mRenderThread.finish();
+            mRenderThread = null;
+        }
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
+            int height) {
+        mRenderThread.setSurface(surface);
+        mRenderThread.setSize(width, height);
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
+            int height) {
+        mRenderThread.setSize(width, height);
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        if (mRenderThread != null) {
+            mRenderThread.setSurface(null);
+        }
+        return false;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            destroy();
+        } catch (Throwable t) {
+            // Ignore
+        }
+        super.finalize();
+    }
+
+    /**
+     * An EGL helper class.
+     */
+
+    private static class EglHelper {
+        private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+        private static final int EGL_OPENGL_ES2_BIT = 4;
+
+        EGL10 mEgl;
+        EGLDisplay mEglDisplay;
+        EGLSurface mEglSurface;
+        EGLConfig mEglConfig;
+        EGLContext mEglContext;
+
+        private EGLConfig chooseEglConfig() {
+            int[] configsCount = new int[1];
+            EGLConfig[] configs = new EGLConfig[1];
+            int[] configSpec = getConfig();
+            if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
+                throw new IllegalArgumentException("eglChooseConfig failed " +
+                        GLUtils.getEGLErrorString(mEgl.eglGetError()));
+            } else if (configsCount[0] > 0) {
+                return configs[0];
+            }
+            return null;
+        }
+
+        private static int[] getConfig() {
+            return new int[] {
+                    EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+                    EGL10.EGL_RED_SIZE, 8,
+                    EGL10.EGL_GREEN_SIZE, 8,
+                    EGL10.EGL_BLUE_SIZE, 8,
+                    EGL10.EGL_ALPHA_SIZE, 8,
+                    EGL10.EGL_DEPTH_SIZE, 0,
+                    EGL10.EGL_STENCIL_SIZE, 0,
+                    EGL10.EGL_NONE
+            };
+        }
+
+        EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
+            int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+            return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
+        }
+
+        /**
+         * Initialize EGL for a given configuration spec.
+         */
+        public void start() {
+            /*
+             * Get an EGL instance
+             */
+            mEgl = (EGL10) EGLContext.getEGL();
+
+            /*
+             * Get to the default display.
+             */
+            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+            if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+                throw new RuntimeException("eglGetDisplay failed");
+            }
+
+            /*
+             * We can now initialize EGL for that display
+             */
+            int[] version = new int[2];
+            if (!mEgl.eglInitialize(mEglDisplay, version)) {
+                throw new RuntimeException("eglInitialize failed");
+            }
+            mEglConfig = chooseEglConfig();
+
+            /*
+            * Create an EGL context. We want to do this as rarely as we can, because an
+            * EGL context is a somewhat heavy object.
+            */
+            mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
+
+            if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
+                mEglContext = null;
+                throwEglException("createContext");
+            }
+
+            mEglSurface = null;
+        }
+
+        /**
+         * Create an egl surface for the current SurfaceTexture surface. If a surface
+         * already exists, destroy it before creating the new surface.
+         *
+         * @return true if the surface was created successfully.
+         */
+        public boolean createSurface(SurfaceTexture surface) {
+            /*
+             * Check preconditions.
+             */
+            if (mEgl == null) {
+                throw new RuntimeException("egl not initialized");
+            }
+            if (mEglDisplay == null) {
+                throw new RuntimeException("eglDisplay not initialized");
+            }
+            if (mEglConfig == null) {
+                throw new RuntimeException("mEglConfig not initialized");
+            }
+
+            /*
+             *  The window size has changed, so we need to create a new
+             *  surface.
+             */
+            destroySurfaceImp();
+
+            /*
+             * Create an EGL surface we can render into.
+             */
+            if (surface != null) {
+                mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null);
+            } else {
+                mEglSurface = null;
+            }
+
+            if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+                int error = mEgl.eglGetError();
+                if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
+                    Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+                }
+                return false;
+            }
+
+            /*
+             * Before we can issue GL commands, we need to make sure
+             * the context is current and bound to a surface.
+             */
+            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+                /*
+                 * Could not make the context current, probably because the underlying
+                 * SurfaceView surface has been destroyed.
+                 */
+                logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
+                return false;
+            }
+
+            return true;
+        }
+
+        /**
+         * Create a GL object for the current EGL context.
+         */
+        public GL10 createGL() {
+            return (GL10) mEglContext.getGL();
+        }
+
+        /**
+         * Display the current render surface.
+         * @return the EGL error code from eglSwapBuffers.
+         */
+        public int swap() {
+            if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
+                return mEgl.eglGetError();
+            }
+            return EGL10.EGL_SUCCESS;
+        }
+
+        public void destroySurface() {
+            destroySurfaceImp();
+        }
+
+        private void destroySurfaceImp() {
+            if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
+                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+                        EGL10.EGL_NO_SURFACE,
+                        EGL10.EGL_NO_CONTEXT);
+                mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+                mEglSurface = null;
+            }
+        }
+
+        public void finish() {
+            if (mEglContext != null) {
+                mEgl.eglDestroyContext(mEglDisplay, mEglContext);
+                mEglContext = null;
+            }
+            if (mEglDisplay != null) {
+                mEgl.eglTerminate(mEglDisplay);
+                mEglDisplay = null;
+            }
+        }
+
+        private void throwEglException(String function) {
+            throwEglException(function, mEgl.eglGetError());
+        }
+
+        public static void throwEglException(String function, int error) {
+            String message = formatEglError(function, error);
+            throw new RuntimeException(message);
+        }
+
+        public static void logEglErrorAsWarning(String tag, String function, int error) {
+            Log.w(tag, formatEglError(function, error));
+        }
+
+        public static String formatEglError(String function, int error) {
+            return function + " failed: " + error;
+        }
+
+    }
+
+    private static class RenderThread extends Thread {
+        private static final int INVALID = -1;
+        private static final int RENDER = 1;
+        private static final int CHANGE_SURFACE = 2;
+        private static final int RESIZE_SURFACE = 3;
+        private static final int FINISH = 4;
+
+        private EglHelper mEglHelper = new EglHelper();
+
+        private Object mLock = new Object();
+        private int mExecMsgId = INVALID;
+        private SurfaceTexture mSurface;
+        private Renderer mRenderer;
+        private int mWidth, mHeight;
+
+        private boolean mFinished = false;
+        private GL10 mGL;
+
+        public RenderThread(Renderer renderer) {
+            super("RenderThread");
+            mRenderer = renderer;
+            start();
+        }
+
+        private void checkRenderer() {
+            if (mRenderer == null) {
+                throw new IllegalArgumentException("Renderer is null!");
+            }
+        }
+
+        private void checkSurface() {
+            if (mSurface == null) {
+                throw new IllegalArgumentException("surface is null!");
+            }
+        }
+
+        public void setSurface(SurfaceTexture surface) {
+            // If the surface is null we're being torn down, don't need a
+            // renderer then
+            if (surface != null) {
+                checkRenderer();
+            }
+            mSurface = surface;
+            exec(CHANGE_SURFACE);
+        }
+
+        public void setSize(int width, int height) {
+            checkRenderer();
+            checkSurface();
+            mWidth = width;
+            mHeight = height;
+            exec(RESIZE_SURFACE);
+        }
+
+        public void render() {
+            checkRenderer();
+            if (mSurface != null) {
+                exec(RENDER);
+                mSurface.updateTexImage();
+            }
+        }
+
+        public void finish() {
+            mSurface = null;
+            exec(FINISH);
+            try {
+                join();
+            } catch (InterruptedException e) {
+                // Ignore
+            }
+        }
+
+        private void exec(int msgid) {
+            synchronized (mLock) {
+                if (mExecMsgId != INVALID) {
+                    throw new IllegalArgumentException(
+                            "Message already set - multithreaded access?");
+                }
+                mExecMsgId = msgid;
+                mLock.notify();
+                try {
+                    mLock.wait();
+                } catch (InterruptedException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        private void handleMessageLocked(int what) {
+            switch (what) {
+            case CHANGE_SURFACE:
+                if (mEglHelper.createSurface(mSurface)) {
+                    mGL = mEglHelper.createGL();
+                    mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig);
+                }
+                break;
+            case RESIZE_SURFACE:
+                mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
+                break;
+            case RENDER:
+                mRenderer.onDrawFrame(mGL);
+                mEglHelper.swap();
+                break;
+            case FINISH:
+                mEglHelper.destroySurface();
+                mEglHelper.finish();
+                mFinished = true;
+                break;
+            }
+        }
+
+        @Override
+        public void run() {
+            synchronized (mLock) {
+                mEglHelper.start();
+                while (!mFinished) {
+                    while (mExecMsgId == INVALID) {
+                        try {
+                            mLock.wait();
+                        } catch (InterruptedException e) {
+                            // Ignore
+                        }
+                    }
+                    handleMessageLocked(mExecMsgId);
+                    mExecMsgId = INVALID;
+                    mLock.notify();
+                }
+                mExecMsgId = FINISH;
+            }
+        }
+    }
+}
diff --git a/src/com/android/photos/views/GalleryThumbnailView.java b/src/com/android/photos/views/GalleryThumbnailView.java
new file mode 100644
index 0000000..e5dd6f2
--- /dev/null
+++ b/src/com/android/photos/views/GalleryThumbnailView.java
@@ -0,0 +1,883 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.EdgeEffectCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.ListAdapter;
+import android.widget.OverScroller;
+
+import java.util.ArrayList;
+
+public class GalleryThumbnailView extends ViewGroup {
+
+    public interface GalleryThumbnailAdapter extends ListAdapter {
+        /**
+         * @param position Position to get the intrinsic aspect ratio for
+         * @return width / height
+         */
+        float getIntrinsicAspectRatio(int position);
+    }
+
+    private static final String TAG = "GalleryThumbnailView";
+    private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f);
+    private static final int LAND_UNITS = 2;
+    private static final int PORT_UNITS = 3;
+
+    private GalleryThumbnailAdapter mAdapter;
+
+    private final RecycleBin mRecycler = new RecycleBin();
+
+    private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
+
+    private boolean mDataChanged;
+    private int mOldItemCount;
+    private int mItemCount;
+    private boolean mHasStableIds;
+
+    private int mFirstPosition;
+
+    private boolean mPopulating;
+    private boolean mInLayout;
+
+    private int mTouchSlop;
+    private int mMaximumVelocity;
+    private int mFlingVelocity;
+    private float mLastTouchX;
+    private float mTouchRemainderX;
+    private int mActivePointerId;
+
+    private static final int TOUCH_MODE_IDLE = 0;
+    private static final int TOUCH_MODE_DRAGGING = 1;
+    private static final int TOUCH_MODE_FLINGING = 2;
+
+    private int mTouchMode;
+    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private final OverScroller mScroller;
+
+    private final EdgeEffectCompat mLeftEdge;
+    private final EdgeEffectCompat mRightEdge;
+
+    private int mLargeColumnWidth;
+    private int mSmallColumnWidth;
+    private int mLargeColumnUnitCount = 8;
+    private int mSmallColumnUnitCount = 10;
+
+    public GalleryThumbnailView(Context context) {
+        this(context, null);
+    }
+
+    public GalleryThumbnailView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        final ViewConfiguration vc = ViewConfiguration.get(context);
+        mTouchSlop = vc.getScaledTouchSlop();
+        mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
+        mFlingVelocity = vc.getScaledMinimumFlingVelocity();
+        mScroller = new OverScroller(context);
+
+        mLeftEdge = new EdgeEffectCompat(context);
+        mRightEdge = new EdgeEffectCompat(context);
+        setWillNotDraw(false);
+        setClipToPadding(false);
+    }
+
+    @Override
+    public void requestLayout() {
+        if (!mPopulating) {
+            super.requestLayout();
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        if (widthMode != MeasureSpec.EXACTLY) {
+            Log.e(TAG, "onMeasure: must have an exact width or match_parent! " +
+                    "Using fallback spec of EXACTLY " + widthSize);
+        }
+        if (heightMode != MeasureSpec.EXACTLY) {
+            Log.e(TAG, "onMeasure: must have an exact height or match_parent! " +
+                    "Using fallback spec of EXACTLY " + heightSize);
+        }
+
+        setMeasuredDimension(widthSize, heightSize);
+
+        float portSpaces = mLargeColumnUnitCount / PORT_UNITS;
+        float height = getMeasuredHeight() / portSpaces;
+        mLargeColumnWidth = (int) (height / ASPECT_RATIO);
+        portSpaces++;
+        height = getMeasuredHeight() / portSpaces;
+        mSmallColumnWidth = (int) (height / ASPECT_RATIO);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        mInLayout = true;
+        populate();
+        mInLayout = false;
+
+        final int width = r - l;
+        final int height = b - t;
+        mLeftEdge.setSize(width, height);
+        mRightEdge.setSize(width, height);
+    }
+
+    private void populate() {
+        if (getWidth() == 0 || getHeight() == 0) {
+            return;
+        }
+
+        // TODO: Handle size changing
+//        final int colCount = mColCount;
+//        if (mItemTops == null || mItemTops.length != colCount) {
+//            mItemTops = new int[colCount];
+//            mItemBottoms = new int[colCount];
+//            final int top = getPaddingTop();
+//            final int offset = top + Math.min(mRestoreOffset, 0);
+//            Arrays.fill(mItemTops, offset);
+//            Arrays.fill(mItemBottoms, offset);
+//            mLayoutRecords.clear();
+//            if (mInLayout) {
+//                removeAllViewsInLayout();
+//            } else {
+//                removeAllViews();
+//            }
+//            mRestoreOffset = 0;
+//        }
+
+        mPopulating = true;
+        layoutChildren(mDataChanged);
+        fillRight(mFirstPosition + getChildCount(), 0);
+        fillLeft(mFirstPosition - 1, 0);
+        mPopulating = false;
+        mDataChanged = false;
+    }
+
+    final void layoutChildren(boolean queryAdapter) {
+// TODO
+//        final int childCount = getChildCount();
+//        for (int i = 0; i < childCount; i++) {
+//            View child = getChildAt(i);
+//
+//            if (child.isLayoutRequested()) {
+//                final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY);
+//                final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY);
+//                child.measure(widthSpec, heightSpec);
+//                child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
+//            }
+//
+//            int childTop = mItemBottoms[col] > Integer.MIN_VALUE ?
+//                    mItemBottoms[col] + mItemMargin : child.getTop();
+//            if (span > 1) {
+//                int lowest = childTop;
+//                for (int j = col + 1; j < col + span; j++) {
+//                    final int bottom = mItemBottoms[j] + mItemMargin;
+//                    if (bottom > lowest) {
+//                        lowest = bottom;
+//                    }
+//                }
+//                childTop = lowest;
+//            }
+//            final int childHeight = child.getMeasuredHeight();
+//            final int childBottom = childTop + childHeight;
+//            final int childLeft = paddingLeft + col * (colWidth + itemMargin);
+//            final int childRight = childLeft + child.getMeasuredWidth();
+//            child.layout(childLeft, childTop, childRight, childBottom);
+//        }
+    }
+
+    /**
+     * Obtain the view and add it to our list of children. The view can be made
+     * fresh, converted from an unused view, or used as is if it was in the
+     * recycle bin.
+     *
+     * @param startPosition Logical position in the list to start from
+     * @param x Left or right edge of the view to add
+     * @param forward If true, align left edge to x and increase position.
+     *                If false, align right edge to x and decrease position.
+     * @return Number of views added
+     */
+    private int makeAndAddColumn(int startPosition, int x, boolean forward) {
+        int columnWidth = mLargeColumnWidth;
+        int addViews = 0;
+        for (int remaining = mLargeColumnUnitCount, i = 0;
+                remaining > 0 && startPosition + i >= 0 && startPosition + i < mItemCount;
+                i += forward ? 1 : -1, addViews++) {
+            if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) {
+                // landscape
+                remaining -= LAND_UNITS;
+            } else {
+                // portrait
+                remaining -= PORT_UNITS;
+                if (remaining < 0) {
+                    remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount);
+                    columnWidth = mSmallColumnWidth;
+                }
+            }
+        }
+        int nextTop = 0;
+        for (int i = 0; i < addViews; i++) {
+            int position = startPosition + (forward ? i : -i);
+            View child = obtainView(position, null);
+            if (child.getParent() != this) {
+                if (mInLayout) {
+                    addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams());
+                } else {
+                    addView(child, forward ? -1 : 0);
+                }
+            }
+            int heightSize = (int) (.5f + (mAdapter.getIntrinsicAspectRatio(position) >= 1f
+                    ? columnWidth / ASPECT_RATIO
+                    : columnWidth * ASPECT_RATIO));
+            int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+            int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
+            child.measure(widthSpec, heightSpec);
+            int childLeft = forward ? x : x - columnWidth;
+            child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize);
+            nextTop += heightSize;
+        }
+        return addViews;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        mVelocityTracker.addMovement(ev);
+        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mVelocityTracker.clear();
+                mScroller.abortAnimation();
+                mLastTouchX = ev.getX();
+                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+                mTouchRemainderX = 0;
+                if (mTouchMode == TOUCH_MODE_FLINGING) {
+                    // Catch!
+                    mTouchMode = TOUCH_MODE_DRAGGING;
+                    return true;
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE: {
+                final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+                if (index < 0) {
+                    Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+                            mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+                            "event stream?");
+                    return false;
+                }
+                final float x = MotionEventCompat.getX(ev, index);
+                final float dx = x - mLastTouchX + mTouchRemainderX;
+                final int deltaY = (int) dx;
+                mTouchRemainderX = dx - deltaY;
+
+                if (Math.abs(dx) > mTouchSlop) {
+                    mTouchMode = TOUCH_MODE_DRAGGING;
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        mVelocityTracker.addMovement(ev);
+        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mVelocityTracker.clear();
+                mScroller.abortAnimation();
+                mLastTouchX = ev.getX();
+                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+                mTouchRemainderX = 0;
+                break;
+
+            case MotionEvent.ACTION_MOVE: {
+                final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+                if (index < 0) {
+                    Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+                            mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+                            "event stream?");
+                    return false;
+                }
+                final float x = MotionEventCompat.getX(ev, index);
+                final float dx = x - mLastTouchX + mTouchRemainderX;
+                final int deltaX = (int) dx;
+                mTouchRemainderX = dx - deltaX;
+
+                if (Math.abs(dx) > mTouchSlop) {
+                    mTouchMode = TOUCH_MODE_DRAGGING;
+                }
+
+                if (mTouchMode == TOUCH_MODE_DRAGGING) {
+                    mLastTouchX = x;
+
+                    if (!trackMotionScroll(deltaX, true)) {
+                        // Break fling velocity if we impacted an edge.
+                        mVelocityTracker.clear();
+                    }
+                }
+            } break;
+
+            case MotionEvent.ACTION_CANCEL:
+                mTouchMode = TOUCH_MODE_IDLE;
+                break;
+
+            case MotionEvent.ACTION_UP: {
+                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                final float velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
+                        mActivePointerId);
+                if (Math.abs(velocity) > mFlingVelocity) { // TODO
+                    mTouchMode = TOUCH_MODE_FLINGING;
+                    mScroller.fling(0, 0, (int) velocity, 0,
+                            Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+                    mLastTouchX = 0;
+                    ViewCompat.postInvalidateOnAnimation(this);
+                } else {
+                    mTouchMode = TOUCH_MODE_IDLE;
+                }
+
+            } break;
+        }
+        return true;
+    }
+
+    /**
+     *
+     * @param deltaX Pixels that content should move by
+     * @return true if the movement completed, false if it was stopped prematurely.
+     */
+    private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) {
+        final boolean contentFits = contentFits();
+        final int allowOverhang = Math.abs(deltaX);
+
+        final int overScrolledBy;
+        final int movedBy;
+        if (!contentFits) {
+            final int overhang;
+            final boolean up;
+            mPopulating = true;
+            if (deltaX > 0) {
+                overhang = fillLeft(mFirstPosition - 1, allowOverhang);
+                up = true;
+            } else {
+                overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang);
+                up = false;
+            }
+            movedBy = Math.min(overhang, allowOverhang);
+            offsetChildren(up ? movedBy : -movedBy);
+            recycleOffscreenViews();
+            mPopulating = false;
+            overScrolledBy = allowOverhang - overhang;
+        } else {
+            overScrolledBy = allowOverhang;
+            movedBy = 0;
+        }
+
+        if (allowOverScroll) {
+            final int overScrollMode = ViewCompat.getOverScrollMode(this);
+
+            if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+                    (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
+
+                if (overScrolledBy > 0) {
+                    EdgeEffectCompat edge = deltaX > 0 ? mLeftEdge : mRightEdge;
+                    edge.onPull((float) Math.abs(deltaX) / getWidth());
+                    ViewCompat.postInvalidateOnAnimation(this);
+                }
+            }
+        }
+
+        return deltaX == 0 || movedBy != 0;
+    }
+
+    /**
+     * Important: this method will leave offscreen views attached if they
+     * are required to maintain the invariant that child view with index i
+     * is always the view corresponding to position mFirstPosition + i.
+     */
+    private void recycleOffscreenViews() {
+        final int height = getHeight();
+        final int clearAbove = 0;
+        final int clearBelow = height;
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            final View child = getChildAt(i);
+            if (child.getTop() <= clearBelow)  {
+                // There may be other offscreen views, but we need to maintain
+                // the invariant documented above.
+                break;
+            }
+
+            if (mInLayout) {
+                removeViewsInLayout(i, 1);
+            } else {
+                removeViewAt(i);
+            }
+
+            mRecycler.addScrap(child);
+        }
+
+        while (getChildCount() > 0) {
+            final View child = getChildAt(0);
+            if (child.getBottom() >= clearAbove) {
+                // There may be other offscreen views, but we need to maintain
+                // the invariant documented above.
+                break;
+            }
+
+            if (mInLayout) {
+                removeViewsInLayout(0, 1);
+            } else {
+                removeViewAt(0);
+            }
+
+            mRecycler.addScrap(child);
+            mFirstPosition++;
+        }
+    }
+
+    final void offsetChildren(int offset) {
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            child.layout(child.getLeft() + offset, child.getTop(),
+                    child.getRight() + offset, child.getBottom());
+        }
+    }
+
+    private boolean contentFits() {
+        final int childCount = getChildCount();
+        if (childCount == 0) return true;
+        if (childCount != mItemCount) return false;
+
+        return getChildAt(0).getLeft() >= getPaddingLeft() &&
+                getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight();
+    }
+
+    private void recycleAllViews() {
+        for (int i = 0; i < getChildCount(); i++) {
+            mRecycler.addScrap(getChildAt(i));
+        }
+
+        if (mInLayout) {
+            removeAllViewsInLayout();
+        } else {
+            removeAllViews();
+        }
+    }
+
+    private int fillRight(int pos, int overhang) {
+        int end = (getRight() - getLeft()) + overhang;
+
+        int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight();
+        while (nextLeft < end && pos < mItemCount) {
+            pos += makeAndAddColumn(pos, nextLeft, true);
+            nextLeft = getChildAt(getChildCount() - 1).getRight();
+        }
+        final int gridRight = getWidth() - getPaddingRight();
+        return getChildAt(getChildCount() - 1).getRight() - gridRight;
+    }
+
+    private int fillLeft(int pos, int overhang) {
+        int end = getPaddingLeft() - overhang;
+
+        int nextRight = getChildAt(0).getLeft();
+        while (nextRight > end && pos >= 0) {
+            pos -= makeAndAddColumn(pos, nextRight, false);
+            nextRight = getChildAt(0).getLeft();
+        }
+
+        mFirstPosition = pos + 1;
+        return getPaddingLeft() - getChildAt(0).getLeft();
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller.computeScrollOffset()) {
+            final int x = mScroller.getCurrX();
+            final int dx = (int) (x - mLastTouchX);
+            mLastTouchX = x;
+            final boolean stopped = !trackMotionScroll(dx, false);
+
+            if (!stopped && !mScroller.isFinished()) {
+                ViewCompat.postInvalidateOnAnimation(this);
+            } else {
+                if (stopped) {
+                    final int overScrollMode = ViewCompat.getOverScrollMode(this);
+                    if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
+                        final EdgeEffectCompat edge;
+                        if (dx > 0) {
+                            edge = mLeftEdge;
+                        } else {
+                            edge = mRightEdge;
+                        }
+                        edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
+                        ViewCompat.postInvalidateOnAnimation(this);
+                    }
+                    mScroller.abortAnimation();
+                }
+                mTouchMode = TOUCH_MODE_IDLE;
+            }
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+
+        if (!mLeftEdge.isFinished()) {
+            final int restoreCount = canvas.save();
+            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+            canvas.rotate(270);
+            canvas.translate(-height + getPaddingTop(), 0);
+            mLeftEdge.setSize(height, getWidth());
+            if (mLeftEdge.draw(canvas)) {
+                postInvalidateOnAnimation();
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+        if (!mRightEdge.isFinished()) {
+            final int restoreCount = canvas.save();
+            final int width = getWidth();
+            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+            canvas.rotate(90);
+            canvas.translate(-getPaddingTop(), width);
+            mRightEdge.setSize(height, width);
+            if (mRightEdge.draw(canvas)) {
+                postInvalidateOnAnimation();
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+    }
+
+    /**
+     * Obtain a populated view from the adapter. If optScrap is non-null and is not
+     * reused it will be placed in the recycle bin.
+     *
+     * @param position position to get view for
+     * @param optScrap Optional scrap view; will be reused if possible
+     * @return A new view, a recycled view from mRecycler, or optScrap
+     */
+    private final View obtainView(int position, View optScrap) {
+        View view = mRecycler.getTransientStateView(position);
+        if (view != null) {
+            return view;
+        }
+
+        // Reuse optScrap if it's of the right type (and not null)
+        final int optType = optScrap != null ?
+                ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
+        final int positionViewType = mAdapter.getItemViewType(position);
+        final View scrap = optType == positionViewType ?
+                optScrap : mRecycler.getScrapView(positionViewType);
+
+        view = mAdapter.getView(position, scrap, this);
+
+        if (view != scrap && scrap != null) {
+            // The adapter didn't use it; put it back.
+            mRecycler.addScrap(scrap);
+        }
+
+        ViewGroup.LayoutParams lp = view.getLayoutParams();
+
+        if (view.getParent() != this) {
+            if (lp == null) {
+                lp = generateDefaultLayoutParams();
+            } else if (!checkLayoutParams(lp)) {
+                lp = generateLayoutParams(lp);
+            }
+            view.setLayoutParams(lp);
+        }
+
+        final LayoutParams sglp = (LayoutParams) lp;
+        sglp.position = position;
+        sglp.viewType = positionViewType;
+
+        return view;
+    }
+
+    public GalleryThumbnailAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    public void setAdapter(GalleryThumbnailAdapter adapter) {
+        if (mAdapter != null) {
+            mAdapter.unregisterDataSetObserver(mObserver);
+        }
+        // TODO: If the new adapter says that there are stable IDs, remove certain layout records
+        // and onscreen views if they have changed instead of removing all of the state here.
+        clearAllState();
+        mAdapter = adapter;
+        mDataChanged = true;
+        mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
+        if (adapter != null) {
+            adapter.registerDataSetObserver(mObserver);
+            mRecycler.setViewTypeCount(adapter.getViewTypeCount());
+            mHasStableIds = adapter.hasStableIds();
+        } else {
+            mHasStableIds = false;
+        }
+        populate();
+    }
+
+    /**
+     * Clear all state because the grid will be used for a completely different set of data.
+     */
+    private void clearAllState() {
+        // Clear all layout records and views
+        removeAllViews();
+
+        // Reset to the top of the grid
+        mFirstPosition = 0;
+
+        // Clear recycler because there could be different view types now
+        mRecycler.clear();
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        return new LayoutParams(lp);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
+        return lp instanceof LayoutParams;
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        private static final int[] LAYOUT_ATTRS = new int[] {
+                android.R.attr.layout_span
+        };
+
+        private static final int SPAN_INDEX = 0;
+
+        /**
+         * The number of columns this item should span
+         */
+        public int span = 1;
+
+        /**
+         * Item position this view represents
+         */
+        int position;
+
+        /**
+         * Type of this view as reported by the adapter
+         */
+        int viewType;
+
+        /**
+         * The column this view is occupying
+         */
+        int column;
+
+        /**
+         * The stable ID of the item this view displays
+         */
+        long id = -1;
+
+        public LayoutParams(int height) {
+            super(MATCH_PARENT, height);
+
+            if (this.height == MATCH_PARENT) {
+                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
+                        "impossible! Falling back to WRAP_CONTENT");
+                this.height = WRAP_CONTENT;
+            }
+        }
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+
+            if (this.width != MATCH_PARENT) {
+                Log.w(TAG, "Inflation setting LayoutParams width to " + this.width +
+                        " - must be MATCH_PARENT");
+                this.width = MATCH_PARENT;
+            }
+            if (this.height == MATCH_PARENT) {
+                Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
+                        "impossible! Falling back to WRAP_CONTENT");
+                this.height = WRAP_CONTENT;
+            }
+
+            TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+            span = a.getInteger(SPAN_INDEX, 1);
+            a.recycle();
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams other) {
+            super(other);
+
+            if (this.width != MATCH_PARENT) {
+                Log.w(TAG, "Constructing LayoutParams with width " + this.width +
+                        " - must be MATCH_PARENT");
+                this.width = MATCH_PARENT;
+            }
+            if (this.height == MATCH_PARENT) {
+                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
+                        "impossible! Falling back to WRAP_CONTENT");
+                this.height = WRAP_CONTENT;
+            }
+        }
+    }
+
+    private class RecycleBin {
+        private ArrayList<View>[] mScrapViews;
+        private int mViewTypeCount;
+        private int mMaxScrap;
+
+        private SparseArray<View> mTransientStateViews;
+
+        public void setViewTypeCount(int viewTypeCount) {
+            if (viewTypeCount < 1) {
+                throw new IllegalArgumentException("Must have at least one view type (" +
+                        viewTypeCount + " types reported)");
+            }
+            if (viewTypeCount == mViewTypeCount) {
+                return;
+            }
+
+            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
+            for (int i = 0; i < viewTypeCount; i++) {
+                scrapViews[i] = new ArrayList<View>();
+            }
+            mViewTypeCount = viewTypeCount;
+            mScrapViews = scrapViews;
+        }
+
+        public void clear() {
+            final int typeCount = mViewTypeCount;
+            for (int i = 0; i < typeCount; i++) {
+                mScrapViews[i].clear();
+            }
+            if (mTransientStateViews != null) {
+                mTransientStateViews.clear();
+            }
+        }
+
+        public void clearTransientViews() {
+            if (mTransientStateViews != null) {
+                mTransientStateViews.clear();
+            }
+        }
+
+        public void addScrap(View v) {
+            final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+            if (ViewCompat.hasTransientState(v)) {
+                if (mTransientStateViews == null) {
+                    mTransientStateViews = new SparseArray<View>();
+                }
+                mTransientStateViews.put(lp.position, v);
+                return;
+            }
+
+            final int childCount = getChildCount();
+            if (childCount > mMaxScrap) {
+                mMaxScrap = childCount;
+            }
+
+            ArrayList<View> scrap = mScrapViews[lp.viewType];
+            if (scrap.size() < mMaxScrap) {
+                scrap.add(v);
+            }
+        }
+
+        public View getTransientStateView(int position) {
+            if (mTransientStateViews == null) {
+                return null;
+            }
+
+            final View result = mTransientStateViews.get(position);
+            if (result != null) {
+                mTransientStateViews.remove(position);
+            }
+            return result;
+        }
+
+        public View getScrapView(int type) {
+            ArrayList<View> scrap = mScrapViews[type];
+            if (scrap.isEmpty()) {
+                return null;
+            }
+
+            final int index = scrap.size() - 1;
+            final View result = scrap.get(index);
+            scrap.remove(index);
+            return result;
+        }
+    }
+
+    private class AdapterDataSetObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            mDataChanged = true;
+            mOldItemCount = mItemCount;
+            mItemCount = mAdapter.getCount();
+
+            // TODO: Consider matching these back up if we have stable IDs.
+            mRecycler.clearTransientViews();
+
+            if (!mHasStableIds) {
+                recycleAllViews();
+            }
+
+            // TODO: consider repopulating in a deferred runnable instead
+            // (so that successive changes may still be batched)
+            requestLayout();
+        }
+
+        @Override
+        public void onInvalidated() {
+        }
+    }
+}
diff --git a/src/com/android/photos/views/HeaderGridView.java b/src/com/android/photos/views/HeaderGridView.java
new file mode 100644
index 0000000..45a5eaf
--- /dev/null
+++ b/src/com/android/photos/views/HeaderGridView.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.WrapperListAdapter;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link GridView} that supports adding header rows in a
+ * very similar way to {@link ListView}.
+ * See {@link HeaderGridView#addHeaderView(View, Object, boolean)}
+ */
+public class HeaderGridView extends GridView {
+    private static final String TAG = "HeaderGridView";
+
+    /**
+     * A class that represents a fixed view in a list, for example a header at the top
+     * or a footer at the bottom.
+     */
+    private static class FixedViewInfo {
+        /** The view to add to the grid */
+        public View view;
+        public ViewGroup viewContainer;
+        /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
+        public Object data;
+        /** <code>true</code> if the fixed view should be selectable in the grid */
+        public boolean isSelectable;
+    }
+
+    private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>();
+
+    private void initHeaderGridView() {
+        super.setClipChildren(false);
+    }
+
+    public HeaderGridView(Context context) {
+        super(context);
+        initHeaderGridView();
+    }
+
+    public HeaderGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initHeaderGridView();
+    }
+
+    public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        initHeaderGridView();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        ListAdapter adapter = getAdapter();
+        if (adapter != null && adapter instanceof HeaderViewGridAdapter) {
+            ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumns());
+        }
+    }
+
+    @Override
+    public void setClipChildren(boolean clipChildren) {
+       // Ignore, since the header rows depend on not being clipped
+    }
+
+    /**
+     * Add a fixed view to appear at the top of the grid. If addHeaderView is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+     * the supplied cursor with one that will also account for header views.
+     *
+     * @param v The view to add.
+     * @param data Data to associate with this view
+     * @param isSelectable whether the item is selectable
+     */
+    public void addHeaderView(View v, Object data, boolean isSelectable) {
+        ListAdapter adapter = getAdapter();
+
+        if (adapter != null && ! (adapter instanceof HeaderViewGridAdapter)) {
+            throw new IllegalStateException(
+                    "Cannot add header view to grid -- setAdapter has already been called.");
+        }
+
+        FixedViewInfo info = new FixedViewInfo();
+        FrameLayout fl = new FullWidthFixedViewLayout(getContext());
+        fl.addView(v);
+        info.view = v;
+        info.viewContainer = fl;
+        info.data = data;
+        info.isSelectable = isSelectable;
+        mHeaderViewInfos.add(info);
+
+        // in the case of re-adding a header view, or adding one later on,
+        // we need to notify the observer
+        if (adapter != null) {
+            ((HeaderViewGridAdapter) adapter).notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * Add a fixed view to appear at the top of the grid. If addHeaderView is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+     * the supplied cursor with one that will also account for header views.
+     *
+     * @param v The view to add.
+     */
+    public void addHeaderView(View v) {
+        addHeaderView(v, null, true);
+    }
+
+    public int getHeaderViewCount() {
+        return mHeaderViewInfos.size();
+    }
+
+    /**
+     * Removes a previously-added header view.
+     *
+     * @param v The view to remove
+     * @return true if the view was removed, false if the view was not a header
+     *         view
+     */
+    public boolean removeHeaderView(View v) {
+        if (mHeaderViewInfos.size() > 0) {
+            boolean result = false;
+            ListAdapter adapter = getAdapter();
+            if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) {
+                result = true;
+            }
+            removeFixedViewInfo(v, mHeaderViewInfos);
+            return result;
+        }
+        return false;
+    }
+
+    private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
+        int len = where.size();
+        for (int i = 0; i < len; ++i) {
+            FixedViewInfo info = where.get(i);
+            if (info.view == v) {
+                where.remove(i);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        if (mHeaderViewInfos.size() > 0) {
+            HeaderViewGridAdapter hadapter = new HeaderViewGridAdapter(mHeaderViewInfos, adapter);
+            int numColumns = getNumColumns();
+            if (numColumns > 1) {
+                hadapter.setNumColumns(numColumns);
+            }
+            super.setAdapter(hadapter);
+        } else {
+            super.setAdapter(adapter);
+        }
+    }
+
+    private class FullWidthFixedViewLayout extends FrameLayout {
+        public FullWidthFixedViewLayout(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            int targetWidth = HeaderGridView.this.getMeasuredWidth()
+                    - HeaderGridView.this.getPaddingLeft()
+                    - HeaderGridView.this.getPaddingRight();
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,
+                    MeasureSpec.getMode(widthMeasureSpec));
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    /**
+     * ListAdapter used when a HeaderGridView has header views. This ListAdapter
+     * wraps another one and also keeps track of the header views and their
+     * associated data objects.
+     *<p>This is intended as a base class; you will probably not need to
+     * use this class directly in your own code.
+     */
+    private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable {
+
+        // This is used to notify the container of updates relating to number of columns
+        // or headers changing, which changes the number of placeholders needed
+        private final DataSetObservable mDataSetObservable = new DataSetObservable();
+
+        private final ListAdapter mAdapter;
+        private int mNumColumns = 1;
+
+        // This ArrayList is assumed to NOT be null.
+        ArrayList<FixedViewInfo> mHeaderViewInfos;
+
+        boolean mAreAllFixedViewsSelectable;
+
+        private final boolean mIsFilterable;
+
+        public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter) {
+            mAdapter = adapter;
+            mIsFilterable = adapter instanceof Filterable;
+
+            if (headerViewInfos == null) {
+                throw new IllegalArgumentException("headerViewInfos cannot be null");
+            }
+            mHeaderViewInfos = headerViewInfos;
+
+            mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+        }
+
+        public int getHeadersCount() {
+            return mHeaderViewInfos.size();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;
+        }
+
+        public void setNumColumns(int numColumns) {
+            if (numColumns < 1) {
+                throw new IllegalArgumentException("Number of columns must be 1 or more");
+            }
+            if (mNumColumns != numColumns) {
+                mNumColumns = numColumns;
+                notifyDataSetChanged();
+            }
+        }
+
+        private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) {
+            if (infos != null) {
+                for (FixedViewInfo info : infos) {
+                    if (!info.isSelectable) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        public boolean removeHeader(View v) {
+            for (int i = 0; i < mHeaderViewInfos.size(); i++) {
+                FixedViewInfo info = mHeaderViewInfos.get(i);
+                if (info.view == v) {
+                    mHeaderViewInfos.remove(i);
+
+                    mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+
+                    mDataSetObservable.notifyChanged();
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        public int getCount() {
+            if (mAdapter != null) {
+                return getHeadersCount() * mNumColumns + mAdapter.getCount();
+            } else {
+                return getHeadersCount() * mNumColumns;
+            }
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            if (mAdapter != null) {
+                return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
+            } else {
+                return true;
+            }
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (position < numHeadersAndPlaceholders) {
+                return (position % mNumColumns == 0)
+                        && mHeaderViewInfos.get(position / mNumColumns).isSelectable;
+            }
+
+            // Adapter
+            final int adjPosition = position - numHeadersAndPlaceholders;
+            int adapterCount = 0;
+            if (mAdapter != null) {
+                adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.isEnabled(adjPosition);
+                }
+            }
+
+            throw new ArrayIndexOutOfBoundsException(position);
+        }
+
+        @Override
+        public Object getItem(int position) {
+            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (position < numHeadersAndPlaceholders) {
+                if (position % mNumColumns == 0) {
+                    return mHeaderViewInfos.get(position / mNumColumns).data;
+                }
+                return null;
+            }
+
+            // Adapter
+            final int adjPosition = position - numHeadersAndPlaceholders;
+            int adapterCount = 0;
+            if (mAdapter != null) {
+                adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getItem(adjPosition);
+                }
+            }
+
+            throw new ArrayIndexOutOfBoundsException(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+                int adjPosition = position - numHeadersAndPlaceholders;
+                int adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getItemId(adjPosition);
+                }
+            }
+            return -1;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            if (mAdapter != null) {
+                return mAdapter.hasStableIds();
+            }
+            return false;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;
+            if (position < numHeadersAndPlaceholders) {
+                View headerViewContainer = mHeaderViewInfos
+                        .get(position / mNumColumns).viewContainer;
+                if (position % mNumColumns == 0) {
+                    return headerViewContainer;
+                } else {
+                    if (convertView == null) {
+                        convertView = new View(parent.getContext());
+                    }
+                    // We need to do this because GridView uses the height of the last item
+                    // in a row to determine the height for the entire row.
+                    convertView.setVisibility(View.INVISIBLE);
+                    convertView.setMinimumHeight(headerViewContainer.getHeight());
+                    return convertView;
+                }
+            }
+
+            // Adapter
+            final int adjPosition = position - numHeadersAndPlaceholders;
+            int adapterCount = 0;
+            if (mAdapter != null) {
+                adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getView(adjPosition, convertView, parent);
+                }
+            }
+
+            throw new ArrayIndexOutOfBoundsException(position);
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+            if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {
+                // Placeholders get the last view type number
+                return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
+            }
+            if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+                int adjPosition = position - numHeadersAndPlaceholders;
+                int adapterCount = mAdapter.getCount();
+                if (adjPosition < adapterCount) {
+                    return mAdapter.getItemViewType(adjPosition);
+                }
+            }
+
+            return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            if (mAdapter != null) {
+                return mAdapter.getViewTypeCount() + 1;
+            }
+            return 2;
+        }
+
+        @Override
+        public void registerDataSetObserver(DataSetObserver observer) {
+            mDataSetObservable.registerObserver(observer);
+            if (mAdapter != null) {
+                mAdapter.registerDataSetObserver(observer);
+            }
+        }
+
+        @Override
+        public void unregisterDataSetObserver(DataSetObserver observer) {
+            mDataSetObservable.unregisterObserver(observer);
+            if (mAdapter != null) {
+                mAdapter.unregisterDataSetObserver(observer);
+            }
+        }
+
+        @Override
+        public Filter getFilter() {
+            if (mIsFilterable) {
+                return ((Filterable) mAdapter).getFilter();
+            }
+            return null;
+        }
+
+        @Override
+        public ListAdapter getWrappedAdapter() {
+            return mAdapter;
+        }
+
+        public void notifyDataSetChanged() {
+            mDataSetObservable.notifyChanged();
+        }
+    }
+}
diff --git a/src/com/android/photos/views/SquareImageView.java b/src/com/android/photos/views/SquareImageView.java
new file mode 100644
index 0000000..14eff10
--- /dev/null
+++ b/src/com/android/photos/views/SquareImageView.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+
+public class SquareImageView extends ImageView {
+
+    public SquareImageView(Context context) {
+        super(context);
+    }
+
+    public SquareImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public SquareImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
+            int width = MeasureSpec.getSize(widthMeasureSpec);
+            int height = width;
+            if (heightMode == MeasureSpec.AT_MOST) {
+                height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
+            }
+            setMeasuredDimension(width, height);
+        } else {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+}
diff --git a/src/com/android/photos/views/TiledImageRenderer.java b/src/com/android/photos/views/TiledImageRenderer.java
new file mode 100644
index 0000000..c4e493b
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageRenderer.java
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.v4.util.LongSparseArray;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+
+/**
+ * Handles laying out, decoding, and drawing of tiles in GL
+ */
+public class TiledImageRenderer {
+    public static final int SIZE_UNKNOWN = -1;
+
+    private static final String TAG = "TiledImageRenderer";
+    private static final int UPLOAD_LIMIT = 1;
+
+    /*
+     *  This is the tile state in the CPU side.
+     *  Life of a Tile:
+     *      ACTIVATED (initial state)
+     *              --> IN_QUEUE - by queueForDecode()
+     *              --> RECYCLED - by recycleTile()
+     *      IN_QUEUE --> DECODING - by decodeTile()
+     *               --> RECYCLED - by recycleTile)
+     *      DECODING --> RECYCLING - by recycleTile()
+     *               --> DECODED  - by decodeTile()
+     *               --> DECODE_FAIL - by decodeTile()
+     *      RECYCLING --> RECYCLED - by decodeTile()
+     *      DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
+     *      DECODED --> RECYCLED - by recycleTile()
+     *      DECODE_FAIL -> RECYCLED - by recycleTile()
+     *      RECYCLED --> ACTIVATED - by obtainTile()
+     */
+    private static final int STATE_ACTIVATED = 0x01;
+    private static final int STATE_IN_QUEUE = 0x02;
+    private static final int STATE_DECODING = 0x04;
+    private static final int STATE_DECODED = 0x08;
+    private static final int STATE_DECODE_FAIL = 0x10;
+    private static final int STATE_RECYCLING = 0x20;
+    private static final int STATE_RECYCLED = 0x40;
+
+    private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
+
+    // TILE_SIZE must be 2^N
+    private int mTileSize;
+
+    private TileSource mModel;
+    private BasicTexture mPreview;
+    protected int mLevelCount;  // cache the value of mScaledBitmaps.length
+
+    // The mLevel variable indicates which level of bitmap we should use.
+    // Level 0 means the original full-sized bitmap, and a larger value means
+    // a smaller scaled bitmap (The width and height of each scaled bitmap is
+    // half size of the previous one). If the value is in [0, mLevelCount), we
+    // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
+    // is mLevelCount
+    private int mLevel = 0;
+
+    private int mOffsetX;
+    private int mOffsetY;
+
+    private int mUploadQuota;
+    private boolean mRenderComplete;
+
+    private final RectF mSourceRect = new RectF();
+    private final RectF mTargetRect = new RectF();
+
+    private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
+
+    // The following three queue are guarded by mQueueLock
+    private final Object mQueueLock = new Object();
+    private final TileQueue mRecycledQueue = new TileQueue();
+    private final TileQueue mUploadQueue = new TileQueue();
+    private final TileQueue mDecodeQueue = new TileQueue();
+
+    // The width and height of the full-sized bitmap
+    protected int mImageWidth = SIZE_UNKNOWN;
+    protected int mImageHeight = SIZE_UNKNOWN;
+
+    protected int mCenterX;
+    protected int mCenterY;
+    protected float mScale;
+    protected int mRotation;
+
+    private boolean mLayoutTiles;
+
+    // Temp variables to avoid memory allocation
+    private final Rect mTileRange = new Rect();
+    private final Rect mActiveRange[] = {new Rect(), new Rect()};
+
+    private TileDecoder mTileDecoder;
+    private boolean mBackgroundTileUploaded;
+
+    private int mViewWidth, mViewHeight;
+    private View mParent;
+
+    /**
+     * Interface for providing tiles to a {@link TiledImageRenderer}
+     */
+    public static interface TileSource {
+
+        /**
+         * If the source does not care about the tile size, it should use
+         * {@link TiledImageRenderer#suggestedTileSize(Context)}
+         */
+        public int getTileSize();
+        public int getImageWidth();
+        public int getImageHeight();
+        public int getRotation();
+
+        /**
+         * Return a Preview image if available. This will be used as the base layer
+         * if higher res tiles are not yet available
+         */
+        public BasicTexture getPreview();
+
+        /**
+         * The tile returned by this method can be specified this way: Assuming
+         * the image size is (width, height), first take the intersection of (0,
+         * 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
+         * in extending the region, we found some part of the region is outside
+         * the image, those pixels are filled with black.
+         *
+         * If level > 0, it does the same operation on a down-scaled version of
+         * the original image (down-scaled by a factor of 2^level), but (x, y)
+         * still refers to the coordinate on the original image.
+         *
+         * The method would be called by the decoder thread.
+         */
+        public Bitmap getTile(int level, int x, int y, Bitmap reuse);
+    }
+
+    public static int suggestedTileSize(Context context) {
+        return isHighResolution(context) ? 512 : 256;
+    }
+
+    private static boolean isHighResolution(Context context) {
+        DisplayMetrics metrics = new DisplayMetrics();
+        WindowManager wm = (WindowManager)
+                context.getSystemService(Context.WINDOW_SERVICE);
+        wm.getDefaultDisplay().getMetrics(metrics);
+        return metrics.heightPixels > 2048 ||  metrics.widthPixels > 2048;
+    }
+
+    public TiledImageRenderer(View parent) {
+        mParent = parent;
+        mTileDecoder = new TileDecoder();
+        mTileDecoder.start();
+    }
+
+    public int getViewWidth() {
+        return mViewWidth;
+    }
+
+    public int getViewHeight() {
+        return mViewHeight;
+    }
+
+    private void invalidate() {
+        mParent.postInvalidate();
+    }
+
+    public void setModel(TileSource model, int rotation) {
+        if (mModel != model) {
+            mModel = model;
+            notifyModelInvalidated();
+        }
+        if (mRotation != rotation) {
+            mRotation = rotation;
+            mLayoutTiles = true;
+        }
+    }
+
+    private void calculateLevelCount() {
+        if (mPreview != null) {
+            mLevelCount = Math.max(0, Utils.ceilLog2(
+                mImageWidth / (float) mPreview.getWidth()));
+        } else {
+            int levels = 1;
+            int maxDim = Math.max(mImageWidth, mImageHeight);
+            int t = mTileSize;
+            while (t < maxDim) {
+                t <<= 1;
+                levels++;
+            }
+            mLevelCount = levels;
+        }
+    }
+
+    public void notifyModelInvalidated() {
+        invalidateTiles();
+        if (mModel == null) {
+            mImageWidth = 0;
+            mImageHeight = 0;
+            mLevelCount = 0;
+            mPreview = null;
+        } else {
+            mImageWidth = mModel.getImageWidth();
+            mImageHeight = mModel.getImageHeight();
+            mPreview = mModel.getPreview();
+            mTileSize = mModel.getTileSize();
+            calculateLevelCount();
+        }
+        mLayoutTiles = true;
+    }
+
+    public void setViewSize(int width, int height) {
+        mViewWidth = width;
+        mViewHeight = height;
+    }
+
+    public void setPosition(int centerX, int centerY, float scale) {
+        if (mCenterX == centerX && mCenterY == centerY
+                && mScale == scale) {
+            return;
+        }
+        mCenterX = centerX;
+        mCenterY = centerY;
+        mScale = scale;
+        mLayoutTiles = true;
+    }
+
+    // Prepare the tiles we want to use for display.
+    //
+    // 1. Decide the tile level we want to use for display.
+    // 2. Decide the tile levels we want to keep as texture (in addition to
+    //    the one we use for display).
+    // 3. Recycle unused tiles.
+    // 4. Activate the tiles we want.
+    private void layoutTiles() {
+        if (mViewWidth == 0 || mViewHeight == 0 || !mLayoutTiles) {
+            return;
+        }
+        mLayoutTiles = false;
+
+        // The tile levels we want to keep as texture is in the range
+        // [fromLevel, endLevel).
+        int fromLevel;
+        int endLevel;
+
+        // We want to use a texture larger than or equal to the display size.
+        mLevel = Utils.clamp(Utils.floorLog2(1f / mScale), 0, mLevelCount);
+
+        // We want to keep one more tile level as texture in addition to what
+        // we use for display. So it can be faster when the scale moves to the
+        // next level. We choose the level closest to the current scale.
+        if (mLevel != mLevelCount) {
+            Rect range = mTileRange;
+            getRange(range, mCenterX, mCenterY, mLevel, mScale, mRotation);
+            mOffsetX = Math.round(mViewWidth / 2f + (range.left - mCenterX) * mScale);
+            mOffsetY = Math.round(mViewHeight / 2f + (range.top - mCenterY) * mScale);
+            fromLevel = mScale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
+        } else {
+            // Activate the tiles of the smallest two levels.
+            fromLevel = mLevel - 2;
+            mOffsetX = Math.round(mViewWidth / 2f - mCenterX * mScale);
+            mOffsetY = Math.round(mViewHeight / 2f - mCenterY * mScale);
+        }
+
+        fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
+        endLevel = Math.min(fromLevel + 2, mLevelCount);
+
+        Rect range[] = mActiveRange;
+        for (int i = fromLevel; i < endLevel; ++i) {
+            getRange(range[i - fromLevel], mCenterX, mCenterY, i, mRotation);
+        }
+
+        // If rotation is transient, don't update the tile.
+        if (mRotation % 90 != 0) {
+            return;
+        }
+
+        synchronized (mQueueLock) {
+            mDecodeQueue.clean();
+            mUploadQueue.clean();
+            mBackgroundTileUploaded = false;
+
+            // Recycle unused tiles: if the level of the active tile is outside the
+            // range [fromLevel, endLevel) or not in the visible range.
+            int n = mActiveTiles.size();
+            for (int i = 0; i < n; i++) {
+                Tile tile = mActiveTiles.valueAt(i);
+                int level = tile.mTileLevel;
+                if (level < fromLevel || level >= endLevel
+                        || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
+                    mActiveTiles.removeAt(i);
+                    i--;
+                    n--;
+                    recycleTile(tile);
+                }
+            }
+        }
+
+        for (int i = fromLevel; i < endLevel; ++i) {
+            int size = mTileSize << i;
+            Rect r = range[i - fromLevel];
+            for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
+                for (int x = r.left, right = r.right; x < right; x += size) {
+                    activateTile(x, y, i);
+                }
+            }
+        }
+        invalidate();
+    }
+
+    private void invalidateTiles() {
+        synchronized (mQueueLock) {
+            mDecodeQueue.clean();
+            mUploadQueue.clean();
+
+            // TODO(xx): disable decoder
+            int n = mActiveTiles.size();
+            for (int i = 0; i < n; i++) {
+                Tile tile = mActiveTiles.valueAt(i);
+                recycleTile(tile);
+            }
+            mActiveTiles.clear();
+        }
+    }
+
+    private void getRange(Rect out, int cX, int cY, int level, int rotation) {
+        getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
+    }
+
+    // If the bitmap is scaled by the given factor "scale", return the
+    // rectangle containing visible range. The left-top coordinate returned is
+    // aligned to the tile boundary.
+    //
+    // (cX, cY) is the point on the original bitmap which will be put in the
+    // center of the ImageViewer.
+    private void getRange(Rect out,
+            int cX, int cY, int level, float scale, int rotation) {
+
+        double radians = Math.toRadians(-rotation);
+        double w = mViewWidth;
+        double h = mViewHeight;
+
+        double cos = Math.cos(radians);
+        double sin = Math.sin(radians);
+        int width = (int) Math.ceil(Math.max(
+                Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
+        int height = (int) Math.ceil(Math.max(
+                Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
+
+        int left = (int) Math.floor(cX - width / (2f * scale));
+        int top = (int) Math.floor(cY - height / (2f * scale));
+        int right = (int) Math.ceil(left + width / scale);
+        int bottom = (int) Math.ceil(top + height / scale);
+
+        // align the rectangle to tile boundary
+        int size = mTileSize << level;
+        left = Math.max(0, size * (left / size));
+        top = Math.max(0, size * (top / size));
+        right = Math.min(mImageWidth, right);
+        bottom = Math.min(mImageHeight, bottom);
+
+        out.set(left, top, right, bottom);
+    }
+
+    public void freeTextures() {
+        mLayoutTiles = true;
+
+        mTileDecoder.finishAndWait();
+        synchronized (mQueueLock) {
+            mUploadQueue.clean();
+            mDecodeQueue.clean();
+            Tile tile = mRecycledQueue.pop();
+            while (tile != null) {
+                tile.recycle();
+                tile = mRecycledQueue.pop();
+            }
+        }
+
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile texture = mActiveTiles.valueAt(i);
+            texture.recycle();
+        }
+        mActiveTiles.clear();
+        mTileRange.set(0, 0, 0, 0);
+
+        while (sTilePool.acquire() != null) {}
+    }
+
+    public boolean draw(GLCanvas canvas) {
+        layoutTiles();
+        uploadTiles(canvas);
+
+        mUploadQuota = UPLOAD_LIMIT;
+        mRenderComplete = true;
+
+        int level = mLevel;
+        int rotation = mRotation;
+        int flags = 0;
+        if (rotation != 0) {
+            flags |= GLCanvas.SAVE_FLAG_MATRIX;
+        }
+
+        if (flags != 0) {
+            canvas.save(flags);
+            if (rotation != 0) {
+                int centerX = mViewWidth / 2, centerY = mViewHeight / 2;
+                canvas.translate(centerX, centerY);
+                canvas.rotate(rotation, 0, 0, 1);
+                canvas.translate(-centerX, -centerY);
+            }
+        }
+        try {
+            if (level != mLevelCount) {
+                int size = (mTileSize << level);
+                float length = size * mScale;
+                Rect r = mTileRange;
+
+                for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
+                    float y = mOffsetY + i * length;
+                    for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
+                        float x = mOffsetX + j * length;
+                        drawTile(canvas, tx, ty, level, x, y, length);
+                    }
+                }
+            } else if (mPreview != null) {
+                mPreview.draw(canvas, mOffsetX, mOffsetY,
+                        Math.round(mImageWidth * mScale),
+                        Math.round(mImageHeight * mScale));
+            }
+        } finally {
+            if (flags != 0) {
+                canvas.restore();
+            }
+        }
+
+        if (mRenderComplete) {
+            if (!mBackgroundTileUploaded) {
+                uploadBackgroundTiles(canvas);
+            }
+        } else {
+            invalidate();
+        }
+        return mRenderComplete || mPreview != null;
+    }
+
+    private void uploadBackgroundTiles(GLCanvas canvas) {
+        mBackgroundTileUploaded = true;
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile tile = mActiveTiles.valueAt(i);
+            if (!tile.isContentValid()) {
+                queueForDecode(tile);
+            }
+        }
+    }
+
+   private void queueForDecode(Tile tile) {
+       synchronized (mQueueLock) {
+           if (tile.mTileState == STATE_ACTIVATED) {
+               tile.mTileState = STATE_IN_QUEUE;
+               if (mDecodeQueue.push(tile)) {
+                   mQueueLock.notifyAll();
+               }
+           }
+       }
+    }
+
+    private void decodeTile(Tile tile) {
+        synchronized (mQueueLock) {
+            if (tile.mTileState != STATE_IN_QUEUE) {
+                return;
+            }
+            tile.mTileState = STATE_DECODING;
+        }
+        boolean decodeComplete = tile.decode();
+        synchronized (mQueueLock) {
+            if (tile.mTileState == STATE_RECYCLING) {
+                tile.mTileState = STATE_RECYCLED;
+                if (tile.mDecodedTile != null) {
+                    sTilePool.release(tile.mDecodedTile);
+                    tile.mDecodedTile = null;
+                }
+                mRecycledQueue.push(tile);
+                return;
+            }
+            tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
+            if (!decodeComplete) {
+                return;
+            }
+            mUploadQueue.push(tile);
+        }
+        invalidate();
+    }
+
+    private Tile obtainTile(int x, int y, int level) {
+        synchronized (mQueueLock) {
+            Tile tile = mRecycledQueue.pop();
+            if (tile != null) {
+                tile.mTileState = STATE_ACTIVATED;
+                tile.update(x, y, level);
+                return tile;
+            }
+            return new Tile(x, y, level);
+        }
+    }
+
+    private void recycleTile(Tile tile) {
+        synchronized (mQueueLock) {
+            if (tile.mTileState == STATE_DECODING) {
+                tile.mTileState = STATE_RECYCLING;
+                return;
+            }
+            tile.mTileState = STATE_RECYCLED;
+            if (tile.mDecodedTile != null) {
+                sTilePool.release(tile.mDecodedTile);
+                tile.mDecodedTile = null;
+            }
+            mRecycledQueue.push(tile);
+        }
+    }
+
+    private void activateTile(int x, int y, int level) {
+        long key = makeTileKey(x, y, level);
+        Tile tile = mActiveTiles.get(key);
+        if (tile != null) {
+            if (tile.mTileState == STATE_IN_QUEUE) {
+                tile.mTileState = STATE_ACTIVATED;
+            }
+            return;
+        }
+        tile = obtainTile(x, y, level);
+        mActiveTiles.put(key, tile);
+    }
+
+    private Tile getTile(int x, int y, int level) {
+        return mActiveTiles.get(makeTileKey(x, y, level));
+    }
+
+    private static long makeTileKey(int x, int y, int level) {
+        long result = x;
+        result = (result << 16) | y;
+        result = (result << 16) | level;
+        return result;
+    }
+
+    private void uploadTiles(GLCanvas canvas) {
+        int quota = UPLOAD_LIMIT;
+        Tile tile = null;
+        while (quota > 0) {
+            synchronized (mQueueLock) {
+                tile = mUploadQueue.pop();
+            }
+            if (tile == null) {
+                break;
+            }
+            if (!tile.isContentValid()) {
+                if (tile.mTileState == STATE_DECODED) {
+                    tile.updateContent(canvas);
+                    --quota;
+                } else {
+                    Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState);
+                }
+            }
+        }
+        if (tile != null) {
+            invalidate();
+        }
+    }
+
+    // Draw the tile to a square at canvas that locates at (x, y) and
+    // has a side length of length.
+    private void drawTile(GLCanvas canvas,
+            int tx, int ty, int level, float x, float y, float length) {
+        RectF source = mSourceRect;
+        RectF target = mTargetRect;
+        target.set(x, y, x + length, y + length);
+        source.set(0, 0, mTileSize, mTileSize);
+
+        Tile tile = getTile(tx, ty, level);
+        if (tile != null) {
+            if (!tile.isContentValid()) {
+                if (tile.mTileState == STATE_DECODED) {
+                    if (mUploadQuota > 0) {
+                        --mUploadQuota;
+                        tile.updateContent(canvas);
+                    } else {
+                        mRenderComplete = false;
+                    }
+                } else if (tile.mTileState != STATE_DECODE_FAIL){
+                    mRenderComplete = false;
+                    queueForDecode(tile);
+                }
+            }
+            if (drawTile(tile, canvas, source, target)) {
+                return;
+            }
+        }
+        if (mPreview != null) {
+            int size = mTileSize << level;
+            float scaleX = (float) mPreview.getWidth() / mImageWidth;
+            float scaleY = (float) mPreview.getHeight() / mImageHeight;
+            source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+                    (ty + size) * scaleY);
+            canvas.drawTexture(mPreview, source, target);
+        }
+    }
+
+    private boolean drawTile(
+            Tile tile, GLCanvas canvas, RectF source, RectF target) {
+        while (true) {
+            if (tile.isContentValid()) {
+                canvas.drawTexture(tile, source, target);
+                return true;
+            }
+
+            // Parent can be divided to four quads and tile is one of the four.
+            Tile parent = tile.getParentTile();
+            if (parent == null) {
+                return false;
+            }
+            if (tile.mX == parent.mX) {
+                source.left /= 2f;
+                source.right /= 2f;
+            } else {
+                source.left = (mTileSize + source.left) / 2f;
+                source.right = (mTileSize + source.right) / 2f;
+            }
+            if (tile.mY == parent.mY) {
+                source.top /= 2f;
+                source.bottom /= 2f;
+            } else {
+                source.top = (mTileSize + source.top) / 2f;
+                source.bottom = (mTileSize + source.bottom) / 2f;
+            }
+            tile = parent;
+        }
+    }
+
+    private class Tile extends UploadedTexture {
+        public int mX;
+        public int mY;
+        public int mTileLevel;
+        public Tile mNext;
+        public Bitmap mDecodedTile;
+        public volatile int mTileState = STATE_ACTIVATED;
+
+        public Tile(int x, int y, int level) {
+            mX = x;
+            mY = y;
+            mTileLevel = level;
+        }
+
+        @Override
+        protected void onFreeBitmap(Bitmap bitmap) {
+            sTilePool.release(bitmap);
+        }
+
+        boolean decode() {
+            // Get a tile from the original image. The tile is down-scaled
+            // by (1 << mTilelevel) from a region in the original image.
+            try {
+                Bitmap reuse = sTilePool.acquire();
+                if (reuse != null && reuse.getWidth() != mTileSize) {
+                    reuse = null;
+                }
+                mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse);
+            } catch (Throwable t) {
+                Log.w(TAG, "fail to decode tile", t);
+            }
+            return mDecodedTile != null;
+        }
+
+        @Override
+        protected Bitmap onGetBitmap() {
+            Utils.assertTrue(mTileState == STATE_DECODED);
+
+            // We need to override the width and height, so that we won't
+            // draw beyond the boundaries.
+            int rightEdge = ((mImageWidth - mX) >> mTileLevel);
+            int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
+            setSize(Math.min(mTileSize, rightEdge), Math.min(mTileSize, bottomEdge));
+
+            Bitmap bitmap = mDecodedTile;
+            mDecodedTile = null;
+            mTileState = STATE_ACTIVATED;
+            return bitmap;
+        }
+
+        // We override getTextureWidth() and getTextureHeight() here, so the
+        // texture can be re-used for different tiles regardless of the actual
+        // size of the tile (which may be small because it is a tile at the
+        // boundary).
+        @Override
+        public int getTextureWidth() {
+            return mTileSize;
+        }
+
+        @Override
+        public int getTextureHeight() {
+            return mTileSize;
+        }
+
+        public void update(int x, int y, int level) {
+            mX = x;
+            mY = y;
+            mTileLevel = level;
+            invalidateContent();
+        }
+
+        public Tile getParentTile() {
+            if (mTileLevel + 1 == mLevelCount) {
+                return null;
+            }
+            int size = mTileSize << (mTileLevel + 1);
+            int x = size * (mX / size);
+            int y = size * (mY / size);
+            return getTile(x, y, mTileLevel + 1);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("tile(%s, %s, %s / %s)",
+                    mX / mTileSize, mY / mTileSize, mLevel, mLevelCount);
+        }
+    }
+
+    private static class TileQueue {
+        private Tile mHead;
+
+        public Tile pop() {
+            Tile tile = mHead;
+            if (tile != null) {
+                mHead = tile.mNext;
+            }
+            return tile;
+        }
+
+        public boolean push(Tile tile) {
+            if (contains(tile)) {
+                Log.w(TAG, "Attempting to add a tile already in the queue!");
+                return false;
+            }
+            boolean wasEmpty = mHead == null;
+            tile.mNext = mHead;
+            mHead = tile;
+            return wasEmpty;
+        }
+
+        private boolean contains(Tile tile) {
+            Tile other = mHead;
+            while (other != null) {
+                if (other == tile) {
+                    return true;
+                }
+                other = other.mNext;
+            }
+            return false;
+        }
+
+        public void clean() {
+            mHead = null;
+        }
+    }
+
+    private class TileDecoder extends Thread {
+
+        public void finishAndWait() {
+            interrupt();
+            try {
+                join();
+            } catch (InterruptedException e) {
+                Log.w(TAG, "Interrupted while waiting for TileDecoder thread to finish!");
+            }
+        }
+
+        private Tile waitForTile() throws InterruptedException {
+            synchronized (mQueueLock) {
+                while (true) {
+                    Tile tile = mDecodeQueue.pop();
+                    if (tile != null) {
+                        return tile;
+                    }
+                    mQueueLock.wait();
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            try {
+                while (!isInterrupted()) {
+                    Tile tile = waitForTile();
+                    decodeTile(tile);
+                }
+            } catch (InterruptedException ex) {
+                // We were finished
+            }
+        }
+
+    }
+}
diff --git a/src/com/android/photos/views/TiledImageView.java b/src/com/android/photos/views/TiledImageView.java
new file mode 100644
index 0000000..8bc07c0
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageView.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.RectF;
+import android.opengl.GLSurfaceView;
+import android.opengl.GLSurfaceView.Renderer;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLES20Canvas;
+import com.android.photos.views.TiledImageRenderer.TileSource;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
+ * or {@link BlockingGLTextureView}.
+ */
+public class TiledImageView extends FrameLayout {
+
+    private static final boolean USE_TEXTURE_VIEW = false;
+    private static final boolean IS_SUPPORTED =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+    private static final boolean USE_CHOREOGRAPHER =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+
+    private BlockingGLTextureView mTextureView;
+    private GLSurfaceView mGLSurfaceView;
+    private boolean mInvalPending = false;
+    private FrameCallback mFrameCallback;
+
+    private static class ImageRendererWrapper {
+        // Guarded by locks
+        float scale;
+        int centerX, centerY;
+        int rotation;
+        TileSource source;
+        Runnable isReadyCallback;
+
+        // GL thread only
+        TiledImageRenderer image;
+    }
+
+    private float[] mValues = new float[9];
+
+    // -------------------------
+    // Guarded by mLock
+    // -------------------------
+    private Object mLock = new Object();
+    private ImageRendererWrapper mRenderer;
+
+    public TiledImageView(Context context) {
+        this(context, null);
+    }
+
+    public TiledImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        if (!IS_SUPPORTED) {
+            return;
+        }
+
+        mRenderer = new ImageRendererWrapper();
+        mRenderer.image = new TiledImageRenderer(this);
+        View view;
+        if (USE_TEXTURE_VIEW) {
+            mTextureView = new BlockingGLTextureView(context);
+            mTextureView.setRenderer(new TileRenderer());
+            view = mTextureView;
+        } else {
+            mGLSurfaceView = new GLSurfaceView(context);
+            mGLSurfaceView.setEGLContextClientVersion(2);
+            mGLSurfaceView.setRenderer(new TileRenderer());
+            mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+            view = mGLSurfaceView;
+        }
+        addView(view, new LayoutParams(
+                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+        //setTileSource(new ColoredTiles());
+    }
+
+    public void destroy() {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (USE_TEXTURE_VIEW) {
+            mTextureView.destroy();
+        } else {
+            mGLSurfaceView.queueEvent(mFreeTextures);
+        }
+    }
+
+    private Runnable mFreeTextures = new Runnable() {
+
+        @Override
+        public void run() {
+            mRenderer.image.freeTextures();
+        }
+    };
+
+    public void onPause() {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (!USE_TEXTURE_VIEW) {
+            mGLSurfaceView.onPause();
+        }
+    }
+
+    public void onResume() {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (!USE_TEXTURE_VIEW) {
+            mGLSurfaceView.onResume();
+        }
+    }
+
+    public void setTileSource(TileSource source, Runnable isReadyCallback) {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        synchronized (mLock) {
+            mRenderer.source = source;
+            mRenderer.isReadyCallback = isReadyCallback;
+            mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0;
+            mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0;
+            mRenderer.rotation = source != null ? source.getRotation() : 0;
+            mRenderer.scale = 0;
+            updateScaleIfNecessaryLocked(mRenderer);
+        }
+        invalidate();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right,
+            int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        synchronized (mLock) {
+            updateScaleIfNecessaryLocked(mRenderer);
+        }
+    }
+
+    private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
+        if (renderer == null || renderer.source == null
+                || renderer.scale > 0 || getWidth() == 0) {
+            return;
+        }
+        renderer.scale = Math.min(
+                (float) getWidth() / (float) renderer.source.getImageWidth(),
+                (float) getHeight() / (float) renderer.source.getImageHeight());
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (USE_TEXTURE_VIEW) {
+            mTextureView.render();
+        }
+        super.dispatchDraw(canvas);
+    }
+
+    @SuppressLint("NewApi")
+    @Override
+    public void setTranslationX(float translationX) {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        super.setTranslationX(translationX);
+    }
+
+    @Override
+    public void invalidate() {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (USE_TEXTURE_VIEW) {
+            super.invalidate();
+            mTextureView.invalidate();
+        } else {
+            if (USE_CHOREOGRAPHER) {
+                invalOnVsync();
+            } else {
+                mGLSurfaceView.requestRender();
+            }
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private void invalOnVsync() {
+        if (!mInvalPending) {
+            mInvalPending = true;
+            if (mFrameCallback == null) {
+                mFrameCallback = new FrameCallback() {
+                    @Override
+                    public void doFrame(long frameTimeNanos) {
+                        mInvalPending = false;
+                        mGLSurfaceView.requestRender();
+                    }
+                };
+            }
+            Choreographer.getInstance().postFrameCallback(mFrameCallback);
+        }
+    }
+
+    private RectF mTempRectF = new RectF();
+    public void positionFromMatrix(Matrix matrix) {
+        if (!IS_SUPPORTED) {
+            return;
+        }
+        if (mRenderer.source != null) {
+            final int rotation = mRenderer.source.getRotation();
+            final boolean swap = !(rotation % 180 == 0);
+            final int width = swap ? mRenderer.source.getImageHeight()
+                    : mRenderer.source.getImageWidth();
+            final int height = swap ? mRenderer.source.getImageWidth()
+                    : mRenderer.source.getImageHeight();
+            mTempRectF.set(0, 0, width, height);
+            matrix.mapRect(mTempRectF);
+            matrix.getValues(mValues);
+            int cx = width / 2;
+            int cy = height / 2;
+            float scale = mValues[Matrix.MSCALE_X];
+            int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale);
+            int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale);
+            if (rotation == 90 || rotation == 180) {
+                cx += (mTempRectF.left / scale) - xoffset;
+            } else {
+                cx -= (mTempRectF.left / scale) - xoffset;
+            }
+            if (rotation == 180 || rotation == 270) {
+                cy += (mTempRectF.top / scale) - yoffset;
+            } else {
+                cy -= (mTempRectF.top / scale) - yoffset;
+            }
+            mRenderer.scale = scale;
+            mRenderer.centerX = swap ? cy : cx;
+            mRenderer.centerY = swap ? cx : cy;
+            invalidate();
+        }
+    }
+
+    private class TileRenderer implements Renderer {
+
+        private GLES20Canvas mCanvas;
+
+        @Override
+        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+            mCanvas = new GLES20Canvas();
+            BasicTexture.invalidateAllTextures();
+            mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
+        }
+
+        @Override
+        public void onSurfaceChanged(GL10 gl, int width, int height) {
+            mCanvas.setSize(width, height);
+            mRenderer.image.setViewSize(width, height);
+        }
+
+        @Override
+        public void onDrawFrame(GL10 gl) {
+            mCanvas.clearBuffer();
+            Runnable readyCallback;
+            synchronized (mLock) {
+                readyCallback = mRenderer.isReadyCallback;
+                mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
+                mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY,
+                        mRenderer.scale);
+            }
+            boolean complete = mRenderer.image.draw(mCanvas);
+            if (complete && readyCallback != null) {
+                synchronized (mLock) {
+                    // Make sure we don't trample on a newly set callback/source
+                    // if it changed while we were rendering
+                    if (mRenderer.isReadyCallback == readyCallback) {
+                        mRenderer.isReadyCallback = null;
+                    }
+                }
+                if (readyCallback != null) {
+                    post(readyCallback);
+                }
+            }
+        }
+
+    }
+
+    @SuppressWarnings("unused")
+    private static class ColoredTiles implements TileSource {
+        private static final int[] COLORS = new int[] {
+            Color.RED,
+            Color.BLUE,
+            Color.YELLOW,
+            Color.GREEN,
+            Color.CYAN,
+            Color.MAGENTA,
+            Color.WHITE,
+        };
+
+        private Paint mPaint = new Paint();
+        private Canvas mCanvas = new Canvas();
+
+        @Override
+        public int getTileSize() {
+            return 256;
+        }
+
+        @Override
+        public int getImageWidth() {
+            return 16384;
+        }
+
+        @Override
+        public int getImageHeight() {
+            return 8192;
+        }
+
+        @Override
+        public int getRotation() {
+            return 0;
+        }
+
+        @Override
+        public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
+            int tileSize = getTileSize();
+            if (bitmap == null) {
+                bitmap = Bitmap.createBitmap(tileSize, tileSize,
+                        Bitmap.Config.ARGB_8888);
+            }
+            mCanvas.setBitmap(bitmap);
+            mCanvas.drawColor(COLORS[level]);
+            mPaint.setColor(Color.BLACK);
+            mPaint.setTextSize(20);
+            mPaint.setTextAlign(Align.CENTER);
+            mCanvas.drawText(x + "x" + y, 128, 128, mPaint);
+            tileSize <<= level;
+            x /= tileSize;
+            y /= tileSize;
+            mCanvas.drawText(x + "x" + y + " @ " + level, 128, 30, mPaint);
+            mCanvas.setBitmap(null);
+            return bitmap;
+        }
+
+        @Override
+        public BasicTexture getPreview() {
+            return null;
+        }
+    }
+}
diff --git a/src_pd/com/android/camera/PanoramaStitchingManager.java b/src_pd/com/android/camera/PanoramaStitchingManager.java
new file mode 100644
index 0000000..5ba16b8
--- /dev/null
+++ b/src_pd/com/android/camera/PanoramaStitchingManager.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 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.net.Uri;
+
+class PanoramaStitchingManager implements ImageTaskManager {
+
+    public PanoramaStitchingManager(Context ctx) {
+    }
+
+    @Override
+    public void addTaskListener(TaskListener l) {
+        // do nothing.
+    }
+
+    @Override
+    public void removeTaskListener(TaskListener l) {
+        // do nothing.
+    }
+
+    @Override
+    public int getTaskProgress(Uri uri) {
+        return -1;
+    }
+}
diff --git a/src_pd/com/android/gallery3d/app/StitchingProgressManager.java b/src_pd/com/android/gallery3d/app/StitchingProgressManager.java
new file mode 100644
index 0000000..7386847
--- /dev/null
+++ b/src_pd/com/android/gallery3d/app/StitchingProgressManager.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.app;
+
+import android.net.Uri;
+
+public class StitchingProgressManager {
+    public StitchingProgressManager(GalleryApp app) {
+    }
+
+    public void addChangeListener(StitchingChangeListener l) {
+    }
+
+    public void removeChangeListener(StitchingChangeListener l) {
+    }
+
+    public Integer getProgress(Uri uri) {
+        return null;
+    }
+}
diff --git a/src_pd/com/android/gallery3d/filtershow/editors/EditorManager.java b/src_pd/com/android/gallery3d/filtershow/editors/EditorManager.java
new file mode 100644
index 0000000..3266425
--- /dev/null
+++ b/src_pd/com/android/gallery3d/filtershow/editors/EditorManager.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.editors;
+
+import com.android.gallery3d.filtershow.EditorPlaceHolder;
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.editors.EditorCurves;
+import com.android.gallery3d.filtershow.editors.EditorZoom;
+
+public class EditorManager {
+
+    public static void addEditors(EditorPlaceHolder editorPlaceHolder) {
+        editorPlaceHolder.addEditor(new EditorGrad());
+        editorPlaceHolder.addEditor(new EditorChanSat());
+        editorPlaceHolder.addEditor(new EditorZoom());
+        editorPlaceHolder.addEditor(new EditorCurves());
+        editorPlaceHolder.addEditor(new EditorTinyPlanet());
+        editorPlaceHolder.addEditor(new EditorDraw());
+        editorPlaceHolder.addEditor(new EditorVignette());
+        editorPlaceHolder.addEditor(new EditorMirror());
+        editorPlaceHolder.addEditor(new EditorRotate());
+        editorPlaceHolder.addEditor(new EditorStraighten());
+        editorPlaceHolder.addEditor(new EditorCrop());
+    }
+
+}
diff --git a/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java b/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java
new file mode 100644
index 0000000..66372c2
--- /dev/null
+++ b/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.filtershow.filters;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Vector;
+
+public class FiltersManager extends BaseFiltersManager {
+    private static FiltersManager sInstance = null;
+    private static FiltersManager sPreviewInstance = null;
+    private static FiltersManager sHighresInstance = null;
+    private static int mImageBorderSize = 4; // in percent
+    public FiltersManager() {
+        init();
+    }
+
+    public static FiltersManager getPreviewManager() {
+        if (sPreviewInstance == null) {
+            sPreviewInstance = new FiltersManager();
+        }
+        return sPreviewInstance;
+    }
+
+    public static FiltersManager getManager() {
+        if (sInstance == null) {
+            sInstance = new FiltersManager();
+        }
+        return sInstance;
+    }
+
+    @Override
+    public void addBorders(Context context) {
+
+        // Do not localize
+        String[] serializationNames = {
+                "FRAME_4X5",
+                "FRAME_BRUSH",
+                "FRAME_GRUNGE",
+                "FRAME_SUMI_E",
+                "FRAME_TAPE",
+                "FRAME_BLACK",
+                "FRAME_BLACK_ROUNDED",
+                "FRAME_WHITE",
+                "FRAME_WHITE_ROUNDED",
+                "FRAME_CREAM",
+                "FRAME_CREAM_ROUNDED"
+        };
+
+        // The "no border" implementation
+        int i = 0;
+        FilterRepresentation rep = new FilterImageBorderRepresentation(0);
+        mBorders.add(rep);
+
+        // Regular borders
+        ArrayList <FilterRepresentation> borderList = new ArrayList<FilterRepresentation>();
+
+
+        rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_4x5);
+        borderList.add(rep);
+
+        rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_brush);
+        borderList.add(rep);
+
+        rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_grunge);
+        borderList.add(rep);
+
+        rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_sumi_e);
+        borderList.add(rep);
+
+        rep = new FilterImageBorderRepresentation(R.drawable.filtershow_border_tape);
+        borderList.add(rep);
+
+        rep = new FilterColorBorderRepresentation(Color.BLACK, mImageBorderSize, 0);
+        borderList.add(rep);
+
+        rep = new FilterColorBorderRepresentation(Color.BLACK, mImageBorderSize,
+                mImageBorderSize);
+        borderList.add(rep);
+
+        rep = new FilterColorBorderRepresentation(Color.WHITE, mImageBorderSize, 0);
+        borderList.add(rep);
+
+        rep = new FilterColorBorderRepresentation(Color.WHITE, mImageBorderSize,
+                mImageBorderSize);
+        borderList.add(rep);
+
+        int creamColor = Color.argb(255, 237, 237, 227);
+        rep = new FilterColorBorderRepresentation(creamColor, mImageBorderSize, 0);
+        borderList.add(rep);
+
+        rep = new FilterColorBorderRepresentation(creamColor, mImageBorderSize,
+                mImageBorderSize);
+        borderList.add(rep);
+
+        for (FilterRepresentation filter : borderList) {
+            filter.setSerializationName(serializationNames[i++]);
+            addRepresentation(filter);
+        }
+
+    }
+
+    public static FiltersManager getHighresManager() {
+        if (sHighresInstance == null) {
+            sHighresInstance = new FiltersManager();
+        }
+        return sHighresInstance;
+    }
+
+    public static void reset() {
+        sInstance = null;
+        sPreviewInstance = null;
+        sHighresInstance = null;
+    }
+
+    public static void setResources(Resources resources) {
+        FiltersManager.getManager().setFilterResources(resources);
+        FiltersManager.getPreviewManager().setFilterResources(resources);
+        FiltersManager.getHighresManager().setFilterResources(resources);
+    }
+}
diff --git a/src_pd/com/android/gallery3d/picasasource/PicasaSource.java b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
new file mode 100644
index 0000000..5e800e2
--- /dev/null
+++ b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.picasasource;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MediaSource;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.data.PathMatcher;
+
+import java.io.FileNotFoundException;
+
+public class PicasaSource extends MediaSource {
+    private static final String TAG = "PicasaSource";
+
+    private static final int NO_MATCH = -1;
+    private static final int IMAGE_MEDIA_ID = 1;
+
+    private static final int PICASA_ALBUMSET = 0;
+    private static final int MAP_BATCH_COUNT = 100;
+
+    private GalleryApp mApplication;
+    private PathMatcher mMatcher;
+
+    public static final Path ALBUM_PATH = Path.fromString("/picasa/all");
+
+    public PicasaSource(GalleryApp application) {
+        super("picasa");
+        mApplication = application;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/picasa/all", PICASA_ALBUMSET);
+        mMatcher.add("/picasa/image", PICASA_ALBUMSET);
+        mMatcher.add("/picasa/video", PICASA_ALBUMSET);
+    }
+
+    private static class EmptyAlbumSet extends MediaSet {
+
+        public EmptyAlbumSet(Path path, long version) {
+            super(path, version);
+        }
+
+        @Override
+        public String getName() {
+            return "picasa";
+        }
+
+        @Override
+        public long reload() {
+            return mDataVersion;
+        }
+    }
+
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        switch (mMatcher.match(path)) {
+            case PICASA_ALBUMSET:
+                return new EmptyAlbumSet(path, MediaObject.nextVersionNumber());
+            default:
+                throw new RuntimeException("bad path: " + path);
+        }
+    }
+
+    public static MediaItem getFaceItem(Context context, MediaItem item, int faceIndex) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static boolean isPicasaImage(MediaObject object) {
+        return false;
+    }
+
+    public static String getImageTitle(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static int getImageSize(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static String getContentType(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static long getDateTaken(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static double getLatitude(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static double getLongitude(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static int getRotation(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static long getPicasaId(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static String getUserAccount(Context context, MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static ParcelFileDescriptor openFile(Context context, MediaObject image, String mode)
+            throws FileNotFoundException {
+        throw new UnsupportedOperationException();
+    }
+
+    public static void initialize(Context context) {/*do nothing*/}
+
+    public static void requestSync(Context context) {/*do nothing*/}
+
+    public static void showSignInReminder(Activity context) {/*do nothing*/}
+
+    public static void onPackageAdded(Context context, String packageName) {/*do nothing*/}
+
+    public static void onPackageRemoved(Context context, String packageName) {/*do nothing*/}
+
+    public static void onPackageChanged(Context context, String packageName) {/*do nothing*/}
+
+    public static Dialog getVersionCheckDialog(Activity activity){
+        return null;
+    }
+}
diff --git a/src_pd/com/android/gallery3d/settings/GallerySettings.java b/src_pd/com/android/gallery3d/settings/GallerySettings.java
new file mode 100644
index 0000000..d30d755
--- /dev/null
+++ b/src_pd/com/android/gallery3d/settings/GallerySettings.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.settings;
+
+import android.preference.PreferenceActivity;
+
+public class GallerySettings extends PreferenceActivity {
+    private static final String TAG = "GallerySettings";
+}
diff --git a/src_pd/com/android/gallery3d/util/HelpUtils.java b/src_pd/com/android/gallery3d/util/HelpUtils.java
new file mode 100644
index 0000000..7da3fca
--- /dev/null
+++ b/src_pd/com/android/gallery3d/util/HelpUtils.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.util;
+
+import android.content.Context;
+import android.content.Intent;
+
+public class HelpUtils {
+
+    public static Intent getHelpIntent(Context context) {
+		return null;
+	}
+
+}
diff --git a/src_pd/com/android/gallery3d/util/LightCycleHelper.java b/src_pd/com/android/gallery3d/util/LightCycleHelper.java
new file mode 100644
index 0000000..5cd910f
--- /dev/null
+++ b/src_pd/com/android/gallery3d/util/LightCycleHelper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import com.android.camera.CameraModule;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingProgressManager;
+
+public class LightCycleHelper {
+    public static class PanoramaMetadata {
+        // Whether a panorama viewer should be used
+        public final boolean mUsePanoramaViewer;
+        // Whether a panorama is 360 degrees
+        public final boolean mIsPanorama360;
+
+        public PanoramaMetadata(boolean usePanoramaViewer, boolean isPanorama360) {
+            mUsePanoramaViewer = usePanoramaViewer;
+            mIsPanorama360 = isPanorama360;
+        }
+    }
+
+    public static class PanoramaViewHelper {
+
+        public PanoramaViewHelper(Activity activity) {
+            /* Do nothing */
+        }
+
+        public void onStart() {
+            /* Do nothing */
+        }
+
+        public void onCreate() {
+            /* Do nothing */
+        }
+
+        public void onStop() {
+            /* Do nothing */
+        }
+
+        public void showPanorama(Uri uri) {
+            /* Do nothing */
+        }
+    }
+
+    public static final PanoramaMetadata NOT_PANORAMA = new PanoramaMetadata(false, false);
+
+    public static void setupCaptureIntent(Context context, Intent it, String outputDir) {
+        /* Do nothing */
+    }
+
+    public static boolean hasLightCycleCapture(Context context) {
+        return false;
+    }
+
+    public static PanoramaMetadata getPanoramaMetadata(Context context, Uri uri) {
+        return NOT_PANORAMA;
+    }
+
+    public static CameraModule createPanoramaModule() {
+        return null;
+    }
+
+    public static StitchingProgressManager createStitchingManagerInstance(GalleryApp app) {
+        return null;
+    }
+
+    /**
+     * Get the file path from a Media storage URI.
+     */
+    public static String getPathFromURI(ContentResolver contentResolver, Uri contentUri) {
+        return null;
+    }
+
+    /**
+     * Get the modified time from a Media storage URI.
+     */
+    public static long getModifiedTimeFromURI(ContentResolver contentResolver, Uri contentUri) {
+        return 0;
+    }
+}
diff --git a/src_pd/com/android/gallery3d/util/RefocusHelper.java b/src_pd/com/android/gallery3d/util/RefocusHelper.java
new file mode 100644
index 0000000..39ded47
--- /dev/null
+++ b/src_pd/com/android/gallery3d/util/RefocusHelper.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.util;
+
+import com.android.camera.CameraModule;
+
+public class RefocusHelper {
+    public static CameraModule createRefocusModule() {
+        return null;
+    }
+}
diff --git a/src_pd/com/android/gallery3d/util/UsageStatistics.java b/src_pd/com/android/gallery3d/util/UsageStatistics.java
new file mode 100644
index 0000000..48fc6ae
--- /dev/null
+++ b/src_pd/com/android/gallery3d/util/UsageStatistics.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.util;
+
+import android.content.Context;
+
+public class UsageStatistics {
+
+    public static final String COMPONENT_GALLERY = "Gallery";
+    public static final String COMPONENT_CAMERA = "Camera";
+    public static final String COMPONENT_EDITOR = "Editor";
+    public static final String COMPONENT_IMPORTER = "Importer";
+
+    public static final String TRANSITION_BACK_BUTTON = "BackButton";
+    public static final String TRANSITION_UP_BUTTON = "UpButton";
+    public static final String TRANSITION_PINCH_IN = "PinchIn";
+    public static final String TRANSITION_PINCH_OUT = "PinchOut";
+    public static final String TRANSITION_INTENT = "Intent";
+    public static final String TRANSITION_ITEM_TAP = "ItemTap";
+    public static final String TRANSITION_MENU_TAP = "MenuTap";
+    public static final String TRANSITION_BUTTON_TAP = "ButtonTap";
+    public static final String TRANSITION_SWIPE = "Swipe";
+
+    public static final String ACTION_CAPTURE_START = "CaptureStart";
+    public static final String ACTION_CAPTURE_FAIL = "CaptureFail";
+    public static final String ACTION_CAPTURE_DONE = "CaptureDone";
+    public static final String ACTION_SHARE = "Share";
+
+    public static final String CATEGORY_LIFECYCLE = "AppLifecycle";
+    public static final String CATEGORY_BUTTON_PRESS = "ButtonPress";
+
+    public static final String LIFECYCLE_START = "Start";
+
+    public static void initialize(Context context) {}
+    public static void setPendingTransitionCause(String cause) {}
+    public static void onContentViewChanged(String screenComponent, String screenName) {}
+    public static void onEvent(String category, String action, String label) {};
+    public static void onEvent(String category, String action, String label, long optional_value) {};
+}
diff --git a/src_pd/com/android/gallery3d/util/XmpUtilHelper.java b/src_pd/com/android/gallery3d/util/XmpUtilHelper.java
new file mode 100644
index 0000000..88b813d
--- /dev/null
+++ b/src_pd/com/android/gallery3d/util/XmpUtilHelper.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import com.adobe.xmp.XMPMeta;
+
+import java.io.InputStream;
+
+public class XmpUtilHelper {
+
+    public static XMPMeta extractXMPMeta(InputStream is) {
+        return null;
+    }
+
+    public static boolean writeXMPMeta(String filename, Object meta) {
+        return false;
+    }
+
+}
diff --git a/src_pd/com/android/photos/data/PhotoProviderAuthority.java b/src_pd/com/android/photos/data/PhotoProviderAuthority.java
new file mode 100644
index 0000000..0ac76cb
--- /dev/null
+++ b/src_pd/com/android/photos/data/PhotoProviderAuthority.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+interface PhotoProviderAuthority {
+    public static final String AUTHORITY = "com.android.gallery3d.photoprovider";
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..0cc5f87
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,18 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := 16
+
+LOCAL_STATIC_JAVA_LIBRARIES := littlemock dexmaker
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := Gallery2Tests
+
+LOCAL_INSTRUMENTATION_FOR := Gallery2
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..f44156e
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.gallery3d.tests">
+
+    <application
+        android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+             android:targetPackage="com.android.gallery3d"
+             android:label="Tests for GalleryNew3D application."/>
+
+    <instrumentation android:name="com.android.gallery3d.CameraTestRunner"
+            android:targetPackage="com.android.gallery3d"
+            android:label="Camera continuous test runner"/>
+
+    <instrumentation android:name="com.android.gallery3d.exif.ExifTestRunner"
+            android:targetPackage="com.android.gallery3d"
+            android:label="Tests for ExifParser."/>
+
+    <instrumentation android:name="com.android.gallery3d.jpegstream.JpegStreamTestRunner"
+            android:targetPackage="com.android.gallery3d"
+            android:label="Tests for JpegStream classes."/>
+
+    <instrumentation android:name="com.android.gallery3d.stress.CameraStressTestRunner"
+            android:targetPackage="com.android.gallery3d"
+            android:label="Camera stress test runner"/>
+
+    <instrumentation android:name="com.android.photos.data.DataTestRunner"
+            android:targetPackage="com.android.gallery3d"
+            android:label="Tests for android photo DataProviders."/>
+</manifest>
diff --git a/tests/res/raw/android_lawn.mp4 b/tests/res/raw/android_lawn.mp4
new file mode 100644
index 0000000..bdeffbe
--- /dev/null
+++ b/tests/res/raw/android_lawn.mp4
Binary files differ
diff --git a/tests/res/raw/galaxy_nexus.jpg b/tests/res/raw/galaxy_nexus.jpg
new file mode 100755
index 0000000..de91df6
--- /dev/null
+++ b/tests/res/raw/galaxy_nexus.jpg
Binary files differ
diff --git a/tests/res/raw/jpeg_control.jpg b/tests/res/raw/jpeg_control.jpg
new file mode 100644
index 0000000..bb468a7
--- /dev/null
+++ b/tests/res/raw/jpeg_control.jpg
Binary files differ
diff --git a/tests/res/xml/galaxy_nexus.xml b/tests/res/xml/galaxy_nexus.xml
new file mode 100644
index 0000000..55dd524
--- /dev/null
+++ b/tests/res/xml/galaxy_nexus.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<exif>
+    <tag ifd="IFD0" id="0x0100" name="ImageWidth">2560</tag>
+    <tag ifd="IFD0" id="0x0101" name="ImageHeight">1920</tag>
+    <tag ifd="IFD0" id="0x010f" name="Make">google</tag>
+    <tag ifd="IFD0" id="0x0110" name="Model">Nexus S</tag>
+    <tag ifd="IFD0" id="0x0112" name="Orientation">1</tag>
+    <tag ifd="IFD0" id="0x0131" name="Software">MASTER</tag>
+    <tag ifd="IFD0" id="0x0132" name="ModifyDate">2012:07:30 16:28:42</tag>
+    <tag ifd="IFD0" id="0x0213" name="YCbCrPositioning">1</tag>
+    <tag ifd="IFD0" id="0x8769" name="ExifOffset">NO_VALUE</tag>
+    <tag ifd="ExifIFD" id="0x829a" name="ExposureTime">1/40</tag>
+    <tag ifd="ExifIFD" id="0x829d" name="FNumber">26/10</tag>
+    <tag ifd="ExifIFD" id="0x8822" name="ExposureProgram">3</tag>
+    <tag ifd="ExifIFD" id="0x8827" name="ISO">100</tag>
+    <tag ifd="ExifIFD" id="0x9000" name="ExifVersion">0220</tag>
+    <tag ifd="ExifIFD" id="0x9003" name="DateTimeOriginal">2012:07:30 16:28:42</tag>
+    <tag ifd="ExifIFD" id="0x9004" name="CreateDate">2012:07:30 16:28:42</tag>
+    <tag ifd="ExifIFD" id="0x9201" name="ShutterSpeedValue">50/10</tag>
+    <tag ifd="ExifIFD" id="0x9202" name="ApertureValue">30/10</tag>
+    <tag ifd="ExifIFD" id="0x9203" name="BrightnessValue">30/10</tag>
+    <tag ifd="ExifIFD" id="0x9204" name="ExposureCompensation">0/0</tag>
+    <tag ifd="ExifIFD" id="0x9205" name="MaxApertureValue">30/10</tag>
+    <tag ifd="ExifIFD" id="0x9207" name="MeteringMode">2</tag>
+    <tag ifd="ExifIFD" id="0x9209" name="Flash">0</tag>
+    <tag ifd="ExifIFD" id="0x920a" name="FocalLength">343/100</tag>
+    <tag ifd="ExifIFD" id="0x9286" name="UserComment">IICSAUser comments</tag>
+    <tag ifd="ExifIFD" id="0xa001" name="ColorSpace">1</tag>
+    <tag ifd="ExifIFD" id="0xa002" name="ExifImageWidth">2560</tag>
+    <tag ifd="ExifIFD" id="0xa003" name="ExifImageHeight">1920</tag>
+    <tag ifd="ExifIFD" id="0xa402" name="ExposureMode">0</tag>
+    <tag ifd="ExifIFD" id="0xa403" name="WhiteBalance">0</tag>
+    <tag ifd="ExifIFD" id="0xa406" name="SceneCaptureType">0</tag>
+    <tag ifd="IFD1" id="0x0100" name="ImageWidth">320</tag>
+    <tag ifd="IFD1" id="0x0101" name="ImageHeight">240</tag>
+    <tag ifd="IFD1" id="0x0103" name="Compression">6</tag>
+    <tag ifd="IFD1" id="0x0112" name="Orientation">1</tag>
+    <tag ifd="IFD1" id="0x011a" name="XResolution">72/1</tag>
+    <tag ifd="IFD1" id="0x011b" name="YResolution">72/1</tag>
+    <tag ifd="IFD1" id="0x0128" name="ResolutionUnit">2</tag>
+    <tag ifd="IFD1" id="0x0201" name="ThumbnailOffset">690</tag>
+    <tag ifd="IFD1" id="0x0202" name="ThumbnailLength">10447</tag>
+</exif>
diff --git a/tests/src/com/android/gallery3d/CameraTestRunner.java b/tests/src/com/android/gallery3d/CameraTestRunner.java
new file mode 100755
index 0000000..5032336
--- /dev/null
+++ b/tests/src/com/android/gallery3d/CameraTestRunner.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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.gallery3d;
+
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+
+import com.android.gallery3d.functional.CameraTest;
+import com.android.gallery3d.functional.ImageCaptureIntentTest;
+import com.android.gallery3d.functional.VideoCaptureIntentTest;
+import com.android.gallery3d.unittest.CameraUnitTest;
+
+import junit.framework.TestSuite;
+
+
+public class CameraTestRunner extends InstrumentationTestRunner {
+
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(CameraTest.class);
+        suite.addTestSuite(ImageCaptureIntentTest.class);
+        suite.addTestSuite(VideoCaptureIntentTest.class);
+        suite.addTestSuite(CameraUnitTest.class);
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return CameraTestRunner.class.getClassLoader();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/StressTests.java b/tests/src/com/android/gallery3d/StressTests.java
new file mode 100755
index 0000000..b991e9e
--- /dev/null
+++ b/tests/src/com/android/gallery3d/StressTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d;
+
+import com.android.gallery3d.stress.CameraLatency;
+import com.android.gallery3d.stress.CameraStartUp;
+import com.android.gallery3d.stress.ImageCapture;
+import com.android.gallery3d.stress.SwitchPreview;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+
+/**
+ * Instrumentation Test Runner for all Camera tests.
+ *
+ * Running all tests:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.gallery3d.StressTests \
+ *    -w com.google.android.gallery3d.tests/com.android.gallery3d.stress.CameraStressTestRunner
+ */
+
+public class StressTests extends TestSuite {
+    public static Test suite() {
+        TestSuite result = new TestSuite();
+        result.addTestSuite(CameraLatency.class);
+        result.addTestSuite(CameraStartUp.class);
+        result.addTestSuite(ImageCapture.class);
+//      result.addTestSuite(SwitchPreview.class);
+        return result;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/anim/AnimationTest.java b/tests/src/com/android/gallery3d/anim/AnimationTest.java
new file mode 100644
index 0000000..c7d5dae
--- /dev/null
+++ b/tests/src/com/android/gallery3d/anim/AnimationTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.anim;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+import android.view.animation.Interpolator;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class AnimationTest extends TestCase {
+    private static final String TAG = "AnimationTest";
+
+    public void testFloatAnimation() {
+        FloatAnimation a = new FloatAnimation(0f, 1f, 10);  // value 0 to 1.0, duration 10
+        a.start();                 // start animation
+        assertTrue(a.isActive());  // should be active now
+        a.calculate(0);            // set start time = 0
+        assertTrue(a.get() == 0);  // start value should be 0
+        a.calculate(1);            // calculate value for time 1
+        assertFloatEq(a.get(), 0.1f);
+        a.calculate(5);            // calculate value for time 5
+        assertTrue(a.get() == 0.5);//
+        a.calculate(9);            // calculate value for time 9
+        assertFloatEq(a.get(), 0.9f);
+        a.calculate(10);           // calculate value for time 10
+        assertTrue(!a.isActive()); // should be inactive now
+        assertTrue(a.get() == 1.0);//
+        a.start();                 // restart
+        assertTrue(a.isActive());  // should be active now
+        a.calculate(5);            // set start time = 5
+        assertTrue(a.get() == 0);  // start value should be 0
+        a.calculate(5+9);          // calculate for time 5+9
+        assertFloatEq(a.get(), 0.9f);
+    }
+
+    private static class MyInterpolator implements Interpolator {
+        public float getInterpolation(float input) {
+            return 4f * (input - 0.5f);  // maps [0,1] to [-2,2]
+        }
+    }
+
+    public void testInterpolator() {
+        FloatAnimation a = new FloatAnimation(0f, 1f, 10);  // value 0 to 1.0, duration 10
+        a.setInterpolator(new MyInterpolator());
+        a.start();                 // start animation
+        a.calculate(0);            // set start time = 0
+        assertTrue(a.get() == -2); // start value should be -2
+        a.calculate(1);            // calculate value for time 1
+        assertFloatEq(a.get(), -1.6f);
+        a.calculate(5);            // calculate value for time 5
+        assertTrue(a.get() == 0);  //
+        a.calculate(9);            // calculate value for time 9
+        assertFloatEq(a.get(), 1.6f);
+        a.calculate(10);           // calculate value for time 10
+        assertTrue(a.get() == 2);  //
+    }
+
+    public static void assertFloatEq(float expected, float actual) {
+        if (Math.abs(actual - expected) > 1e-6) {
+            Log.v(TAG, "expected: " + expected + ", actual: " + actual);
+            fail();
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/common/BlobCacheTest.java b/tests/src/com/android/gallery3d/common/BlobCacheTest.java
new file mode 100644
index 0000000..2a911c4
--- /dev/null
+++ b/tests/src/com/android/gallery3d/common/BlobCacheTest.java
@@ -0,0 +1,738 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.common;
+
+import com.android.gallery3d.common.BlobCache;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Random;
+
+public class BlobCacheTest extends AndroidTestCase {
+    private static final String TAG = "BlobCacheTest";
+
+    @SmallTest
+    public void testReadIntLong() {
+        byte[] buf = new byte[9];
+        assertEquals(0, BlobCache.readInt(buf, 0));
+        assertEquals(0, BlobCache.readLong(buf, 0));
+        buf[0] = 1;
+        assertEquals(1, BlobCache.readInt(buf, 0));
+        assertEquals(1, BlobCache.readLong(buf, 0));
+        buf[3] = 0x7f;
+        assertEquals(0x7f000001, BlobCache.readInt(buf, 0));
+        assertEquals(0x7f000001, BlobCache.readLong(buf, 0));
+        assertEquals(0x007f0000, BlobCache.readInt(buf, 1));
+        assertEquals(0x007f0000, BlobCache.readLong(buf, 1));
+        buf[3] = (byte) 0x80;
+        buf[7] = (byte) 0xA0;
+        buf[0] = 0;
+        assertEquals(0x80000000, BlobCache.readInt(buf, 0));
+        assertEquals(0xA000000080000000L, BlobCache.readLong(buf, 0));
+        for (int i = 0; i < 8; i++) {
+            buf[i] = (byte) (0x11 * (i+8));
+        }
+        assertEquals(0xbbaa9988, BlobCache.readInt(buf, 0));
+        assertEquals(0xffeeddccbbaa9988L, BlobCache.readLong(buf, 0));
+        buf[8] = 0x33;
+        assertEquals(0x33ffeeddccbbaa99L, BlobCache.readLong(buf, 1));
+    }
+
+    @SmallTest
+    public void testWriteIntLong() {
+        byte[] buf = new byte[8];
+        BlobCache.writeInt(buf, 0, 0x12345678);
+        assertEquals(0x78, buf[0]);
+        assertEquals(0x56, buf[1]);
+        assertEquals(0x34, buf[2]);
+        assertEquals(0x12, buf[3]);
+        assertEquals(0x00, buf[4]);
+        BlobCache.writeLong(buf, 0, 0xffeeddccbbaa9988L);
+        for (int i = 0; i < 8; i++) {
+            assertEquals((byte) (0x11 * (i+8)), buf[i]);
+        }
+    }
+
+    @MediumTest
+    public void testChecksum() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true);
+        byte[] buf = new byte[0];
+        assertEquals(0x1, bc.checkSum(buf));
+        buf = new byte[1];
+        assertEquals(0x10001, bc.checkSum(buf));
+        buf[0] = 0x47;
+        assertEquals(0x480048, bc.checkSum(buf));
+        buf = new byte[3];
+        buf[0] = 0x10;
+        buf[1] = 0x30;
+        buf[2] = 0x01;
+        assertEquals(0x940042, bc.checkSum(buf));
+        assertEquals(0x310031, bc.checkSum(buf, 1, 1));
+        assertEquals(0x1, bc.checkSum(buf, 1, 0));
+        assertEquals(0x630032, bc.checkSum(buf, 1, 2));
+        buf = new byte[1024];
+        for (int i = 0; i < buf.length; i++) {
+            buf[i] = (byte)(i*i);
+        }
+        assertEquals(0x3574a610, bc.checkSum(buf));
+        bc.close();
+    }
+
+    private static final int HEADER_SIZE = 32;
+    private static final int DATA_HEADER_SIZE = 4;
+    private static final int BLOB_HEADER_SIZE = 20;
+
+    private static final String TEST_FILE_NAME = "/sdcard/btest";
+    private static final int MAX_ENTRIES = 100;
+    private static final int MAX_BYTES = 1000;
+    private static final int INDEX_SIZE = HEADER_SIZE + MAX_ENTRIES * 12 * 2;
+    private static final long KEY_0 = 0x1122334455667788L;
+    private static final long KEY_1 = 0x1122334455667789L;
+    private static final long KEY_2 = 0x112233445566778AL;
+    private static byte[] DATA_0 = new byte[10];
+    private static byte[] DATA_1 = new byte[10];
+
+    @MediumTest
+    public void testBasic() throws IOException {
+        String name = TEST_FILE_NAME;
+        BlobCache bc;
+        File idxFile = new File(name + ".idx");
+        File data0File = new File(name + ".0");
+        File data1File = new File(name + ".1");
+
+        // Create a brand new cache.
+        bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, true);
+        bc.close();
+
+        // Make sure the initial state is correct.
+        assertTrue(idxFile.exists());
+        assertTrue(data0File.exists());
+        assertTrue(data1File.exists());
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE, data0File.length());
+        assertEquals(DATA_HEADER_SIZE, data1File.length());
+        assertEquals(0, bc.getActiveCount());
+
+        // Re-open it.
+        bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false);
+        assertNull(bc.lookup(KEY_0));
+
+        // insert one blob
+        genData(DATA_0, 1);
+        bc.insert(KEY_0, DATA_0);
+        assertSameData(DATA_0, bc.lookup(KEY_0));
+        assertEquals(1, bc.getActiveCount());
+        bc.close();
+
+        // Make sure the file size is right.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + BLOB_HEADER_SIZE + DATA_0.length,
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE, data1File.length());
+
+        // Re-open it and make sure we can get the old data
+        bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false);
+        assertSameData(DATA_0, bc.lookup(KEY_0));
+
+        // insert with the same key (but using a different blob)
+        genData(DATA_0, 2);
+        bc.insert(KEY_0, DATA_0);
+        assertSameData(DATA_0, bc.lookup(KEY_0));
+        assertEquals(1, bc.getActiveCount());
+        bc.close();
+
+        // Make sure the file size is right.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + 2 * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE, data1File.length());
+
+        // Re-open it and make sure we can get the old data
+        bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false);
+        assertSameData(DATA_0, bc.lookup(KEY_0));
+
+        // insert another key and make sure we can get both key.
+        assertNull(bc.lookup(KEY_1));
+        genData(DATA_1, 3);
+        bc.insert(KEY_1, DATA_1);
+        assertSameData(DATA_0, bc.lookup(KEY_0));
+        assertSameData(DATA_1, bc.lookup(KEY_1));
+        assertEquals(2, bc.getActiveCount());
+        bc.close();
+
+        // Make sure the file size is right.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + 3 * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE, data1File.length());
+
+        // Re-open it and make sure we can get the old data
+        bc = new BlobCache(name, 100, 1000, false);
+        assertSameData(DATA_0, bc.lookup(KEY_0));
+        assertSameData(DATA_1, bc.lookup(KEY_1));
+        assertEquals(2, bc.getActiveCount());
+        bc.close();
+    }
+
+    @MediumTest
+    public void testNegativeKey() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true);
+
+        // insert one blob
+        genData(DATA_0, 1);
+        bc.insert(-123, DATA_0);
+        assertSameData(DATA_0, bc.lookup(-123));
+        bc.close();
+    }
+
+    @MediumTest
+    public void testEmptyBlob() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true);
+
+        byte[] data = new byte[0];
+        bc.insert(123, data);
+        assertSameData(data, bc.lookup(123));
+        bc.close();
+    }
+
+    @MediumTest
+    public void testLookupRequest() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true);
+
+        // insert one blob
+        genData(DATA_0, 1);
+        bc.insert(1, DATA_0);
+        assertSameData(DATA_0, bc.lookup(1));
+
+        // the same size buffer
+        byte[] buf = new byte[DATA_0.length];
+        BlobCache.LookupRequest req = new BlobCache.LookupRequest();
+        req.key = 1;
+        req.buffer = buf;
+        assertTrue(bc.lookup(req));
+        assertEquals(1, req.key);
+        assertSame(buf, req.buffer);
+        assertEquals(DATA_0.length, req.length);
+
+        // larger buffer
+        buf = new byte[DATA_0.length + 22];
+        req = new BlobCache.LookupRequest();
+        req.key = 1;
+        req.buffer = buf;
+        assertTrue(bc.lookup(req));
+        assertEquals(1, req.key);
+        assertSame(buf, req.buffer);
+        assertEquals(DATA_0.length, req.length);
+
+        // smaller buffer
+        buf = new byte[DATA_0.length - 1];
+        req = new BlobCache.LookupRequest();
+        req.key = 1;
+        req.buffer = buf;
+        assertTrue(bc.lookup(req));
+        assertEquals(1, req.key);
+        assertNotSame(buf, req.buffer);
+        assertEquals(DATA_0.length, req.length);
+        assertSameData(DATA_0, req.buffer, DATA_0.length);
+
+        // null buffer
+        req = new BlobCache.LookupRequest();
+        req.key = 1;
+        req.buffer = null;
+        assertTrue(bc.lookup(req));
+        assertEquals(1, req.key);
+        assertNotNull(req.buffer);
+        assertEquals(DATA_0.length, req.length);
+        assertSameData(DATA_0, req.buffer, DATA_0.length);
+
+        bc.close();
+    }
+
+    @MediumTest
+    public void testKeyCollision() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true);
+
+        for (int i = 0; i < MAX_ENTRIES / 2; i++) {
+            genData(DATA_0, i);
+            long key = KEY_1 + i * MAX_ENTRIES;
+            bc.insert(key, DATA_0);
+        }
+
+        for (int i = 0; i < MAX_ENTRIES / 2; i++) {
+            genData(DATA_0, i);
+            long key = KEY_1 + i * MAX_ENTRIES;
+            assertSameData(DATA_0, bc.lookup(key));
+        }
+        bc.close();
+    }
+
+    @MediumTest
+    public void testRegionFlip() throws IOException {
+        String name = TEST_FILE_NAME;
+        BlobCache bc;
+        File idxFile = new File(name + ".idx");
+        File data0File = new File(name + ".0");
+        File data1File = new File(name + ".1");
+
+        // Create a brand new cache.
+        bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, true);
+
+        // This is the number of blobs fits into a region.
+        int maxFit = (MAX_BYTES - DATA_HEADER_SIZE) /
+                (BLOB_HEADER_SIZE + DATA_0.length);
+
+        for (int k = 0; k < maxFit; k++) {
+            genData(DATA_0, k);
+            bc.insert(k, DATA_0);
+        }
+        assertEquals(maxFit, bc.getActiveCount());
+
+        // Make sure the file size is right.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE, data1File.length());
+
+        // Now insert another one and let it flip.
+        genData(DATA_0, 777);
+        bc.insert(KEY_1, DATA_0);
+        assertEquals(1, bc.getActiveCount());
+
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length),
+                data1File.length());
+
+        // Make sure we can find the new data
+        assertSameData(DATA_0, bc.lookup(KEY_1));
+
+        // Now find an old blob
+        int old = maxFit / 2;
+        genData(DATA_0, old);
+        assertSameData(DATA_0, bc.lookup(old));
+        assertEquals(2, bc.getActiveCount());
+
+        // Observed data is copied.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE + 2 * (BLOB_HEADER_SIZE + DATA_0.length),
+                data1File.length());
+
+        // Now copy everything over (except we should have no space for the last one)
+        assertTrue(old < maxFit - 1);
+        for (int k = 0; k < maxFit; k++) {
+            genData(DATA_0, k);
+            assertSameData(DATA_0, bc.lookup(k));
+        }
+        assertEquals(maxFit, bc.getActiveCount());
+
+        // Now both file should be full.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data1File.length());
+
+        // Now insert one to make it flip.
+        genData(DATA_0, 888);
+        bc.insert(KEY_2, DATA_0);
+        assertEquals(1, bc.getActiveCount());
+
+        // Check the size after the second flip.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data1File.length());
+
+        // Now the last key should be gone.
+        assertNull(bc.lookup(maxFit - 1));
+
+        // But others should remain
+        for (int k = 0; k < maxFit - 1; k++) {
+            genData(DATA_0, k);
+            assertSameData(DATA_0, bc.lookup(k));
+        }
+
+        assertEquals(maxFit, bc.getActiveCount());
+        genData(DATA_0, 777);
+        assertSameData(DATA_0, bc.lookup(KEY_1));
+        genData(DATA_0, 888);
+        assertSameData(DATA_0, bc.lookup(KEY_2));
+        assertEquals(maxFit, bc.getActiveCount());
+
+        // Now two files should be full.
+        assertEquals(INDEX_SIZE, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data1File.length());
+
+        bc.close();
+    }
+
+    @MediumTest
+    public void testEntryLimit() throws IOException {
+        String name = TEST_FILE_NAME;
+        BlobCache bc;
+        File idxFile = new File(name + ".idx");
+        File data0File = new File(name + ".0");
+        File data1File = new File(name + ".1");
+        int maxEntries = 10;
+        int maxFit = maxEntries / 2;
+        int indexSize = HEADER_SIZE + maxEntries * 12 * 2;
+
+        // Create a brand new cache with a small entry limit.
+        bc = new BlobCache(name, maxEntries, MAX_BYTES, true);
+
+        // Fill to just before flipping
+        for (int i = 0; i < maxFit; i++) {
+            genData(DATA_0, i);
+            bc.insert(i, DATA_0);
+        }
+        assertEquals(maxFit, bc.getActiveCount());
+
+        // Check the file size.
+        assertEquals(indexSize, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE, data1File.length());
+
+        // Insert one and make it flip
+        genData(DATA_0, 777);
+        bc.insert(777, DATA_0);
+        assertEquals(1, bc.getActiveCount());
+
+        // Check the file size.
+        assertEquals(indexSize, idxFile.length());
+        assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length),
+                data0File.length());
+        assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length),
+                data1File.length());
+        bc.close();
+    }
+
+    @LargeTest
+    public void testDataIntegrity() throws IOException {
+        String name = TEST_FILE_NAME;
+        File idxFile = new File(name + ".idx");
+        File data0File = new File(name + ".0");
+        File data1File = new File(name + ".1");
+        RandomAccessFile f;
+
+        Log.v(TAG, "It should be readable if the content is not changed.");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(1);
+        byte b = f.readByte();
+        f.seek(1);
+        f.write(b);
+        f.close();
+        assertReadable();
+
+        Log.v(TAG, "Change the data file magic field");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(1);
+        f.write(0xFF);
+        f.close();
+        assertUnreadable();
+
+        prepareNewCache();
+        f = new RandomAccessFile(data1File, "rw");
+        f.write(0xFF);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the blob key");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(4);
+        f.write(0x00);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the blob checksum");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(4 + 8);
+        f.write(0x00);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the blob offset");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(4 + 12);
+        f.write(0x20);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the blob length: some other value");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(4 + 16);
+        f.write(0x20);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the blob length: -1");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(4 + 16);
+        f.writeInt(0xFFFFFFFF);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the blob length: big value");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(4 + 16);
+        f.writeInt(0xFFFFFF00);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the blob content");
+        prepareNewCache();
+        f = new RandomAccessFile(data0File, "rw");
+        f.seek(4 + 20);
+        f.write(0x01);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the index magic");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        f.seek(1);
+        f.write(0x00);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the active region");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        f.seek(12);
+        f.write(0x01);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the reserved data");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        f.seek(24);
+        f.write(0x01);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the checksum");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        f.seek(29);
+        f.write(0x00);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the key");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES));
+        f.write(0x00);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the offset");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES) + 8);
+        f.write(0x05);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Change the offset");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES) + 8 + 3);
+        f.write(0xFF);
+        f.close();
+        assertUnreadable();
+
+        Log.v(TAG, "Garbage index");
+        prepareNewCache();
+        f = new RandomAccessFile(idxFile, "rw");
+        int n = (int) idxFile.length();
+        f.seek(32);
+        byte[] garbage = new byte[1024];
+        for (int i = 0; i < garbage.length; i++) {
+            garbage[i] = (byte) 0x80;
+        }
+        int i = 32;
+        while (i < n) {
+            int todo = Math.min(garbage.length, n - i);
+            f.write(garbage, 0, todo);
+            i += todo;
+        }
+        f.close();
+        assertUnreadable();
+    }
+
+    // Create a brand new cache and put one entry into it.
+    private void prepareNewCache() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true);
+        genData(DATA_0, 777);
+        bc.insert(KEY_1, DATA_0);
+        bc.close();
+    }
+
+    private void assertReadable() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, false);
+        genData(DATA_0, 777);
+        assertSameData(DATA_0, bc.lookup(KEY_1));
+        bc.close();
+    }
+
+    private void assertUnreadable() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, false);
+        genData(DATA_0, 777);
+        assertNull(bc.lookup(KEY_1));
+        bc.close();
+    }
+
+    @LargeTest
+    public void testRandomSize() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true);
+
+        // Random size test
+        Random rand = new Random(0);
+        for (int i = 0; i < 100; i++) {
+            byte[] data = new byte[rand.nextInt(MAX_BYTES*12/10)];
+            try {
+                bc.insert(rand.nextLong(), data);
+                if (data.length > MAX_BYTES - 4 - 20) fail();
+            } catch (RuntimeException ex) {
+                if (data.length <= MAX_BYTES - 4 - 20) fail();
+            }
+        }
+
+        bc.close();
+    }
+
+    @LargeTest
+    public void testBandwidth() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, 1000, 10000000, true);
+
+        // Write
+        int count = 0;
+        byte[] data = new byte[20000];
+        long t0 = System.nanoTime();
+        for (int i = 0; i < 1000; i++) {
+            bc.insert(i, data);
+            count += data.length;
+        }
+        bc.syncAll();
+        float delta = (System.nanoTime() - t0) * 1e-3f;
+        Log.v(TAG, "write bandwidth = " + (count / delta) + " M/s");
+
+        // Copy over
+        BlobCache.LookupRequest req = new BlobCache.LookupRequest();
+        count = 0;
+        t0 = System.nanoTime();
+        for (int i = 0; i < 1000; i++) {
+            req.key = i;
+            req.buffer = data;
+            if (bc.lookup(req)) {
+                count += req.length;
+            }
+        }
+        bc.syncAll();
+        delta = (System.nanoTime() - t0) * 1e-3f;
+        Log.v(TAG, "copy over bandwidth = " + (count / delta) + " M/s");
+
+        // Read
+        count = 0;
+        t0 = System.nanoTime();
+        for (int i = 0; i < 1000; i++) {
+            req.key = i;
+            req.buffer = data;
+            if (bc.lookup(req)) {
+                count += req.length;
+            }
+        }
+        bc.syncAll();
+        delta = (System.nanoTime() - t0) * 1e-3f;
+        Log.v(TAG, "read bandwidth = " + (count / delta) + " M/s");
+
+        bc.close();
+    }
+
+    @LargeTest
+    public void testSmallSize() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, 40, true);
+
+        // Small size test
+        Random rand = new Random(0);
+        for (int i = 0; i < 100; i++) {
+            byte[] data = new byte[rand.nextInt(3)];
+            bc.insert(rand.nextLong(), data);
+        }
+
+        bc.close();
+    }
+
+    @LargeTest
+    public void testManyEntries() throws IOException {
+        BlobCache bc = new BlobCache(TEST_FILE_NAME, 1, MAX_BYTES, true);
+
+        // Many entries test
+        Random rand = new Random(0);
+        for (int i = 0; i < 100; i++) {
+            byte[] data = new byte[rand.nextInt(10)];
+        }
+
+        bc.close();
+    }
+
+    private void genData(byte[] data, int seed) {
+        for(int i = 0; i < data.length; i++) {
+            data[i] = (byte) (seed * i);
+        }
+    }
+
+    private void assertSameData(byte[] data1, byte[] data2) {
+        if (data1 == null && data2 == null) return;
+        if (data1 == null || data2 == null) fail();
+        if (data1.length != data2.length) fail();
+        for (int i = 0; i < data1.length; i++) {
+            if (data1[i] != data2[i]) fail();
+        }
+    }
+
+    private void assertSameData(byte[] data1, byte[] data2, int n) {
+        if (data1 == null || data2 == null) fail();
+        for (int i = 0; i < n; i++) {
+            if (data1[i] != data2[i]) fail();
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/common/UtilsTest.java b/tests/src/com/android/gallery3d/common/UtilsTest.java
new file mode 100644
index 0000000..a20ebeb
--- /dev/null
+++ b/tests/src/com/android/gallery3d/common/UtilsTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.common;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+public class UtilsTest extends AndroidTestCase {
+    private static final String TAG = "UtilsTest";
+
+    private static final int [] testData = new int [] {
+        /* outWidth, outHeight, minSideLength, maxNumOfPixels, sample size */
+        1, 1, BitmapUtils.UNCONSTRAINED, BitmapUtils.UNCONSTRAINED, 1,
+        1, 1, 1, 1, 1,
+        100, 100, 100, 10000, 1,
+        100, 100, 100, 2500, 2,
+        99, 66, 33, 10000, 2,
+        66, 99, 33, 10000, 2,
+        99, 66, 34, 10000, 1,
+        99, 66, 22, 10000, 4,
+        99, 66, 16, 10000, 4,
+
+        10000, 10000, 20000, 1000000, 16,
+
+        100, 100, 100, 10000, 1, // 1
+        100, 100, 50, 10000, 2,  // 2
+        100, 100, 30, 10000, 4,  // 3->4
+        100, 100, 22, 10000, 4,  // 4
+        100, 100, 20, 10000, 8,  // 5->8
+        100, 100, 11, 10000, 16, // 9->16
+        100, 100, 5,  10000, 24, // 20->24
+        100, 100, 2,  10000, 56, // 50->56
+
+        100, 100, 100, 10000 - 1, 2,                  // a bit less than 1
+        100, 100, 100, 10000 / (2 * 2) - 1, 4,        // a bit less than 2
+        100, 100, 100, 10000 / (3 * 3) - 1, 4,        // a bit less than 3
+        100, 100, 100, 10000 / (4 * 4) - 1, 8,        // a bit less than 4
+        100, 100, 100, 10000 / (8 * 8) - 1, 16,       // a bit less than 8
+        100, 100, 100, 10000 / (16 * 16) - 1, 24,     // a bit less than 16
+        100, 100, 100, 10000 / (24 * 24) - 1, 32,     // a bit less than 24
+        100, 100, 100, 10000 / (32 * 32) - 1, 40,     // a bit less than 32
+
+        640, 480, 480, BitmapUtils.UNCONSTRAINED, 1,  // 1
+        640, 480, 240, BitmapUtils.UNCONSTRAINED, 2,  // 2
+        640, 480, 160, BitmapUtils.UNCONSTRAINED, 4,  // 3->4
+        640, 480, 120, BitmapUtils.UNCONSTRAINED, 4,  // 4
+        640, 480, 96, BitmapUtils.UNCONSTRAINED,  8,  // 5->8
+        640, 480, 80, BitmapUtils.UNCONSTRAINED,  8,  // 6->8
+        640, 480, 60, BitmapUtils.UNCONSTRAINED,  8,  // 8
+        640, 480, 48, BitmapUtils.UNCONSTRAINED, 16,  // 10->16
+        640, 480, 40, BitmapUtils.UNCONSTRAINED, 16,  // 12->16
+        640, 480, 30, BitmapUtils.UNCONSTRAINED, 16,  // 16
+        640, 480, 24, BitmapUtils.UNCONSTRAINED, 24,  // 20->24
+        640, 480, 20, BitmapUtils.UNCONSTRAINED, 24,  // 24
+        640, 480, 16, BitmapUtils.UNCONSTRAINED, 32,  // 30->32
+        640, 480, 12, BitmapUtils.UNCONSTRAINED, 40,  // 40
+        640, 480, 10, BitmapUtils.UNCONSTRAINED, 48,  // 48
+        640, 480, 8, BitmapUtils.UNCONSTRAINED,  64,  // 60->64
+        640, 480, 6, BitmapUtils.UNCONSTRAINED,  80,  // 80
+        640, 480, 4, BitmapUtils.UNCONSTRAINED, 120,  // 120
+        640, 480, 3, BitmapUtils.UNCONSTRAINED, 160,  // 160
+        640, 480, 2, BitmapUtils.UNCONSTRAINED, 240,  // 240
+        640, 480, 1, BitmapUtils.UNCONSTRAINED, 480,  // 480
+
+        640, 480, BitmapUtils.UNCONSTRAINED, BitmapUtils.UNCONSTRAINED, 1,
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480, 1,                  // 1
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 - 1, 2,              // a bit less than 1
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 4, 2,              // 2
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 4 - 1, 4,          // a bit less than 2
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 9, 4,              // 3
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 9 - 1, 4,          // a bit less than 3
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 16, 4,             // 4
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 16 - 1, 8,         // a bit less than 4
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 64, 8,             // 8
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 64 - 1, 16,        // a bit less than 8
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 256, 16,           // 16
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 256 - 1, 24,       // a bit less than 16
+        640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / (24 * 24) - 1, 32, // a bit less than 24
+    };
+
+    @SmallTest
+    public void testComputeSampleSize() {
+
+        for (int i = 0; i < testData.length; i += 5) {
+            int w = testData[i];
+            int h = testData[i + 1];
+            int minSide = testData[i + 2];
+            int maxPixels = testData[i + 3];
+            int sampleSize = testData[i + 4];
+            int result = BitmapUtils.computeSampleSize(w, h, minSide, maxPixels);
+            if (result != sampleSize) {
+                Log.v(TAG, w + "x" + h + ", minSide = " + minSide + ", maxPixels = "
+                        + maxPixels + ", sampleSize = " + sampleSize + ", result = "
+                        + result);
+            }
+            assertTrue(sampleSize == result);
+        }
+    }
+
+    public void testAssert() {
+        // This should not throw an exception.
+        Utils.assertTrue(true);
+
+        // This should throw an exception.
+        try {
+            Utils.assertTrue(false);
+            fail();
+        } catch (AssertionError ex) {
+            // expected.
+        }
+    }
+
+    public void testCheckNotNull() {
+        // These should not throw an expection.
+        Utils.checkNotNull(new Object());
+        Utils.checkNotNull(0);
+        Utils.checkNotNull("");
+
+        // This should throw an expection.
+        try {
+            Utils.checkNotNull(null);
+            fail();
+        } catch (NullPointerException ex) {
+            // expected.
+        }
+    }
+
+    public void testEquals() {
+        Object a = new Object();
+        Object b = new Object();
+
+        assertTrue(Utils.equals(null, null));
+        assertTrue(Utils.equals(a, a));
+        assertFalse(Utils.equals(null, a));
+        assertFalse(Utils.equals(a, null));
+        assertFalse(Utils.equals(a, b));
+    }
+
+    public void testNextPowerOf2() {
+        int[] q = new int[] {1, 2, 3, 4, 5, 6, 10, 65535, (1 << 30) - 1, (1 << 30)};
+        int[] a = new int[] {1, 2, 4, 4, 8, 8, 16, 65536, (1 << 30)    , (1 << 30)};
+
+        for (int i = 0; i < q.length; i++) {
+            assertEquals(a[i], Utils.nextPowerOf2(q[i]));
+        }
+
+        int[] e = new int[] {0, -1, -2, -4, -65536, (1 << 30) + 1, Integer.MAX_VALUE};
+
+        for (int v : e) {
+            try {
+                Utils.nextPowerOf2(v);
+                fail();
+            } catch (IllegalArgumentException ex) {
+                // expected.
+            }
+        }
+    }
+
+    public void testClamp() {
+        assertEquals(1000, Utils.clamp(300, 1000, 2000));
+        assertEquals(1300, Utils.clamp(1300, 1000, 2000));
+        assertEquals(2000, Utils.clamp(2300, 1000, 2000));
+
+        assertEquals(0.125f, Utils.clamp(0.1f, 0.125f, 0.5f));
+        assertEquals(0.25f, Utils.clamp(0.25f, 0.125f, 0.5f));
+        assertEquals(0.5f, Utils.clamp(0.9f, 0.125f, 0.5f));
+    }
+
+    public void testIsOpaque() {
+        assertTrue(Utils.isOpaque(0xFF000000));
+        assertTrue(Utils.isOpaque(0xFFFFFFFF));
+        assertTrue(Utils.isOpaque(0xFF123456));
+
+        assertFalse(Utils.isOpaque(0xFEFFFFFF));
+        assertFalse(Utils.isOpaque(0x8FFFFFFF));
+        assertFalse(Utils.isOpaque(0x00FF0000));
+        assertFalse(Utils.isOpaque(0x5500FF00));
+        assertFalse(Utils.isOpaque(0xAA0000FF));
+    }
+
+    public static void assertFloatEq(float expected, float actual) {
+        if (Math.abs(actual - expected) > 1e-6) {
+            Log.v(TAG, "expected: " + expected + ", actual: " + actual);
+            fail();
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/data/GalleryAppMock.java b/tests/src/com/android/gallery3d/data/GalleryAppMock.java
new file mode 100644
index 0000000..bbc5692
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/GalleryAppMock.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Looper;
+
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootStub;
+
+class GalleryAppMock extends GalleryAppStub {
+    GLRoot mGLRoot = new GLRootStub();
+    DataManager mDataManager = new DataManager(this);
+    ContentResolver mResolver;
+    Context mContext;
+    Looper mMainLooper;
+
+    GalleryAppMock(Context context,
+            ContentResolver resolver, Looper mainLooper) {
+        mContext = context;
+        mResolver = resolver;
+        mMainLooper = mainLooper;
+    }
+
+    @Override
+    public GLRoot getGLRoot() { return mGLRoot; }
+    @Override
+    public DataManager getDataManager() { return mDataManager; }
+    @Override
+    public Context getAndroidContext() { return mContext; }
+    @Override
+    public ContentResolver getContentResolver() { return mResolver; }
+    @Override
+    public Looper getMainLooper() { return mMainLooper; }
+}
diff --git a/tests/src/com/android/gallery3d/data/GalleryAppStub.java b/tests/src/com/android/gallery3d/data/GalleryAppStub.java
new file mode 100644
index 0000000..5aff2a2
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/GalleryAppStub.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StateManager;
+import com.android.gallery3d.app.StitchingProgressManager;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+class GalleryAppStub implements GalleryApp {
+    public ImageCacheService getImageCacheService() { return null; }
+    public StateManager getStateManager() { return null; }
+    public DataManager getDataManager() { return null; }
+    public DownloadUtils getDownloadService() { return null; }
+    public DecodeUtils getDecodeService() { return null; }
+
+    public GLRoot getGLRoot() { return null; }
+
+    public Context getAndroidContext() { return null; }
+
+    public Looper getMainLooper() { return null; }
+    public Resources getResources() { return null; }
+    public ContentResolver getContentResolver() { return null; }
+    public ThreadPool getThreadPool() { return null; }
+    public DownloadCache getDownloadCache() { return null; }
+    public StitchingProgressManager getStitchingProgressManager() { return null; }
+}
diff --git a/tests/src/com/android/gallery3d/data/LocalDataTest.java b/tests/src/com/android/gallery3d/data/LocalDataTest.java
new file mode 100644
index 0000000..8f6a46b
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/LocalDataTest.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Looper;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class LocalDataTest extends AndroidTestCase {
+    @SuppressWarnings("unused")
+    private static final String TAG = "LocalDataTest";
+    private static final long DEFAULT_TIMEOUT = 1000; // one second
+
+    @MediumTest
+    public void testLocalAlbum() throws Exception {
+        new TestZeroImage().run();
+        new TestOneImage().run();
+        new TestMoreImages().run();
+        new TestZeroVideo().run();
+        new TestOneVideo().run();
+        new TestMoreVideos().run();
+        new TestDeleteOneImage().run();
+        new TestDeleteOneAlbum().run();
+    }
+
+    abstract class TestLocalAlbumBase {
+        private boolean mIsImage;
+        protected GalleryAppStub mApp;
+        protected LocalAlbumSet mAlbumSet;
+
+        TestLocalAlbumBase(boolean isImage) {
+            mIsImage = isImage;
+        }
+
+        public void run() throws Exception {
+            SQLiteDatabase db = SQLiteDatabase.create(null);
+            prepareData(db);
+            mApp = newGalleryContext(db, Looper.getMainLooper());
+            Path.clearAll();
+            Path path = Path.fromString(
+                    mIsImage ? "/local/image" : "/local/video");
+            mAlbumSet = new LocalAlbumSet(path, mApp);
+            mAlbumSet.reload();
+            verifyResult();
+        }
+
+        abstract void prepareData(SQLiteDatabase db);
+        abstract void verifyResult() throws Exception;
+    }
+
+    abstract class TestLocalImageAlbum extends TestLocalAlbumBase {
+        TestLocalImageAlbum() {
+            super(true);
+        }
+    }
+
+    abstract class TestLocalVideoAlbum extends TestLocalAlbumBase {
+        TestLocalVideoAlbum() {
+            super(false);
+        }
+    }
+
+    class TestZeroImage extends TestLocalImageAlbum {
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            createImageTable(db);
+        }
+
+        @Override
+        public void verifyResult() {
+            assertEquals(0, mAlbumSet.getMediaItemCount());
+            assertEquals(0, mAlbumSet.getSubMediaSetCount());
+            assertEquals(0, mAlbumSet.getTotalMediaItemCount());
+         }
+    }
+
+    class TestOneImage extends TestLocalImageAlbum {
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            createImageTable(db);
+            insertImageData(db);
+        }
+
+        @Override
+        public void verifyResult() {
+            assertEquals(0, mAlbumSet.getMediaItemCount());
+            assertEquals(1, mAlbumSet.getSubMediaSetCount());
+            assertEquals(1, mAlbumSet.getTotalMediaItemCount());
+            MediaSet sub = mAlbumSet.getSubMediaSet(0);
+            assertEquals(1, sub.getMediaItemCount());
+            assertEquals(0, sub.getSubMediaSetCount());
+            LocalMediaItem item = (LocalMediaItem) sub.getMediaItem(0, 1).get(0);
+            assertEquals(1, item.id);
+            assertEquals("IMG_0072", item.caption);
+            assertEquals("image/jpeg", item.mimeType);
+            assertEquals(12.0, item.latitude);
+            assertEquals(34.0, item.longitude);
+            assertEquals(0xD000, item.dateTakenInMs);
+            assertEquals(1280395646L, item.dateAddedInSec);
+            assertEquals(1275934796L, item.dateModifiedInSec);
+            assertEquals("/mnt/sdcard/DCIM/100CANON/IMG_0072.JPG", item.filePath);
+        }
+    }
+
+    class TestMoreImages extends TestLocalImageAlbum {
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            // Albums are sorted by names, and items are sorted by
+            // dateTimeTaken (descending)
+            createImageTable(db);
+            // bucket 0xB000
+            insertImageData(db, 1000, 0xB000, "second");  // id 1
+            insertImageData(db, 2000, 0xB000, "second");  // id 2
+            // bucket 0xB001
+            insertImageData(db, 3000, 0xB001, "first");   // id 3
+        }
+
+        @Override
+        public void verifyResult() {
+            assertEquals(0, mAlbumSet.getMediaItemCount());
+            assertEquals(2, mAlbumSet.getSubMediaSetCount());
+            assertEquals(3, mAlbumSet.getTotalMediaItemCount());
+
+            MediaSet first = mAlbumSet.getSubMediaSet(0);
+            assertEquals(1, first.getMediaItemCount());
+            LocalMediaItem item = (LocalMediaItem) first.getMediaItem(0, 1).get(0);
+            assertEquals(3, item.id);
+            assertEquals(3000L, item.dateTakenInMs);
+
+            MediaSet second = mAlbumSet.getSubMediaSet(1);
+            assertEquals(2, second.getMediaItemCount());
+            item = (LocalMediaItem) second.getMediaItem(0, 1).get(0);
+            assertEquals(2, item.id);
+            assertEquals(2000L, item.dateTakenInMs);
+            item = (LocalMediaItem) second.getMediaItem(1, 1).get(0);
+            assertEquals(1, item.id);
+            assertEquals(1000L, item.dateTakenInMs);
+        }
+    }
+
+    class OnContentDirtyLatch implements ContentListener {
+        private CountDownLatch mLatch = new CountDownLatch(1);
+
+        public void onContentDirty() {
+            mLatch.countDown();
+        }
+
+        public boolean isOnContentDirtyBeCalled(long timeout)
+                throws InterruptedException {
+            return mLatch.await(timeout, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    class TestDeleteOneAlbum extends TestLocalImageAlbum {
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            // Albums are sorted by names, and items are sorted by
+            // dateTimeTaken (descending)
+            createImageTable(db);
+            // bucket 0xB000
+            insertImageData(db, 1000, 0xB000, "second");  // id 1
+            insertImageData(db, 2000, 0xB000, "second");  // id 2
+            // bucket 0xB001
+            insertImageData(db, 3000, 0xB001, "first");   // id 3
+        }
+
+        @Override
+        public void verifyResult() throws Exception {
+            MediaSet sub = mAlbumSet.getSubMediaSet(1);  // "second"
+            assertEquals(2, mAlbumSet.getSubMediaSetCount());
+            OnContentDirtyLatch latch = new OnContentDirtyLatch();
+            sub.addContentListener(latch);
+            assertTrue((sub.getSupportedOperations() & MediaSet.SUPPORT_DELETE) != 0);
+            sub.delete();
+            mAlbumSet.fakeChange();
+            latch.isOnContentDirtyBeCalled(DEFAULT_TIMEOUT);
+            mAlbumSet.reload();
+            assertEquals(1, mAlbumSet.getSubMediaSetCount());
+        }
+    }
+
+    class TestDeleteOneImage extends TestLocalImageAlbum {
+
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            createImageTable(db);
+            insertImageData(db);
+        }
+
+        @Override
+        public void verifyResult() {
+            MediaSet sub = mAlbumSet.getSubMediaSet(0);
+            LocalMediaItem item = (LocalMediaItem) sub.getMediaItem(0, 1).get(0);
+            assertEquals(1, sub.getMediaItemCount());
+            assertTrue((sub.getSupportedOperations() & MediaSet.SUPPORT_DELETE) != 0);
+            sub.delete();
+            sub.reload();
+            assertEquals(0, sub.getMediaItemCount());
+        }
+    }
+
+    static void createImageTable(SQLiteDatabase db) {
+        // This is copied from MediaProvider
+        db.execSQL("CREATE TABLE IF NOT EXISTS images (" +
+                "_id INTEGER PRIMARY KEY," +
+                "_data TEXT," +
+                "_size INTEGER," +
+                "_display_name TEXT," +
+                "mime_type TEXT," +
+                "title TEXT," +
+                "date_added INTEGER," +
+                "date_modified INTEGER," +
+                "description TEXT," +
+                "picasa_id TEXT," +
+                "isprivate INTEGER," +
+                "latitude DOUBLE," +
+                "longitude DOUBLE," +
+                "datetaken INTEGER," +
+                "orientation INTEGER," +
+                "mini_thumb_magic INTEGER," +
+                "bucket_id TEXT," +
+                "bucket_display_name TEXT" +
+               ");");
+    }
+
+    static void insertImageData(SQLiteDatabase db) {
+        insertImageData(db, 0xD000, 0xB000, "name");
+    }
+
+    static void insertImageData(SQLiteDatabase db, long dateTaken,
+            int bucketId, String bucketName) {
+        db.execSQL("INSERT INTO images (title, mime_type, latitude, longitude, "
+                + "datetaken, date_added, date_modified, bucket_id, "
+                + "bucket_display_name, _data, orientation) "
+                + "VALUES ('IMG_0072', 'image/jpeg', 12, 34, "
+                + dateTaken + ", 1280395646, 1275934796, '" + bucketId + "', "
+                + "'" + bucketName + "', "
+                + "'/mnt/sdcard/DCIM/100CANON/IMG_0072.JPG', 0)");
+    }
+
+    class TestZeroVideo extends TestLocalVideoAlbum {
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            createVideoTable(db);
+        }
+
+        @Override
+        public void verifyResult() {
+            assertEquals(0, mAlbumSet.getMediaItemCount());
+            assertEquals(0, mAlbumSet.getSubMediaSetCount());
+            assertEquals(0, mAlbumSet.getTotalMediaItemCount());
+        }
+    }
+
+    class TestOneVideo extends TestLocalVideoAlbum {
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            createVideoTable(db);
+            insertVideoData(db);
+        }
+
+        @Override
+        public void verifyResult() {
+            assertEquals(0, mAlbumSet.getMediaItemCount());
+            assertEquals(1, mAlbumSet.getSubMediaSetCount());
+            assertEquals(1, mAlbumSet.getTotalMediaItemCount());
+            MediaSet sub = mAlbumSet.getSubMediaSet(0);
+            assertEquals(1, sub.getMediaItemCount());
+            assertEquals(0, sub.getSubMediaSetCount());
+            LocalMediaItem item = (LocalMediaItem) sub.getMediaItem(0, 1).get(0);
+            assertEquals(1, item.id);
+            assertEquals("VID_20100811_051413", item.caption);
+            assertEquals("video/mp4", item.mimeType);
+            assertEquals(11.0, item.latitude);
+            assertEquals(22.0, item.longitude);
+            assertEquals(0xD000, item.dateTakenInMs);
+            assertEquals(1281503663L, item.dateAddedInSec);
+            assertEquals(1281503662L, item.dateModifiedInSec);
+            assertEquals("/mnt/sdcard/DCIM/Camera/VID_20100811_051413.3gp",
+                    item.filePath);
+        }
+    }
+
+    class TestMoreVideos extends TestLocalVideoAlbum {
+        @Override
+        public void prepareData(SQLiteDatabase db) {
+            // Albums are sorted by names, and items are sorted by
+            // dateTimeTaken (descending)
+            createVideoTable(db);
+            // bucket 0xB002
+            insertVideoData(db, 1000, 0xB000, "second");  // id 1
+            insertVideoData(db, 2000, 0xB000, "second");  // id 2
+            // bucket 0xB001
+            insertVideoData(db, 3000, 0xB001, "first");   // id 3
+        }
+
+        @Override
+        public void verifyResult() {
+            assertEquals(0, mAlbumSet.getMediaItemCount());
+            assertEquals(2, mAlbumSet.getSubMediaSetCount());
+            assertEquals(3, mAlbumSet.getTotalMediaItemCount());
+
+            MediaSet first = mAlbumSet.getSubMediaSet(0);
+            assertEquals(1, first.getMediaItemCount());
+            LocalMediaItem item = (LocalMediaItem) first.getMediaItem(0, 1).get(0);
+            assertEquals(3, item.id);
+            assertEquals(3000L, item.dateTakenInMs);
+
+            MediaSet second = mAlbumSet.getSubMediaSet(1);
+            assertEquals(2, second.getMediaItemCount());
+            item = (LocalMediaItem) second.getMediaItem(0, 1).get(0);
+            assertEquals(2, item.id);
+            assertEquals(2000L, item.dateTakenInMs);
+            item = (LocalMediaItem) second.getMediaItem(1, 1).get(0);
+            assertEquals(1, item.id);
+            assertEquals(1000L, item.dateTakenInMs);
+        }
+    }
+
+    static void createVideoTable(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE IF NOT EXISTS video (" +
+                   "_id INTEGER PRIMARY KEY," +
+                   "_data TEXT NOT NULL," +
+                   "_display_name TEXT," +
+                   "_size INTEGER," +
+                   "mime_type TEXT," +
+                   "date_added INTEGER," +
+                   "date_modified INTEGER," +
+                   "title TEXT," +
+                   "duration INTEGER," +
+                   "artist TEXT," +
+                   "album TEXT," +
+                   "resolution TEXT," +
+                   "description TEXT," +
+                   "isprivate INTEGER," +   // for YouTube videos
+                   "tags TEXT," +           // for YouTube videos
+                   "category TEXT," +       // for YouTube videos
+                   "language TEXT," +       // for YouTube videos
+                   "mini_thumb_data TEXT," +
+                   "latitude DOUBLE," +
+                   "longitude DOUBLE," +
+                   "datetaken INTEGER," +
+                   "mini_thumb_magic INTEGER" +
+                   ");");
+        db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;");
+        db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT");
+    }
+
+    static void insertVideoData(SQLiteDatabase db) {
+        insertVideoData(db, 0xD000, 0xB000, "name");
+    }
+
+    static void insertVideoData(SQLiteDatabase db, long dateTaken,
+            int bucketId, String bucketName) {
+        db.execSQL("INSERT INTO video (title, mime_type, latitude, longitude, "
+                + "datetaken, date_added, date_modified, bucket_id, "
+                + "bucket_display_name, _data, duration) "
+                + "VALUES ('VID_20100811_051413', 'video/mp4', 11, 22, "
+                + dateTaken + ", 1281503663, 1281503662, '" + bucketId + "', "
+                + "'" + bucketName + "', "
+                + "'/mnt/sdcard/DCIM/Camera/VID_20100811_051413.3gp', 2964)");
+    }
+
+    static GalleryAppStub newGalleryContext(SQLiteDatabase db, Looper mainLooper) {
+        MockContentResolver cr = new MockContentResolver();
+        ContentProvider cp = new DbContentProvider(db, cr);
+        cr.addProvider("media", cp);
+        return new GalleryAppMock(null, cr, mainLooper);
+    }
+}
+
+class DbContentProvider extends MockContentProvider {
+    private static final String TAG = "DbContentProvider";
+    private SQLiteDatabase mDatabase;
+    private ContentResolver mContentResolver;
+
+    DbContentProvider(SQLiteDatabase db, ContentResolver cr) {
+        mDatabase = db;
+        mContentResolver = cr;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        // This is a simplified version extracted from MediaProvider.
+
+        String tableName = getTableName(uri);
+        if (tableName == null) return null;
+
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(tableName);
+
+        String groupBy = null;
+        String limit = uri.getQueryParameter("limit");
+
+        if (uri.getQueryParameter("distinct") != null) {
+            qb.setDistinct(true);
+        }
+
+        Log.v(TAG, "query = " + qb.buildQuery(projection, selection,
+                selectionArgs, groupBy, null, sortOrder, limit));
+
+        if (selectionArgs != null) {
+            for (String s : selectionArgs) {
+                Log.v(TAG, "  selectionArgs = " + s);
+            }
+        }
+
+        Cursor c = qb.query(mDatabase, projection, selection,
+                selectionArgs, groupBy, null, sortOrder, limit);
+
+        return c;
+    }
+
+    @Override
+    public int delete(Uri uri, String whereClause, String[] whereArgs) {
+        Log.v(TAG, "delete " + uri + "," + whereClause + "," + whereArgs[0]);
+        String tableName = getTableName(uri);
+        if (tableName == null) return 0;
+        int count = mDatabase.delete(tableName, whereClause, whereArgs);
+        mContentResolver.notifyChange(uri, null);
+        return count;
+    }
+
+    private String getTableName(Uri uri) {
+        String uriString = uri.toString();
+        if (uriString.startsWith("content://media/external/images/media")) {
+            return "images";
+        } else if (uriString.startsWith("content://media/external/video/media")) {
+            return "video";
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/data/MediaSetTest.java b/tests/src/com/android/gallery3d/data/MediaSetTest.java
new file mode 100644
index 0000000..33dfe96
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/MediaSetTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+public class MediaSetTest extends AndroidTestCase {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MediaSetTest";
+
+    @SmallTest
+    public void testComboAlbumSet() {
+        GalleryApp app = new GalleryAppMock(null, null, null);
+        Path.clearAll();
+        DataManager dataManager = app.getDataManager();
+
+        dataManager.addSource(new ComboSource(app));
+        dataManager.addSource(new MockSource(app));
+
+        MockSet set00 = new MockSet(Path.fromString("/mock/00"), dataManager, 0, 2000);
+        MockSet set01 = new MockSet(Path.fromString("/mock/01"), dataManager, 1, 3000);
+        MockSet set10 = new MockSet(Path.fromString("/mock/10"), dataManager, 2, 4000);
+        MockSet set11 = new MockSet(Path.fromString("/mock/11"), dataManager, 3, 5000);
+        MockSet set12 = new MockSet(Path.fromString("/mock/12"), dataManager, 4, 6000);
+
+        MockSet set0 = new MockSet(Path.fromString("/mock/0"), dataManager, 7, 7000);
+        set0.addMediaSet(set00);
+        set0.addMediaSet(set01);
+
+        MockSet set1 = new MockSet(Path.fromString("/mock/1"), dataManager, 8, 8000);
+        set1.addMediaSet(set10);
+        set1.addMediaSet(set11);
+        set1.addMediaSet(set12);
+
+        MediaSet combo = dataManager.getMediaSet("/combo/{/mock/0,/mock/1}");
+        assertEquals(5, combo.getSubMediaSetCount());
+        assertEquals(0, combo.getMediaItemCount());
+        assertEquals("/mock/00", combo.getSubMediaSet(0).getPath().toString());
+        assertEquals("/mock/01", combo.getSubMediaSet(1).getPath().toString());
+        assertEquals("/mock/10", combo.getSubMediaSet(2).getPath().toString());
+        assertEquals("/mock/11", combo.getSubMediaSet(3).getPath().toString());
+        assertEquals("/mock/12", combo.getSubMediaSet(4).getPath().toString());
+
+        assertEquals(10, combo.getTotalMediaItemCount());
+    }
+}
diff --git a/tests/src/com/android/gallery3d/data/MockItem.java b/tests/src/com/android/gallery3d/data/MockItem.java
new file mode 100644
index 0000000..2901979
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/MockItem.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.util.ThreadPool.Job;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+public class MockItem extends MediaItem {
+    public MockItem(Path path) {
+        super(path, nextVersionNumber());
+    }
+
+    @Override
+    public Job<Bitmap> requestImage(int type) {
+        return null;
+    }
+
+    @Override
+    public Job<BitmapRegionDecoder> requestLargeImage() {
+        return null;
+    }
+
+    @Override
+    public String getMimeType() {
+        return null;
+    }
+
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/data/MockSet.java b/tests/src/com/android/gallery3d/data/MockSet.java
new file mode 100644
index 0000000..fa83c79
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/MockSet.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import java.util.ArrayList;
+
+public class MockSet extends MediaSet {
+    ArrayList<MediaItem> mItems = new ArrayList<MediaItem>();
+    ArrayList<MediaSet> mSets = new ArrayList<MediaSet>();
+    Path mItemPath;
+
+    public MockSet(Path path, DataManager dataManager) {
+        super(path, nextVersionNumber());
+        mItemPath = Path.fromString("/mock/item");
+    }
+
+    public MockSet(Path path, DataManager dataManager,
+            int items, int item_id_start) {
+        this(path, dataManager);
+        for (int i = 0; i < items; i++) {
+            Path childPath = mItemPath.getChild(item_id_start + i);
+            mItems.add(new MockItem(childPath));
+        }
+    }
+
+    public void addMediaSet(MediaSet sub) {
+        mSets.add(sub);
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return mItems.size();
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+        int end = Math.min(start + count, mItems.size());
+
+        for (int i = start; i < end; i++) {
+            result.add(mItems.get(i));
+        }
+        return result;
+    }
+
+    @Override
+    public int getSubMediaSetCount() {
+        return mSets.size();
+    }
+
+    @Override
+    public MediaSet getSubMediaSet(int index) {
+        return mSets.get(index);
+    }
+
+    @Override
+    public int getTotalMediaItemCount() {
+        int result = mItems.size();
+        for (MediaSet s : mSets) {
+            result += s.getTotalMediaItemCount();
+        }
+        return result;
+    }
+
+    @Override
+    public String getName() {
+        return "Set " + mPath;
+    }
+
+    @Override
+    public long reload() {
+        return 0;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/data/MockSource.java b/tests/src/com/android/gallery3d/data/MockSource.java
new file mode 100644
index 0000000..27ed4d0
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/MockSource.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+class MockSource extends MediaSource {
+    GalleryApp mApplication;
+    PathMatcher mMatcher;
+
+    private static final int MOCK_SET = 0;
+    private static final int MOCK_ITEM = 1;
+
+    public MockSource(GalleryApp context) {
+        super("mock");
+        mApplication = context;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/mock/*", MOCK_SET);
+        mMatcher.add("/mock/item/*", MOCK_ITEM);
+    }
+
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        MediaObject obj;
+        switch (mMatcher.match(path)) {
+            case MOCK_SET:
+                return new MockSet(path, mApplication.getDataManager());
+            case MOCK_ITEM:
+                return new MockItem(path);
+            default:
+                throw new RuntimeException("bad path: " + path);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/data/PathTest.java b/tests/src/com/android/gallery3d/data/PathTest.java
new file mode 100644
index 0000000..b43d109
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/PathTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+public class PathTest extends AndroidTestCase {
+    @SuppressWarnings("unused")
+    private static final String TAG = "PathTest";
+
+    @SmallTest
+    public void testToString() {
+        Path p = Path.fromString("/hello/world");
+        assertEquals("/hello/world", p.toString());
+
+        p = Path.fromString("/a");
+        assertEquals("/a", p.toString());
+
+        p = Path.fromString("");
+        assertEquals("", p.toString());
+    }
+
+    @SmallTest
+    public void testSplit() {
+        Path p = Path.fromString("/hello/world");
+        String[] s = p.split();
+        assertEquals(2, s.length);
+        assertEquals("hello", s[0]);
+        assertEquals("world", s[1]);
+
+        p = Path.fromString("");
+        assertEquals(0, p.split().length);
+    }
+
+    @SmallTest
+    public void testPrefix() {
+        Path p = Path.fromString("/hello/world");
+        assertEquals("hello", p.getPrefix());
+
+        p = Path.fromString("");
+        assertEquals("", p.getPrefix());
+    }
+
+    @SmallTest
+    public void testGetChild() {
+        Path p = Path.fromString("/hello");
+        Path q = Path.fromString("/hello/world");
+        assertSame(q, p.getChild("world"));
+        Path r = q.getChild(17);
+        assertEquals("/hello/world/17", r.toString());
+    }
+
+    @SmallTest
+    public void testSplitSequence() {
+        String[] s = Path.splitSequence("{a,bb,ccc}");
+        assertEquals(3, s.length);
+        assertEquals("a", s[0]);
+        assertEquals("bb", s[1]);
+        assertEquals("ccc", s[2]);
+
+        s = Path.splitSequence("{a,{bb,ccc},d}");
+        assertEquals(3, s.length);
+        assertEquals("a", s[0]);
+        assertEquals("{bb,ccc}", s[1]);
+        assertEquals("d", s[2]);
+    }
+}
diff --git a/tests/src/com/android/gallery3d/data/RealDataTest.java b/tests/src/com/android/gallery3d/data/RealDataTest.java
new file mode 100644
index 0000000..526cfe3
--- /dev/null
+++ b/tests/src/com/android/gallery3d/data/RealDataTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import android.os.Looper;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+// This test reads real data directly and dump information out in the log.
+public class RealDataTest extends AndroidTestCase {
+    private static final String TAG = "RealDataTest";
+
+    private HashSet<Path> mUsedId = new HashSet<Path>();
+    private GalleryApp mApplication;
+    private DataManager mDataManager;
+
+    @LargeTest
+    public void testRealData() {
+        mUsedId.clear();
+        mApplication = new GalleryAppMock(
+                mContext,
+                mContext.getContentResolver(),
+                Looper.myLooper());
+        mDataManager = mApplication.getDataManager();
+        mDataManager.addSource(new LocalSource(mApplication));
+        mDataManager.addSource(new PicasaSource(mApplication));
+        new TestLocalImage().run();
+        new TestLocalVideo().run();
+        new TestPicasa().run();
+    }
+
+    class TestLocalImage {
+        public void run() {
+            MediaSet set = mDataManager.getMediaSet("/local/image");
+            set.reload();
+            Log.v(TAG, "LocalAlbumSet (Image)");
+            dumpMediaSet(set, "");
+        }
+    }
+
+    class TestLocalVideo {
+        public void run() {
+            MediaSet set = mDataManager.getMediaSet("/local/video");
+            set.reload();
+            Log.v(TAG, "LocalAlbumSet (Video)");
+            dumpMediaSet(set, "");
+        }
+    }
+
+    class TestPicasa implements Runnable {
+        public void run() {
+            MediaSet set = mDataManager.getMediaSet("/picasa");
+            set.reload();
+            Log.v(TAG, "PicasaAlbumSet");
+            dumpMediaSet(set, "");
+        }
+    }
+
+    void dumpMediaSet(MediaSet set, String prefix) {
+        Log.v(TAG, "getName() = " + set.getName());
+        Log.v(TAG, "getPath() = " + set.getPath());
+        Log.v(TAG, "getMediaItemCount() = " + set.getMediaItemCount());
+        Log.v(TAG, "getSubMediaSetCount() = " + set.getSubMediaSetCount());
+        Log.v(TAG, "getTotalMediaItemCount() = " + set.getTotalMediaItemCount());
+        assertNewId(set.getPath());
+        for (int i = 0, n = set.getSubMediaSetCount(); i < n; i++) {
+            MediaSet sub = set.getSubMediaSet(i);
+            Log.v(TAG, prefix + "got set " + i);
+            dumpMediaSet(sub, prefix + "  ");
+        }
+        for (int i = 0, n = set.getMediaItemCount(); i < n; i += 10) {
+            ArrayList<MediaItem> list = set.getMediaItem(i, 10);
+            Log.v(TAG, prefix + "got item " + i + " (+" + list.size() + ")");
+            for (MediaItem item : list) {
+                dumpMediaItem(item, prefix + "..");
+            }
+        }
+    }
+
+    void dumpMediaItem(MediaItem item, String prefix) {
+        assertNewId(item.getPath());
+        Log.v(TAG, prefix + "getPath() = " + item.getPath());
+    }
+
+    void assertNewId(Path key) {
+        assertFalse(key + " has already appeared.", mUsedId.contains(key));
+        mUsedId.add(key);
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifDataTest.java b/tests/src/com/android/gallery3d/exif/ExifDataTest.java
new file mode 100644
index 0000000..142cc6b
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifDataTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import junit.framework.TestCase;
+import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ExifDataTest extends TestCase {
+    Map<Integer, ExifTag> mTestTags;
+    ExifInterface mInterface;
+    private ExifTag mVersionTag;
+    private ExifTag mGpsVersionTag;
+    private ExifTag mModelTag;
+    private ExifTag mDateTimeTag;
+    private ExifTag mCompressionTag;
+    private ExifTag mThumbnailFormatTag;
+    private ExifTag mLongitudeTag;
+    private ExifTag mShutterTag;
+    private ExifTag mInteropIndex;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mInterface = new ExifInterface();
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                5, 4, 3, 2
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                6, 7, 8, 9
+        });
+        // TYPE ASCII with arbitrary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "helloworld");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2013:02:11 20:20:20");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
+        // TYPE_UNSIGNED_LONG with 1 components
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(2, 2), new Rational(11, 11),
+                new Rational(102, 102)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(4, 6));
+        // TYPE_ASCII with arbitrary length
+        mInteropIndex = mInterface.buildTag(ExifInterface.TAG_INTEROPERABILITY_INDEX, "foo");
+
+        mTestTags = new HashMap<Integer, ExifTag>();
+
+        mTestTags.put(ExifInterface.TAG_EXIF_VERSION, mVersionTag);
+        mTestTags.put(ExifInterface.TAG_GPS_VERSION_ID, mGpsVersionTag);
+        mTestTags.put(ExifInterface.TAG_MODEL, mModelTag);
+        mTestTags.put(ExifInterface.TAG_DATE_TIME, mDateTimeTag);
+        mTestTags.put(ExifInterface.TAG_COMPRESSION, mCompressionTag);
+        mTestTags.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, mThumbnailFormatTag);
+        mTestTags.put(ExifInterface.TAG_GPS_LONGITUDE, mLongitudeTag);
+        mTestTags.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, mShutterTag);
+        mTestTags.put(ExifInterface.TAG_INTEROPERABILITY_INDEX, mInteropIndex);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mInterface = null;
+        mTestTags = null;
+    }
+
+    @SmallTest
+    public void testAddTag() {
+        ExifData exifData = new ExifData(ByteOrder.BIG_ENDIAN);
+
+        // Add all test tags
+        for (ExifTag t : mTestTags.values()) {
+            assertTrue(exifData.addTag(t) == null);
+        }
+
+        // Make sure no initial thumbnails
+        assertFalse(exifData.hasCompressedThumbnail());
+        assertFalse(exifData.hasUncompressedStrip());
+
+        // Check that we can set thumbnails
+        exifData.setStripBytes(3, new byte[] {
+                1, 2, 3, 4, 5
+        });
+        assertTrue(exifData.hasUncompressedStrip());
+        exifData.setCompressedThumbnail(new byte[] {
+            1
+        });
+        assertTrue(exifData.hasCompressedThumbnail());
+
+        // Check that we can clear thumbnails
+        exifData.clearThumbnailAndStrips();
+        assertFalse(exifData.hasCompressedThumbnail());
+        assertFalse(exifData.hasUncompressedStrip());
+
+        // Make sure ifds exist
+        for (int i : IfdData.getIfds()) {
+            assertTrue(exifData.getIfdData(i) != null);
+        }
+
+        // Get all test tags
+        List<ExifTag> allTags = exifData.getAllTags();
+        assertTrue(allTags != null);
+
+        // Make sure all test tags are in data
+        for (ExifTag t : mTestTags.values()) {
+            boolean check = false;
+            for (ExifTag i : allTags) {
+                if (t.equals(i)) {
+                    check = true;
+                    break;
+                }
+            }
+            assertTrue(check);
+        }
+
+        // Check if getting tags for a tid works
+        List<ExifTag> tidTags = exifData.getAllTagsForTagId(ExifInterface
+                .getTrueTagKey(ExifInterface.TAG_SHUTTER_SPEED_VALUE));
+        assertTrue(tidTags.size() == 1);
+        assertTrue(tidTags.get(0).equals(mShutterTag));
+
+        // Check if getting tags for an ifd works
+        List<ExifTag> ifdTags = exifData.getAllTagsForIfd(IfdId.TYPE_IFD_INTEROPERABILITY);
+        assertTrue(ifdTags.size() == 1);
+        assertTrue(ifdTags.get(0).equals(mInteropIndex));
+
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifInterfaceTest.java b/tests/src/com/android/gallery3d/exif/ExifInterfaceTest.java
new file mode 100644
index 0000000..01b2a32
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifInterfaceTest.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.exif;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+import java.io.ByteArrayInputStream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ExifInterfaceTest extends ExifXmlDataTestCase {
+
+    private File mTmpFile;
+    private List<Map<Short, List<String>>> mGroundTruth;
+    private ExifInterface mInterface;
+    private ExifTag mVersionTag;
+    private ExifTag mGpsVersionTag;
+    private ExifTag mModelTag;
+    private ExifTag mDateTimeTag;
+    private ExifTag mCompressionTag;
+    private ExifTag mThumbnailFormatTag;
+    private ExifTag mLongitudeTag;
+    private ExifTag mShutterTag;
+    Map<Integer, ExifTag> mTestTags;
+    Map<Integer, Integer> mTagDefinitions;
+
+    public ExifInterfaceTest(int imageRes, int xmlRes) {
+        super(imageRes, xmlRes);
+    }
+
+    public ExifInterfaceTest(String imagePath, String xmlPath) {
+        super(imagePath, xmlPath);
+    }
+
+    @MediumTest
+    public void testInterface() throws Exception {
+
+        InputStream imageInputStream = null;
+        try {
+            // Basic checks
+
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // Check defines
+            int tag = ExifInterface.defineTag(1, (short) 0x0100);
+            assertTrue(getImageTitle(), tag == 0x00010100);
+            int tagDef = mInterface.getTagDefinition((short) 0x0100, IfdId.TYPE_IFD_0);
+            assertTrue(getImageTitle(), tagDef == 0x03040001);
+            int[] allowed = ExifInterface.getAllowedIfdsFromInfo(mInterface.getTagInfo().get(
+                    ExifInterface.TAG_IMAGE_WIDTH));
+            assertTrue(getImageTitle(), allowed.length == 2 && allowed[0] == IfdId.TYPE_IFD_0
+                    && allowed[1] == IfdId.TYPE_IFD_1);
+
+            // Check if there are any initial tags
+            assertTrue(getImageTitle(), mInterface.getAllTags() == null);
+
+            // ///////// Basic read/write testing
+
+            // Make sure we can read
+            imageInputStream = new ByteArrayInputStream(imgData);
+            mInterface.readExif(imageInputStream);
+
+            // Check tags against ground truth
+            checkTagsAgainstXml(mInterface.getAllTags());
+
+            // Make sure clearing Exif works
+            mInterface.clearExif();
+            assertTrue(getImageTitle(), mInterface.getAllTags() == null);
+
+            // Make sure setting tags works
+            mInterface.setTags(mTestTags.values());
+            checkTagsAgainstHash(mInterface.getAllTags(), mTestTags);
+
+            // Try writing over bitmap exif
+            ByteArrayOutputStream imgModified = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData, imgModified);
+
+            // Check if bitmap is valid
+            byte[] imgData2 = imgModified.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+            // Make sure we get the same tags out
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            mInterface.readExif(imageInputStream);
+            checkTagsAgainstHash(mInterface.getAllTags(), mTestTags);
+
+            // Reread original image
+            imageInputStream = new ByteArrayInputStream(imgData);
+            mInterface.readExif(imageInputStream);
+
+            // Write out with original exif
+            imgModified = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData2, imgModified);
+
+            // Read back in exif and check tags
+            imgData2 = imgModified.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            mInterface.readExif(imageInputStream);
+            checkTagsAgainstXml(mInterface.getAllTags());
+
+            // Check if bitmap is valid
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    @MediumTest
+    public void testInterfaceModify() throws Exception {
+
+        // TODO: This test is dependent on galaxy_nexus jpeg/xml file.
+        InputStream imageInputStream = null;
+        try {
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // ///////// Exif modifier testing.
+
+            // Read exif and write to temp file
+            imageInputStream = new ByteArrayInputStream(imgData);
+            mInterface.readExif(imageInputStream);
+            mInterface.writeExif(imgData, mTmpFile.getPath());
+
+            // Check if bitmap is valid
+            imageInputStream = new FileInputStream(mTmpFile);
+            checkBitmap(imageInputStream);
+
+            // Create some tags to overwrite with
+            ArrayList<ExifTag> tags = new ArrayList<ExifTag>();
+            tags.add(mInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.Orientation.RIGHT_TOP));
+            tags.add(mInterface.buildTag(ExifInterface.TAG_USER_COMMENT, "goooooooooooooooooogle"));
+
+            // Attempt to rewrite tags
+            assertTrue(getImageTitle(), mInterface.rewriteExif(mTmpFile.getPath(), tags));
+
+            imageInputStream.close();
+            // Check if bitmap is valid
+            imageInputStream = new FileInputStream(mTmpFile);
+            checkBitmap(imageInputStream);
+
+            // Read tags and check against xml
+            mInterface.readExif(mTmpFile.getPath());
+            for (ExifTag t : mInterface.getAllTags()) {
+                short tid = t.getTagId();
+                if (tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_ORIENTATION)
+                        && tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT)) {
+                    checkTagAgainstXml(t);
+                }
+            }
+            assertTrue(getImageTitle(), mInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION)
+                    .shortValue() == ExifInterface.Orientation.RIGHT_TOP);
+            String valString = mInterface.getTagStringValue(ExifInterface.TAG_USER_COMMENT);
+            assertTrue(getImageTitle(), valString.equals("goooooooooooooooooogle"));
+
+            // Test forced modify
+
+            // Create some tags to overwrite with
+            tags = new ArrayList<ExifTag>();
+            tags.add(mInterface.buildTag(ExifInterface.TAG_SOFTWARE, "magic super photomaker pro"));
+            tags.add(mInterface.buildTag(ExifInterface.TAG_USER_COMMENT, "noodles"));
+            tags.add(mInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.Orientation.TOP_LEFT));
+
+            // Force rewrite tags
+            mInterface.forceRewriteExif(mTmpFile.getPath(), tags);
+
+            imageInputStream.close();
+            // Check if bitmap is valid
+            imageInputStream = new FileInputStream(mTmpFile);
+            checkBitmap(imageInputStream);
+
+            // Read tags and check against xml
+            mInterface.readExif(mTmpFile.getPath());
+            for (ExifTag t : mInterface.getAllTags()) {
+                short tid = t.getTagId();
+                if (!ExifInterface.isOffsetTag(tid)
+                        && tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_SOFTWARE)
+                        && tid != ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT)) {
+                    checkTagAgainstXml(t);
+                }
+            }
+            valString = mInterface.getTagStringValue(ExifInterface.TAG_SOFTWARE);
+            String compareString = "magic super photomaker pro\0";
+            assertTrue(getImageTitle(), valString.equals(compareString));
+            valString = mInterface.getTagStringValue(ExifInterface.TAG_USER_COMMENT);
+            assertTrue(getImageTitle(), valString.equals("noodles"));
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    @MediumTest
+    public void testInterfaceDefines() throws Exception {
+
+        InputStream imageInputStream = null;
+        try {
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // Set some tags.
+            mInterface.setTags(mTestTags.values());
+
+            // Check tag definitions against default
+            for (Integer i : mTestTags.keySet()) {
+                int check = mTagDefinitions.get(i).intValue();
+                int actual = mInterface.getTagInfo().get(i);
+                assertTrue(check == actual);
+            }
+
+            // Check defines
+            int tag1 = ExifInterface.defineTag(IfdId.TYPE_IFD_1, (short) 42);
+            int tag2 = ExifInterface.defineTag(IfdId.TYPE_IFD_INTEROPERABILITY, (short) 43);
+            assertTrue(tag1 == 0x0001002a);
+            assertTrue(tag2 == 0x0003002b);
+
+            // Define some non-standard tags
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1,
+                    ExifTag.TYPE_UNSIGNED_BYTE, (short) 16, new int[] {
+                        IfdId.TYPE_IFD_1
+                    }) == tag1);
+            assertTrue(mInterface.getTagInfo().get(tag1) == 0x02010010);
+            assertTrue(mInterface.setTagDefinition((short) 43, IfdId.TYPE_IFD_INTEROPERABILITY,
+                    ExifTag.TYPE_ASCII, (short) 5, new int[] {
+                            IfdId.TYPE_IFD_GPS, IfdId.TYPE_IFD_INTEROPERABILITY
+                    }) == tag2);
+            assertTrue(mInterface.getTagInfo().get(tag2) == 0x18020005);
+
+            // Make sure these don't work
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1,
+                    ExifTag.TYPE_UNSIGNED_BYTE, (short) 16, new int[] {
+                        IfdId.TYPE_IFD_0
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1, (short) 0,
+                    (short) 16, new int[] {
+                        IfdId.TYPE_IFD_1
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 42, 5, ExifTag.TYPE_UNSIGNED_BYTE,
+                    (short) 16, new int[] {
+                        5
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 42, IfdId.TYPE_IFD_1,
+                    ExifTag.TYPE_UNSIGNED_BYTE, (short) 16, new int[] {
+                        -1
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 43, IfdId.TYPE_IFD_GPS,
+                    ExifTag.TYPE_ASCII, (short) 5, new int[] {
+                        IfdId.TYPE_IFD_GPS
+                    }) == ExifInterface.TAG_NULL);
+            assertTrue(mInterface.setTagDefinition((short) 43, IfdId.TYPE_IFD_0,
+                    ExifTag.TYPE_ASCII, (short) 5, new int[] {
+                            IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_GPS
+                    }) == ExifInterface.TAG_NULL);
+
+            // Set some tags
+            mInterface.setTags(mTestTags.values());
+            checkTagsAgainstHash(mInterface.getAllTags(), mTestTags);
+
+            // Make some tags using new defines
+            ExifTag defTag0 = mInterface.buildTag(tag1, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            });
+            assertTrue(defTag0 != null);
+            ExifTag defTag1 = mInterface.buildTag(tag2, "hihi");
+            assertTrue(defTag1 != null);
+            ExifTag defTag2 = mInterface.buildTag(tag2, IfdId.TYPE_IFD_GPS, "byte");
+            assertTrue(defTag2 != null);
+
+            // Make sure these don't work
+            ExifTag badTag = mInterface.buildTag(tag1, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
+            });
+            assertTrue(badTag == null);
+            badTag = mInterface.buildTag(tag1, IfdId.TYPE_IFD_0, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            });
+            assertTrue(badTag == null);
+            badTag = mInterface.buildTag(0x0002002a, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            });
+            assertTrue(badTag == null);
+            badTag = mInterface.buildTag(tag2, new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17
+            });
+            assertTrue(badTag == null);
+
+            // Set the tags
+            assertTrue(mInterface.setTag(defTag0) == null);
+            assertTrue(mInterface.setTag(defTag1) == null);
+            assertTrue(mInterface.setTag(defTag2) == null);
+            assertTrue(mInterface.setTag(defTag0).equals(defTag0));
+            assertTrue(mInterface.setTag(null) == null);
+            assertTrue(mInterface.setTagValue(tag2, "yoyo") == true);
+            assertTrue(mInterface.setTagValue(tag2, "yaaarggg") == false);
+            assertTrue(mInterface.getTagStringValue(tag2).equals("yoyo\0"));
+
+            // Try writing over bitmap exif
+            ByteArrayOutputStream imgModified = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData, imgModified);
+
+            // Check if bitmap is valid
+            byte[] imgData2 = imgModified.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+            // Read back in the tags
+            mInterface.readExif(imgData2);
+
+            // Check tags
+            for (ExifTag t : mInterface.getAllTags()) {
+                int tid = t.getTagId();
+                if (tid != ExifInterface.getTrueTagKey(tag1)
+                        && tid != ExifInterface.getTrueTagKey(tag2)) {
+                    checkTagAgainstHash(t, mTestTags);
+                }
+            }
+            assertTrue(Arrays.equals(mInterface.getTagByteValues(tag1), new byte[] {
+                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+            }));
+            assertTrue(mInterface.getTagStringValue(tag2).equals("yoyo\0"));
+            assertTrue(mInterface.getTagStringValue(tag2, IfdId.TYPE_IFD_GPS).equals("byte\0"));
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    @MediumTest
+    public void testInterfaceThumbnails() throws Exception {
+
+        InputStream imageInputStream = null;
+        try {
+            // Check if bitmap is valid
+            byte[] imgData = Util.readToByteArray(getImageInputStream());
+            imageInputStream = new ByteArrayInputStream(imgData);
+            checkBitmap(imageInputStream);
+
+            // Check thumbnails
+            mInterface.readExif(imgData);
+            Bitmap bmap = mInterface.getThumbnailBitmap();
+            assertTrue(getImageTitle(), bmap != null);
+
+            // Make a new thumbnail and set it
+            BitmapFactory.Options opts = new BitmapFactory.Options();
+            opts.inSampleSize = 16;
+            Bitmap thumb = BitmapFactory.decodeByteArray(imgData, 0, imgData.length, opts);
+            assertTrue(getImageTitle(), thumb != null);
+            assertTrue(getImageTitle(), mInterface.setCompressedThumbnail(thumb) == true);
+
+            // Write out image
+            ByteArrayOutputStream outData = new ByteArrayOutputStream();
+            mInterface.writeExif(imgData, outData);
+
+            // Make sure bitmap is still valid
+            byte[] imgData2 = outData.toByteArray();
+            imageInputStream = new ByteArrayInputStream(imgData2);
+            checkBitmap(imageInputStream);
+
+            // Read in bitmap and make sure thumbnail is still valid
+            mInterface.readExif(imgData2);
+            bmap = mInterface.getThumbnailBitmap();
+            assertTrue(getImageTitle(), bmap != null);
+
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        } finally {
+            Util.closeSilently(imageInputStream);
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mTmpFile = File.createTempFile("exif_test", ".jpg");
+        mGroundTruth = ExifXmlReader.readXml(getXmlParser());
+
+        mInterface = new ExifInterface();
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                5, 4, 3, 2
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                6, 7, 8, 9
+        });
+        // TYPE ASCII with arbitary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "helloworld");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2013:02:11 20:20:20");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
+        // TYPE_UNSIGNED_LONG with 1 components
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(2, 2), new Rational(11, 11),
+                new Rational(102, 102)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(4, 6));
+
+        mTestTags = new HashMap<Integer, ExifTag>();
+
+        mTestTags.put(ExifInterface.TAG_EXIF_VERSION, mVersionTag);
+        mTestTags.put(ExifInterface.TAG_GPS_VERSION_ID, mGpsVersionTag);
+        mTestTags.put(ExifInterface.TAG_MODEL, mModelTag);
+        mTestTags.put(ExifInterface.TAG_DATE_TIME, mDateTimeTag);
+        mTestTags.put(ExifInterface.TAG_COMPRESSION, mCompressionTag);
+        mTestTags.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, mThumbnailFormatTag);
+        mTestTags.put(ExifInterface.TAG_GPS_LONGITUDE, mLongitudeTag);
+        mTestTags.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, mShutterTag);
+
+        mTagDefinitions = new HashMap<Integer, Integer>();
+        mTagDefinitions.put(ExifInterface.TAG_EXIF_VERSION, 0x04070004);
+        mTagDefinitions.put(ExifInterface.TAG_GPS_VERSION_ID, 0x10010004);
+        mTagDefinitions.put(ExifInterface.TAG_MODEL, 0x03020000);
+        mTagDefinitions.put(ExifInterface.TAG_DATE_TIME, 0x03020014);
+        mTagDefinitions.put(ExifInterface.TAG_COMPRESSION, 0x03030001);
+        mTagDefinitions.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 0x02040001);
+        mTagDefinitions.put(ExifInterface.TAG_GPS_LONGITUDE, 0x100a0003);
+        mTagDefinitions.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, 0x040a0001);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mTmpFile.delete();
+    }
+
+    // Helper functions
+
+    private void checkTagAgainstXml(ExifTag tag) {
+        List<String> truth = mGroundTruth.get(tag.getIfd()).get(tag.getTagId());
+
+        if (truth == null) {
+            fail(String.format("Unknown Tag %02x", tag.getTagId()) + ", " + getImageTitle());
+        }
+
+        // No value from exiftool.
+        if (truth.contains(null))
+            return;
+
+        String dataString = Util.tagValueToString(tag).trim();
+        assertTrue(String.format("Tag %02x", tag.getTagId()) + ", " + getImageTitle()
+                + ": " + dataString,
+                truth.contains(dataString));
+    }
+
+    private void checkTagsAgainstXml(List<ExifTag> tags) {
+        for (ExifTag t : tags) {
+            checkTagAgainstXml(t);
+        }
+    }
+
+    private void checkTagAgainstHash(ExifTag tag, Map<Integer, ExifTag> testTags) {
+        int tagdef = mInterface.getTagDefinitionForTag(tag);
+        assertTrue(getImageTitle(), tagdef != ExifInterface.TAG_NULL);
+        ExifTag t = testTags.get(tagdef);
+        // Ignore offset tags & other special tags
+        if (!ExifInterface.sBannedDefines.contains(tag.getTagId())) {
+            assertTrue(getImageTitle(), t != null);
+        } else {
+            return;
+        }
+        if (t == tag)
+            return;
+        assertTrue(getImageTitle(), tag.equals(t));
+        assertTrue(getImageTitle(), tag.getDataType() == t.getDataType());
+        assertTrue(getImageTitle(), tag.getTagId() == t.getTagId());
+        assertTrue(getImageTitle(), tag.getIfd() == t.getIfd());
+        assertTrue(getImageTitle(), tag.getComponentCount() == t.getComponentCount());
+    }
+
+    private void checkTagsAgainstHash(List<ExifTag> tags, Map<Integer, ExifTag> testTags) {
+        for (ExifTag t : tags) {
+            checkTagAgainstHash(t, testTags);
+        }
+    }
+
+    private void checkBitmap(InputStream inputStream) throws IOException {
+        Bitmap bmp = BitmapFactory.decodeStream(inputStream);
+        assertTrue(getImageTitle(), bmp != null);
+    }
+
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifModifierTest.java b/tests/src/com/android/gallery3d/exif/ExifModifierTest.java
new file mode 100644
index 0000000..96f405e
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifModifierTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.test.suitebuilder.annotation.MediumTest;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel.MapMode;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ExifModifierTest extends ExifXmlDataTestCase {
+
+    private File mTmpFile;
+    private List<Map<Short, List<String>>> mGroundTruth;
+    private ExifInterface mInterface;
+    private Map<Short, ExifTag> mTestTags;
+    ExifTag mVersionTag;
+    ExifTag mGpsVersionTag;
+    ExifTag mModelTag;
+    ExifTag mDateTimeTag;
+    ExifTag mCompressionTag;
+    ExifTag mThumbnailFormatTag;
+    ExifTag mLongitudeTag;
+    ExifTag mShutterTag;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mGroundTruth = ExifXmlReader.readXml(getXmlParser());
+        mTmpFile = File.createTempFile("exif_test", ".jpg");
+        FileOutputStream os = null;
+        InputStream is = getImageInputStream();
+        try {
+            os = new FileOutputStream(mTmpFile);
+            byte[] buf = new byte[1024];
+            int n;
+            while ((n = is.read(buf)) > 0) {
+                os.write(buf, 0, n);
+            }
+        } finally {
+            Util.closeSilently(os);
+        }
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                1, 2, 3, 4
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                4, 3, 2, 1
+        });
+        // TYPE ASCII with arbitary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "end-of-the-world");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2012:12:31 23:59:59");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
+        // TYPE_UNSIGNED_LONG with 1 components
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(1, 1), new Rational(10, 10),
+                new Rational(100, 100)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(1, 1));
+
+        mTestTags = new HashMap<Short, ExifTag>();
+
+        mTestTags.put(mVersionTag.getTagId(), mVersionTag);
+        mTestTags.put(mGpsVersionTag.getTagId(), mGpsVersionTag);
+        mTestTags.put(mModelTag.getTagId(), mModelTag);
+        mTestTags.put(mDateTimeTag.getTagId(), mDateTimeTag);
+        mTestTags.put(mCompressionTag.getTagId(), mCompressionTag);
+        mTestTags.put(mThumbnailFormatTag.getTagId(), mThumbnailFormatTag);
+        mTestTags.put(mLongitudeTag.getTagId(), mLongitudeTag);
+        mTestTags.put(mShutterTag.getTagId(), mShutterTag);
+    }
+
+    public ExifModifierTest(int imageRes, int xmlRes) {
+        super(imageRes, xmlRes);
+        mInterface = new ExifInterface();
+    }
+
+    public ExifModifierTest(String imagePath, String xmlPath) {
+        super(imagePath, xmlPath);
+        mInterface = new ExifInterface();
+    }
+
+    @MediumTest
+    public void testModify() throws Exception {
+        Map<Short, Boolean> results = new HashMap<Short, Boolean>();
+
+        RandomAccessFile file = null;
+        try {
+            file = new RandomAccessFile(mTmpFile, "rw");
+            MappedByteBuffer buf = file.getChannel().map(MapMode.READ_WRITE, 0, file.length());
+            for (ExifTag tag : mTestTags.values()) {
+                ExifModifier modifier = new ExifModifier(buf, mInterface);
+                modifier.modifyTag(tag);
+                boolean result = modifier.commit();
+                results.put(tag.getTagId(), result);
+                buf.force();
+                buf.position(0);
+
+                if (!result) {
+                    List<String> value = mGroundTruth.get(tag.getIfd()).get(tag.getTagId());
+                    assertTrue(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
+                            value == null || tag.getTagId() == ExifInterface.TAG_MODEL);
+                }
+            }
+        } finally {
+            Util.closeSilently(file);
+        }
+
+        // Parse the new file and check the result
+        InputStream is = null;
+        try {
+            is = new FileInputStream(mTmpFile);
+            ExifData data = new ExifReader(mInterface).read(is);
+            for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+                checkIfd(data.getIfdData(i), mGroundTruth.get(i), results);
+            }
+        } finally {
+            Util.closeSilently(is);
+        }
+
+    }
+
+    private void checkIfd(IfdData ifd, Map<Short, List<String>> ifdValue,
+            Map<Short, Boolean> results) {
+        if (ifd == null) {
+            assertEquals(getImageTitle(), 0, ifdValue.size());
+            return;
+        }
+        ExifTag[] tags = ifd.getAllTags();
+        for (ExifTag tag : tags) {
+            List<String> truth = ifdValue.get(tag.getTagId());
+            assertNotNull(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(), truth);
+            if (truth.contains(null)) {
+                continue;
+            }
+
+            ExifTag newTag = mTestTags.get(tag.getTagId());
+            if (newTag != null
+                    && results.get(tag.getTagId())) {
+                assertEquals(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
+                        Util.tagValueToString(newTag), Util.tagValueToString(tag));
+            } else {
+                assertTrue(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
+                        truth.contains(Util.tagValueToString(tag).trim()));
+            }
+        }
+        assertEquals(getImageTitle(), ifdValue.size(), tags.length);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mTmpFile.delete();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java b/tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java
new file mode 100644
index 0000000..151bdbc
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifOutputStreamTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class ExifOutputStreamTest extends ExifXmlDataTestCase {
+
+    private File mTmpFile;
+
+    private ExifInterface mInterface;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mTmpFile = File.createTempFile("exif_test", ".jpg");
+    }
+
+    public ExifOutputStreamTest(int imgRes, int xmlRes) {
+        super(imgRes, xmlRes);
+        mInterface = new ExifInterface();
+    }
+
+    public ExifOutputStreamTest(String imgPath, String xmlPath) {
+        super(imgPath, xmlPath);
+        mInterface = new ExifInterface();
+    }
+
+    @MediumTest
+    public void testExifOutputStream() throws Exception {
+        InputStream imageInputStream = null;
+        InputStream exifInputStream = null;
+        FileInputStream reDecodeInputStream = null;
+        FileInputStream reParseInputStream = null;
+
+        InputStream dangerInputStream = null;
+        OutputStream dangerOutputStream = null;
+        try {
+            try {
+                byte[] imgData = Util.readToByteArray(getImageInputStream());
+                imageInputStream = new ByteArrayInputStream(imgData);
+                exifInputStream = new ByteArrayInputStream(imgData);
+
+                // Read the image data
+                Bitmap bmp = BitmapFactory.decodeStream(imageInputStream);
+                // The image is invalid
+                if (bmp == null) {
+                    return;
+                }
+
+                // Read exif data
+                ExifData exifData = new ExifReader(mInterface).read(exifInputStream);
+
+                // Encode the image with the exif data
+                FileOutputStream outputStream = new FileOutputStream(mTmpFile);
+                ExifOutputStream exifOutputStream = new ExifOutputStream(outputStream, mInterface);
+                exifOutputStream.setExifData(exifData);
+                bmp.compress(Bitmap.CompressFormat.JPEG, 90, exifOutputStream);
+                exifOutputStream.close();
+                exifOutputStream = null;
+
+                // Re-decode the temp file and check the data.
+                reDecodeInputStream = new FileInputStream(mTmpFile);
+                Bitmap decodedBmp = BitmapFactory.decodeStream(reDecodeInputStream);
+                assertNotNull(getImageTitle(), decodedBmp);
+                reDecodeInputStream.close();
+
+                // Re-parse the temp file the check EXIF tag
+                reParseInputStream = new FileInputStream(mTmpFile);
+                ExifData reExifData = new ExifReader(mInterface).read(reParseInputStream);
+                assertEquals(getImageTitle(), exifData, reExifData);
+                reParseInputStream.close();
+
+                // Try writing exif to file with existing exif.
+                dangerOutputStream = (OutputStream) new FileOutputStream(mTmpFile);
+                exifOutputStream = new ExifOutputStream(dangerOutputStream, mInterface);
+                exifOutputStream.setExifData(exifData);
+                exifOutputStream.write(imgData);
+                // exifOutputStream.write(strippedImgData);
+                exifOutputStream.close();
+                exifOutputStream = null;
+
+                // Make sure it still can be parsed into a bitmap.
+                dangerInputStream = (InputStream) new FileInputStream(mTmpFile);
+                decodedBmp = null;
+                decodedBmp = BitmapFactory.decodeStream(dangerInputStream);
+                assertNotNull(getImageTitle(), decodedBmp);
+                dangerInputStream.close();
+                dangerInputStream = null;
+
+                // Make sure exif is still well-formatted.
+                dangerInputStream = (InputStream) new FileInputStream(mTmpFile);
+                reExifData = null;
+                reExifData = new ExifReader(mInterface).read(dangerInputStream);
+                assertEquals(getImageTitle(), exifData, reExifData);
+                dangerInputStream.close();
+                dangerInputStream = null;
+
+            } finally {
+                Util.closeSilently(imageInputStream);
+                Util.closeSilently(exifInputStream);
+                Util.closeSilently(reDecodeInputStream);
+                Util.closeSilently(reParseInputStream);
+
+                Util.closeSilently(dangerInputStream);
+                Util.closeSilently(dangerOutputStream);
+            }
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+
+    @MediumTest
+    public void testOutputSpeed() throws Exception {
+        final String LOGTAG = "testOutputSpeed";
+        InputStream imageInputStream = null;
+        OutputStream imageOutputStream = null;
+        try {
+            try {
+                imageInputStream = getImageInputStream();
+                // Read the image data
+                Bitmap bmp = BitmapFactory.decodeStream(imageInputStream);
+                // The image is invalid
+                if (bmp == null) {
+                    return;
+                }
+                imageInputStream.close();
+                int nLoops = 20;
+                long totalReadDuration = 0;
+                long totalWriteDuration = 0;
+                for (int i = 0; i < nLoops; i++) {
+                    imageInputStream = reopenFileStream();
+                    // Read exif data
+                    long startTime = System.nanoTime();
+                    ExifData exifData = new ExifReader(mInterface).read(imageInputStream);
+                    long endTime = System.nanoTime();
+                    long duration = endTime - startTime;
+                    totalReadDuration += duration;
+                    Log.v(LOGTAG, " read time: " + duration);
+                    imageInputStream.close();
+
+                    // Encode the image with the exif data
+                    imageOutputStream = (OutputStream) new FileOutputStream(mTmpFile);
+                    ExifOutputStream exifOutputStream = new ExifOutputStream(imageOutputStream,
+                            mInterface);
+                    exifOutputStream.setExifData(exifData);
+                    startTime = System.nanoTime();
+                    bmp.compress(Bitmap.CompressFormat.JPEG, 90, exifOutputStream);
+                    endTime = System.nanoTime();
+                    duration = endTime - startTime;
+                    totalWriteDuration += duration;
+                    Log.v(LOGTAG, " write time: " + duration);
+                    exifOutputStream.close();
+                }
+                Log.v(LOGTAG, "======================= normal");
+                Log.v(LOGTAG, "avg read time: " + totalReadDuration / nLoops);
+                Log.v(LOGTAG, "avg write time: " + totalWriteDuration / nLoops);
+                Log.v(LOGTAG, "=======================");
+            } finally {
+                Util.closeSilently(imageInputStream);
+                Util.closeSilently(imageOutputStream);
+            }
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mTmpFile.delete();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifParserTest.java b/tests/src/com/android/gallery3d/exif/ExifParserTest.java
new file mode 100644
index 0000000..247ea02
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifParserTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import java.util.List;
+import java.util.Map;
+
+public class ExifParserTest extends ExifXmlDataTestCase {
+    private static final String TAG = "ExifParserTest";
+
+    private ExifInterface mInterface;
+
+    public ExifParserTest(int imgRes, int xmlRes) {
+        super(imgRes, xmlRes);
+        mInterface = new ExifInterface();
+    }
+
+    public ExifParserTest(String imgPath, String xmlPath) {
+        super(imgPath, xmlPath);
+        mInterface = new ExifInterface();
+    }
+
+    private List<Map<Short, List<String>>> mGroundTruth;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mGroundTruth = ExifXmlReader.readXml(getXmlParser());
+    }
+
+    @MediumTest
+    public void testParse() throws Exception {
+        try {
+            ExifParser parser = ExifParser.parse(getImageInputStream(), mInterface);
+            int event = parser.next();
+            while (event != ExifParser.EVENT_END) {
+                switch (event) {
+                    case ExifParser.EVENT_START_OF_IFD:
+                        break;
+                    case ExifParser.EVENT_NEW_TAG:
+                        ExifTag tag = parser.getTag();
+                        if (!tag.hasValue()) {
+                            parser.registerForTagValue(tag);
+                        } else {
+                            checkTag(tag);
+                        }
+                        break;
+                    case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+                        tag = parser.getTag();
+                        if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
+                            byte[] buf = new byte[tag.getComponentCount()];
+                            parser.read(buf);
+                            assertTrue(TAG, tag.setValue(buf));
+                        }
+                        checkTag(tag);
+                        break;
+                }
+                event = parser.next();
+            }
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+
+    private void checkTag(ExifTag tag) {
+        List<String> truth = mGroundTruth.get(tag.getIfd()).get(tag.getTagId());
+
+        if (truth == null) {
+            fail(String.format("Unknown Tag %02x", tag.getTagId()) + ", " + getImageTitle());
+        }
+
+        // No value from exiftool.
+        if (truth.contains(null)) {
+            return;
+        }
+
+        String dataString = Util.tagValueToString(tag).trim();
+        assertTrue(String.format("Tag %02x", tag.getTagId()) + ", " + getImageTitle()
+                + ": " + dataString,
+                truth.contains(dataString));
+    }
+
+    private void parseOneIfd(int ifd, int options) throws Exception {
+        try {
+            Map<Short, List<String>> expectedResult = mGroundTruth.get(ifd);
+            int numOfTag = 0;
+            ExifParser parser = ExifParser.parse(getImageInputStream(), options, mInterface);
+            int event = parser.next();
+            while (event != ExifParser.EVENT_END) {
+                switch (event) {
+                    case ExifParser.EVENT_START_OF_IFD:
+                        assertEquals(getImageTitle(), ifd, parser.getCurrentIfd());
+                        break;
+                    case ExifParser.EVENT_NEW_TAG:
+                        ExifTag tag = parser.getTag();
+                        numOfTag++;
+                        if (tag.hasValue()) {
+                            checkTag(tag);
+                        } else {
+                            parser.registerForTagValue(tag);
+                        }
+                        break;
+                    case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+                        tag = parser.getTag();
+                        if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
+                            byte[] buf = new byte[tag.getComponentCount()];
+                            parser.read(buf);
+                            tag.setValue(buf);
+                        }
+                        checkTag(tag);
+                        break;
+                    case ExifParser.EVENT_COMPRESSED_IMAGE:
+                    case ExifParser.EVENT_UNCOMPRESSED_STRIP:
+                        fail("Invalid Event type: " + event + ", " + getImageTitle());
+                        break;
+                }
+                event = parser.next();
+            }
+            assertEquals(getImageTitle(), ExifXmlReader.getTrueTagNumber(expectedResult), numOfTag);
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+
+    @MediumTest
+    public void testOnlyExifIfd() throws Exception {
+        parseOneIfd(IfdId.TYPE_IFD_EXIF, ExifParser.OPTION_IFD_EXIF);
+    }
+
+    @MediumTest
+    public void testOnlyIfd0() throws Exception {
+        parseOneIfd(IfdId.TYPE_IFD_0, ExifParser.OPTION_IFD_0);
+    }
+
+    @MediumTest
+    public void testOnlyIfd1() throws Exception {
+        parseOneIfd(IfdId.TYPE_IFD_1, ExifParser.OPTION_IFD_1);
+    }
+
+    @MediumTest
+    public void testOnlyInteroperabilityIfd() throws Exception {
+        parseOneIfd(IfdId.TYPE_IFD_INTEROPERABILITY, ExifParser.OPTION_IFD_INTEROPERABILITY);
+    }
+
+    @MediumTest
+    public void testOnlyReadSomeTag() throws Exception {
+        // Do not do this test if there is no model tag.
+        if (mGroundTruth.get(IfdId.TYPE_IFD_0).get(ExifInterface.TAG_MODEL) == null) {
+            return;
+        }
+
+        try {
+            ExifParser parser = ExifParser.parse(getImageInputStream(), ExifParser.OPTION_IFD_0,
+                    mInterface);
+            int event = parser.next();
+            boolean isTagFound = false;
+            while (event != ExifParser.EVENT_END) {
+                switch (event) {
+                    case ExifParser.EVENT_START_OF_IFD:
+                        assertEquals(getImageTitle(), IfdId.TYPE_IFD_0, parser.getCurrentIfd());
+                        break;
+                    case ExifParser.EVENT_NEW_TAG:
+                        ExifTag tag = parser.getTag();
+                        if (tag.getTagId() == ExifInterface.TAG_MODEL) {
+                            if (tag.hasValue()) {
+                                isTagFound = true;
+                                checkTag(tag);
+                            } else {
+                                parser.registerForTagValue(tag);
+                            }
+                            parser.skipRemainingTagsInCurrentIfd();
+                        }
+                        break;
+                    case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+                        tag = parser.getTag();
+                        assertEquals(getImageTitle(), ExifInterface.TAG_MODEL, tag.getTagId());
+                        checkTag(tag);
+                        isTagFound = true;
+                        break;
+                }
+                event = parser.next();
+            }
+            assertTrue(getImageTitle(), isTagFound);
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+
+    @MediumTest
+    public void testReadThumbnail() throws Exception {
+        try {
+            ExifParser parser = ExifParser.parse(getImageInputStream(),
+                    ExifParser.OPTION_IFD_1 | ExifParser.OPTION_THUMBNAIL, mInterface);
+
+            int event = parser.next();
+            Bitmap bmp = null;
+            boolean mIsContainCompressedImage = false;
+            while (event != ExifParser.EVENT_END) {
+                switch (event) {
+                    case ExifParser.EVENT_NEW_TAG:
+                        ExifTag tag = parser.getTag();
+                        if (tag.getTagId() == ExifInterface.TAG_COMPRESSION) {
+                            if (tag.getValueAt(0) == ExifInterface.Compression.JPEG) {
+                                mIsContainCompressedImage = true;
+                            }
+                        }
+                        break;
+                    case ExifParser.EVENT_COMPRESSED_IMAGE:
+                        int imageSize = parser.getCompressedImageSize();
+                        byte buf[] = new byte[imageSize];
+                        parser.read(buf);
+                        bmp = BitmapFactory.decodeByteArray(buf, 0, imageSize);
+                        break;
+                }
+                event = parser.next();
+            }
+            if (mIsContainCompressedImage) {
+                assertNotNull(getImageTitle(), bmp);
+            }
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifReaderTest.java b/tests/src/com/android/gallery3d/exif/ExifReaderTest.java
new file mode 100644
index 0000000..4b5c029
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifReaderTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+import android.graphics.BitmapFactory;
+
+import java.util.List;
+import java.util.Map;
+
+public class ExifReaderTest extends ExifXmlDataTestCase {
+    private static final String TAG = "ExifReaderTest";
+
+    private ExifInterface mInterface;
+    private List<Map<Short, List<String>>> mGroundTruth;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mGroundTruth = ExifXmlReader.readXml(getXmlParser());
+    }
+
+    public ExifReaderTest(int imgRes, int xmlRes) {
+        super(imgRes, xmlRes);
+        mInterface = new ExifInterface();
+    }
+
+    public ExifReaderTest(String imgPath, String xmlPath) {
+        super(imgPath, xmlPath);
+        mInterface = new ExifInterface();
+    }
+
+    @MediumTest
+    public void testRead() throws Exception {
+        try {
+            ExifReader reader = new ExifReader(mInterface);
+            ExifData exifData = reader.read(getImageInputStream());
+            for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+                checkIfd(exifData.getIfdData(i), mGroundTruth.get(i));
+            }
+            checkThumbnail(exifData);
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+
+    private void checkThumbnail(ExifData exifData) {
+        Map<Short, List<String>> ifd1Truth = mGroundTruth.get(IfdId.TYPE_IFD_1);
+
+        List<String> typeTagValue = ifd1Truth.get(ExifInterface.TAG_COMPRESSION);
+        if (typeTagValue == null)
+            return;
+
+        IfdData ifd1 = exifData.getIfdData(IfdId.TYPE_IFD_1);
+        if (ifd1 == null)
+            fail(getImageTitle() + ": failed to find IFD1");
+
+        String typeTagTruth = typeTagValue.get(0);
+
+        int type = (int) ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_COMPRESSION))
+                .getValueAt(0);
+
+        if (String.valueOf(ExifInterface.Compression.JPEG).equals(typeTagTruth)) {
+            assertTrue(getImageTitle(), type == ExifInterface.Compression.JPEG);
+            assertTrue(getImageTitle(), exifData.hasCompressedThumbnail());
+            byte[] thumbnail = exifData.getCompressedThumbnail();
+            assertTrue(getImageTitle(),
+                    BitmapFactory.decodeByteArray(thumbnail, 0, thumbnail.length) != null);
+        } else if (String.valueOf(ExifInterface.Compression.UNCOMPRESSION).equals(typeTagTruth)) {
+            assertTrue(getImageTitle(), type == ExifInterface.Compression.UNCOMPRESSION);
+            // Try to check the strip count with the formula provided by EXIF spec.
+            int planarType = ExifInterface.PlanarConfiguration.CHUNKY;
+            ExifTag planarTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_PLANAR_CONFIGURATION));
+            if (planarTag != null) {
+                planarType = (int) planarTag.getValueAt(0);
+            }
+
+            if (!ifd1Truth.containsKey(ExifInterface.TAG_IMAGE_LENGTH) ||
+                    !ifd1Truth.containsKey(ExifInterface.TAG_ROWS_PER_STRIP)) {
+                return;
+            }
+
+            ExifTag heightTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_IMAGE_LENGTH));
+            ExifTag rowPerStripTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_ROWS_PER_STRIP));
+
+            // Fail the test if required tags are missing
+            if (heightTag == null || rowPerStripTag == null) {
+                fail(getImageTitle());
+            }
+
+            int imageLength = (int) heightTag.getValueAt(0);
+            int rowsPerStrip = (int) rowPerStripTag.getValueAt(0);
+            int stripCount = ifd1.getTag(
+                    ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
+                    .getComponentCount();
+
+            if (planarType == ExifInterface.PlanarConfiguration.CHUNKY) {
+                assertTrue(getImageTitle(),
+                        stripCount == (imageLength + rowsPerStrip - 1) / rowsPerStrip);
+            } else {
+                if (!ifd1Truth.containsKey(ExifInterface.TAG_SAMPLES_PER_PIXEL)) {
+                    return;
+                }
+                ExifTag samplePerPixelTag = ifd1.getTag(ExifInterface
+                        .getTrueTagKey(ExifInterface.TAG_SAMPLES_PER_PIXEL));
+                int samplePerPixel = (int) samplePerPixelTag.getValueAt(0);
+                assertTrue(getImageTitle(),
+                        stripCount ==
+                        (imageLength + rowsPerStrip - 1) / rowsPerStrip * samplePerPixel);
+            }
+
+            if (!ifd1Truth.containsKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)) {
+                return;
+            }
+            ExifTag byteCountTag = ifd1.getTag(ExifInterface
+                    .getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
+            short byteCountDataType = byteCountTag.getDataType();
+            for (int i = 0; i < stripCount; i++) {
+                if (byteCountDataType == ExifTag.TYPE_UNSIGNED_SHORT) {
+                    assertEquals(getImageTitle(),
+                            byteCountTag.getValueAt(i), exifData.getStrip(i).length);
+                } else {
+                    assertEquals(getImageTitle(),
+                            byteCountTag.getValueAt(i), exifData.getStrip(i).length);
+                }
+            }
+        }
+    }
+
+    private void checkIfd(IfdData ifd, Map<Short, List<String>> ifdValue) {
+        if (ifd == null) {
+            assertEquals(getImageTitle(), 0, ifdValue.size());
+            return;
+        }
+        ExifTag[] tags = ifd.getAllTags();
+        for (ExifTag tag : tags) {
+            List<String> truth = ifdValue.get(tag.getTagId());
+            assertNotNull(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(), truth);
+            if (truth.contains(null)) {
+                continue;
+            }
+            assertTrue(String.format("Tag %x, ", tag.getTagId()) + getImageTitle(),
+                    truth.contains(Util.tagValueToString(tag).trim()));
+        }
+        assertEquals(getImageTitle(), ifdValue.size(), tags.length);
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifTagTest.java b/tests/src/com/android/gallery3d/exif/ExifTagTest.java
new file mode 100644
index 0000000..e6a41ec
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifTagTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ExifTagTest extends TestCase {
+
+    private static long MAX_UNSIGNED_LONG = (1L << 32) - 1;
+    private static int MAX_LONG = Integer.MAX_VALUE;
+    private static int MIN_LONG = Integer.MIN_VALUE;
+
+    Map<Integer, ExifTag> mTestTags;
+    ExifInterface mInterface;
+    private ExifTag mVersionTag;
+    private ExifTag mGpsVersionTag;
+    private ExifTag mModelTag;
+    private ExifTag mDateTimeTag;
+    private ExifTag mCompressionTag;
+    private ExifTag mThumbnailFormatTag;
+    private ExifTag mLongitudeTag;
+    private ExifTag mShutterTag;
+    private ExifTag mInteropIndex;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mInterface = new ExifInterface();
+
+        // TYPE_UNDEFINED with 4 components
+        mVersionTag = mInterface.buildTag(ExifInterface.TAG_EXIF_VERSION, new byte[] {
+                5, 4, 3, 2
+        });
+        // TYPE_UNSIGNED_BYTE with 4 components
+        mGpsVersionTag = mInterface.buildTag(ExifInterface.TAG_GPS_VERSION_ID, new byte[] {
+                6, 7, 8, 9
+        });
+        // TYPE ASCII with arbitrary length
+        mModelTag = mInterface.buildTag(ExifInterface.TAG_MODEL, "helloworld");
+        // TYPE_ASCII with 20 components
+        mDateTimeTag = mInterface.buildTag(ExifInterface.TAG_DATE_TIME, "2013:02:11 20:20:20");
+        // TYPE_UNSIGNED_SHORT with 1 components
+        mCompressionTag = mInterface.buildTag(ExifInterface.TAG_COMPRESSION, 100);
+        // TYPE_UNSIGNED_LONG with 1 components
+        mThumbnailFormatTag =
+                mInterface.buildTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, 100);
+        // TYPE_UNSIGNED_RATIONAL with 3 components
+        mLongitudeTag = mInterface.buildTag(ExifInterface.TAG_GPS_LONGITUDE, new Rational[] {
+                new Rational(2, 2), new Rational(11, 11),
+                new Rational(102, 102)
+        });
+        // TYPE_RATIONAL with 1 components
+        mShutterTag = mInterface
+                .buildTag(ExifInterface.TAG_SHUTTER_SPEED_VALUE, new Rational(4, 6));
+        // TYPE_ASCII with arbitrary length
+        mInteropIndex = mInterface.buildTag(ExifInterface.TAG_INTEROPERABILITY_INDEX, "foo");
+
+        mTestTags = new HashMap<Integer, ExifTag>();
+
+        mTestTags.put(ExifInterface.TAG_EXIF_VERSION, mVersionTag);
+        mTestTags.put(ExifInterface.TAG_GPS_VERSION_ID, mGpsVersionTag);
+        mTestTags.put(ExifInterface.TAG_MODEL, mModelTag);
+        mTestTags.put(ExifInterface.TAG_DATE_TIME, mDateTimeTag);
+        mTestTags.put(ExifInterface.TAG_COMPRESSION, mCompressionTag);
+        mTestTags.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, mThumbnailFormatTag);
+        mTestTags.put(ExifInterface.TAG_GPS_LONGITUDE, mLongitudeTag);
+        mTestTags.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, mShutterTag);
+        mTestTags.put(ExifInterface.TAG_INTEROPERABILITY_INDEX, mInteropIndex);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mInterface = null;
+        mTestTags = null;
+    }
+
+    @SmallTest
+    public void testValueType() {
+        for (ExifTag tag : mTestTags.values()) {
+            assertTrue(tag != null);
+            int count = tag.getComponentCount();
+            int intBuf[] = new int[count];
+            long longBuf[] = new long[count];
+            byte byteBuf[] = new byte[count];
+            Rational rationalBuf[] = new Rational[count];
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < count; i++) {
+                intBuf[i] = 0;
+                longBuf[i] = 0;
+                byteBuf[i] = 0;
+                rationalBuf[i] = new Rational(0, 0);
+                // The string size should equal to component count - 1
+                if (i != count - 1) {
+                    sb.append("*");
+                } else {
+                    sb.append("\0");
+                }
+            }
+            String strBuf = sb.toString();
+
+            checkTypeByte(tag, byteBuf);
+            checkTypeAscii(tag, strBuf);
+            checkTypeUnsignedShort(tag, intBuf);
+            checkTypeUnsignedLong(tag, intBuf, longBuf);
+            checkTypeLong(tag, intBuf);
+            checkTypeRational(tag, rationalBuf);
+            checkTypeUnsignedRational(tag, rationalBuf);
+        }
+    }
+
+    private void checkTypeByte(ExifTag tag, byte[] buf) {
+        short type = tag.getDataType();
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(buf)
+                ^ (type == ExifTag.TYPE_UNDEFINED || type == ExifTag.TYPE_UNSIGNED_BYTE));
+    }
+
+    private void checkTypeAscii(ExifTag tag, String str) {
+        short type = tag.getDataType();
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(str)
+                ^ (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED));
+    }
+
+    private void checkTypeUnsignedShort(ExifTag tag, int[] intBuf) {
+        short type = tag.getDataType();
+        assertFalse("\nTag: " + tag.toString(),
+                tag.setValue(intBuf)
+                        ^ (type == ExifTag.TYPE_UNSIGNED_SHORT
+                                || type == ExifTag.TYPE_UNSIGNED_LONG
+                                || type == ExifTag.TYPE_LONG));
+    }
+
+    private void checkTypeUnsignedLong(ExifTag tag, int[] intBuf, long[] longBuf) {
+
+        // Test value only for unsigned long.
+        int count = intBuf.length;
+        intBuf[count - 1] = MAX_LONG;
+        tag.setValue(intBuf);
+        longBuf[count - 1] = MAX_UNSIGNED_LONG;
+
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(longBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_UNSIGNED_LONG));
+
+        intBuf[count - 1] = 0;
+        // Test invalid value for all type.
+        longBuf[count - 1] = MAX_UNSIGNED_LONG + 1;
+        assertFalse(tag.setValue(longBuf));
+        longBuf[count - 1] = 0;
+    }
+
+    private void checkTypeLong(ExifTag tag, int[] intBuf) {
+        int count = intBuf.length;
+        intBuf[count - 1] = MAX_LONG;
+        tag.setValue(intBuf);
+        intBuf[count - 1] = MIN_LONG;
+
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(intBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_LONG));
+        intBuf[count - 1] = 0;
+    }
+
+    private void checkTypeRational(ExifTag tag, Rational rationalBuf[]) {
+        int count = rationalBuf.length;
+        Rational r = rationalBuf[count - 1];
+        rationalBuf[count - 1] = new Rational(MAX_LONG, MIN_LONG);
+
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(rationalBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_RATIONAL));
+
+        if (tag.getDataType() == ExifTag.TYPE_RATIONAL) {
+            // check overflow
+
+            rationalBuf[count - 1] = new Rational(MAX_LONG + 1L, MIN_LONG);
+            assertFalse(tag.setValue(rationalBuf));
+
+            rationalBuf[count - 1] = new Rational(MAX_LONG, MIN_LONG - 1L);
+            assertFalse(tag.setValue(rationalBuf));
+        }
+        rationalBuf[count - 1] = r;
+    }
+
+    private void checkTypeUnsignedRational(ExifTag tag, Rational rationalBuf[]) {
+        int count = rationalBuf.length;
+        Rational r = rationalBuf[count - 1];
+        rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG, MAX_UNSIGNED_LONG);
+
+        assertFalse("\nTag: " + tag.toString(), tag.setValue(rationalBuf)
+                ^ (tag.getDataType() == ExifTag.TYPE_UNSIGNED_RATIONAL));
+
+        if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_RATIONAL) {
+            // check overflow
+            rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG + 1, 0);
+            assertFalse(tag.setValue(rationalBuf));
+
+            rationalBuf[count - 1] = new Rational(MAX_UNSIGNED_LONG, -1);
+            assertFalse(tag.setValue(rationalBuf));
+        }
+        rationalBuf[count - 1] = r;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifTestRunner.java b/tests/src/com/android/gallery3d/exif/ExifTestRunner.java
new file mode 100644
index 0000000..162baea
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifTestRunner.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.content.Context;
+import android.os.Environment;
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+import android.util.Log;
+
+import com.android.gallery3d.tests.R;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ExifTestRunner extends InstrumentationTestRunner {
+    private static final String TAG = "ExifTestRunner";
+
+    private static final int[] IMG_RESOURCE = {
+            R.raw.galaxy_nexus
+    };
+
+    private static final int[] EXIF_DATA_RESOURCE = {
+            R.xml.galaxy_nexus
+    };
+
+    private static List<String> mTestImgPath = new ArrayList<String>();
+    private static List<String> mTestXmlPath = new ArrayList<String>();
+
+    @Override
+    public TestSuite getAllTests() {
+        getTestImagePath();
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(ExifDataTest.class);
+        suite.addTestSuite(ExifTagTest.class);
+        addAllTestsFromExifTestCase(ExifParserTest.class, suite);
+        addAllTestsFromExifTestCase(ExifReaderTest.class, suite);
+        addAllTestsFromExifTestCase(ExifOutputStreamTest.class, suite);
+        addAllTestsFromExifTestCase(ExifModifierTest.class, suite);
+        addAllTestsFromExifTestCase(ExifInterfaceTest.class, suite);
+        return suite;
+    }
+
+    private void getTestImagePath() {
+        Context context = getContext();
+        File imgDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+        File xmlDir = new File(context.getExternalFilesDir(null).getPath(), "Xml");
+
+        if (imgDir != null && xmlDir != null) {
+            String[] imgs = imgDir.list();
+            if (imgs == null) {
+                return;
+            }
+            for (String imgName : imgs) {
+                String xmlName = imgName.substring(0, imgName.lastIndexOf('.')) + ".xml";
+                File xmlFile = new File(xmlDir, xmlName);
+                if (xmlFile.exists()) {
+                    mTestImgPath.add(new File(imgDir, imgName).getAbsolutePath());
+                    mTestXmlPath.add(xmlFile.getAbsolutePath());
+                }
+            }
+        }
+    }
+
+    private void addAllTestsFromExifTestCase(Class<? extends ExifXmlDataTestCase> testClass,
+            TestSuite suite) {
+        for (Method method : testClass.getDeclaredMethods()) {
+            if (method.getName().startsWith("test") && method.getParameterTypes().length == 0) {
+                for (int i = 0; i < IMG_RESOURCE.length; i++) {
+                    TestCase test;
+                    try {
+                        test = testClass.getDeclaredConstructor(int.class, int.class).
+                                newInstance(IMG_RESOURCE[i], EXIF_DATA_RESOURCE[i]);
+                        test.setName(method.getName());
+                        suite.addTest(test);
+                    } catch (IllegalArgumentException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (InstantiationException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (IllegalAccessException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (InvocationTargetException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (NoSuchMethodException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    }
+                }
+                for (int i = 0, n = mTestImgPath.size(); i < n; i++) {
+                    TestCase test;
+                    try {
+                        test = testClass.getDeclaredConstructor(String.class, String.class).
+                                newInstance(mTestImgPath.get(i), mTestXmlPath.get(i));
+                        test.setName(method.getName());
+                        suite.addTest(test);
+                    } catch (IllegalArgumentException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (InstantiationException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (IllegalAccessException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (InvocationTargetException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (NoSuchMethodException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return ExifTestRunner.class.getClassLoader();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifXmlDataTestCase.java b/tests/src/com/android/gallery3d/exif/ExifXmlDataTestCase.java
new file mode 100644
index 0000000..da86020
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifXmlDataTestCase.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import android.content.res.Resources;
+import android.test.InstrumentationTestCase;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class ExifXmlDataTestCase extends InstrumentationTestCase {
+
+    private static final String RES_ID_TITLE = "Resource ID: %x";
+
+    private InputStream mImageInputStream;
+    private InputStream mXmlInputStream;
+    private XmlPullParser mXmlParser;
+    private final String mImagePath;
+    private final String mXmlPath;
+    private final int mImageResourceId;
+    private final int mXmlResourceId;
+
+    public ExifXmlDataTestCase(int imageRes, int xmlRes) {
+        mImagePath = null;
+        mXmlPath = null;
+        mImageResourceId = imageRes;
+        mXmlResourceId = xmlRes;
+    }
+
+    public ExifXmlDataTestCase(String imagePath, String xmlPath) {
+        mImagePath = imagePath;
+        mXmlPath = xmlPath;
+        mImageResourceId = 0;
+        mXmlResourceId = 0;
+    }
+
+    protected InputStream getImageInputStream() {
+        return mImageInputStream;
+    }
+
+    protected XmlPullParser getXmlParser() {
+        return mXmlParser;
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        try {
+            if (mImagePath != null) {
+                mImageInputStream = new FileInputStream(mImagePath);
+                mXmlInputStream = new FileInputStream(mXmlPath);
+                mXmlParser = Xml.newPullParser();
+                mXmlParser.setInput(new InputStreamReader(mXmlInputStream));
+            } else {
+                Resources res = getInstrumentation().getContext().getResources();
+                mImageInputStream = res.openRawResource(mImageResourceId);
+                mXmlParser = res.getXml(mXmlResourceId);
+            }
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        Util.closeSilently(mImageInputStream);
+        Util.closeSilently(mXmlInputStream);
+        mXmlParser = null;
+    }
+
+    protected String getImageTitle() {
+        if (mImagePath != null) {
+            return mImagePath;
+        } else {
+            return String.format(RES_ID_TITLE, mImageResourceId);
+        }
+    }
+
+    protected InputStream reopenFileStream() throws Exception {
+        try {
+            if (mImagePath != null) {
+                return new FileInputStream(mImagePath);
+            } else {
+                Resources res = getInstrumentation().getContext().getResources();
+                return res.openRawResource(mImageResourceId);
+            }
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/ExifXmlReader.java b/tests/src/com/android/gallery3d/exif/ExifXmlReader.java
new file mode 100644
index 0000000..12e9cf7
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/ExifXmlReader.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ExifXmlReader {
+    private static final String TAG_EXIF = "exif";
+    private static final String TAG_TAG = "tag";
+
+    private static final String IFD0 = "IFD0";
+    private static final String EXIF_IFD = "ExifIFD";
+    private static final String GPS_IFD = "GPS";
+    private static final String IFD1 = "IFD1";
+    private static final String INTEROP_IFD = "InteropIFD";
+
+    private static final String ATTR_ID = "id";
+    private static final String ATTR_IFD = "ifd";
+
+    private static final String NO_VALUE = "NO_VALUE";
+
+    /**
+     * This function read the ground truth XML.
+     *
+     * @throws XmlPullParserException
+     * @throws IOException
+     */
+    static public List<Map<Short, List<String>>> readXml(XmlPullParser parser)
+            throws XmlPullParserException, IOException {
+
+        List<Map<Short, List<String>>> exifData =
+                new ArrayList<Map<Short, List<String>>>(IfdId.TYPE_IFD_COUNT);
+        for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+            exifData.add(new HashMap<Short, List<String>>());
+        }
+
+        while (parser.next() != XmlPullParser.END_DOCUMENT) {
+            if (parser.getEventType() == XmlPullParser.START_TAG) {
+                break;
+            }
+        }
+        parser.require(XmlPullParser.START_TAG, null, TAG_EXIF);
+
+        while (parser.next() != XmlPullParser.END_TAG) {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                continue;
+            }
+
+            parser.require(XmlPullParser.START_TAG, null, TAG_TAG);
+
+            int ifdId = getIfdIdFromString(parser.getAttributeValue(null, ATTR_IFD));
+            short id = Integer.decode(parser.getAttributeValue(null, ATTR_ID)).shortValue();
+
+            String value = "";
+            if (parser.next() == XmlPullParser.TEXT) {
+                value = parser.getText();
+                parser.next();
+            }
+
+            if (ifdId < 0) {
+                // TODO: the MarkerNote segment.
+            } else {
+                List<String> tagData = exifData.get(ifdId).get(id);
+                if (tagData == null) {
+                    tagData = new ArrayList<String>();
+                    exifData.get(ifdId).put(id, tagData);
+                }
+                if (NO_VALUE.equals(value)) {
+                    tagData.add(null);
+                } else {
+                    tagData.add(value.trim());
+                }
+            }
+
+            parser.require(XmlPullParser.END_TAG, null, null);
+        }
+        return exifData;
+    }
+
+    static private int getIfdIdFromString(String prefix) {
+        if (IFD0.equals(prefix)) {
+            return IfdId.TYPE_IFD_0;
+        } else if (EXIF_IFD.equals(prefix)) {
+            return IfdId.TYPE_IFD_EXIF;
+        } else if (GPS_IFD.equals(prefix)) {
+            return IfdId.TYPE_IFD_GPS;
+        } else if (IFD1.equals(prefix)) {
+            return IfdId.TYPE_IFD_1;
+        } else if (INTEROP_IFD.equals(prefix)) {
+            return IfdId.TYPE_IFD_INTEROPERABILITY;
+        } else {
+            assert (false);
+            return -1;
+        }
+    }
+
+    static public int getTrueTagNumber(Map<Short, List<String>> ifdData) {
+        int size = 0;
+        for (List<String> tag : ifdData.values()) {
+            size += tag.size();
+        }
+        return size;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/exif/Util.java b/tests/src/com/android/gallery3d/exif/Util.java
new file mode 100644
index 0000000..15de007
--- /dev/null
+++ b/tests/src/com/android/gallery3d/exif/Util.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.exif;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+class Util {
+    public static boolean equals(Object a, Object b) {
+        return (a == b) || (a == null ? false : a.equals(b));
+    }
+
+    public static void closeSilently(Closeable c) {
+        if (c == null)
+            return;
+        try {
+            c.close();
+        } catch (Throwable t) {
+            // do nothing
+        }
+    }
+
+    public static byte[] readToByteArray(InputStream is) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        int len;
+        byte[] buf = new byte[1024];
+        while ((len = is.read(buf)) > -1) {
+            bos.write(buf, 0, len);
+        }
+        bos.flush();
+        return bos.toByteArray();
+    }
+
+    /**
+     * Tags that are not defined in the spec.
+     */
+    static final short TAG_XP_TITLE = (short) 0x9c9b;
+    static final short TAG_XP_COMMENT = (short) 0x9c9c;
+    static final short TAG_XP_AUTHOR = (short) 0x9c9d;
+    static final short TAG_XP_KEYWORDS = (short) 0x9c9e;
+    static final short TAG_XP_SUBJECT = (short) 0x9c9f;
+
+    private static String tagUndefinedTypeValueToString(ExifTag tag) {
+        StringBuilder sbuilder = new StringBuilder();
+        byte[] buf = new byte[tag.getComponentCount()];
+        tag.getBytes(buf);
+        short tagId = tag.getTagId();
+        if (tagId == ExifInterface.getTrueTagKey(ExifInterface.TAG_COMPONENTS_CONFIGURATION)) {
+            for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                if (i != 0) {
+                    sbuilder.append(" ");
+                }
+                sbuilder.append(buf[i]);
+            }
+        } else {
+            if (buf.length == 1) {
+                sbuilder.append(buf[0]);
+            } else {
+                for (int i = 0, n = buf.length; i < n; i++) {
+                    byte code = buf[i];
+                    if (code == 0) {
+                        continue;
+                    }
+                    if (code > 31 && code < 127) {
+                        sbuilder.append((char) code);
+                    } else {
+                        sbuilder.append('.');
+                    }
+                }
+            }
+        }
+        return sbuilder.toString();
+    }
+
+    /**
+     * Returns a string representation of the value of this tag.
+     */
+    public static String tagValueToString(ExifTag tag) {
+        StringBuilder sbuilder = new StringBuilder();
+        short id = tag.getTagId();
+        switch (tag.getDataType()) {
+            case ExifTag.TYPE_UNDEFINED:
+                sbuilder.append(tagUndefinedTypeValueToString(tag));
+                break;
+            case ExifTag.TYPE_UNSIGNED_BYTE:
+                if (id == ExifInterface.TAG_MAKER_NOTE || id == TAG_XP_TITLE ||
+                        id == TAG_XP_COMMENT || id == TAG_XP_AUTHOR ||
+                        id == TAG_XP_KEYWORDS || id == TAG_XP_SUBJECT) {
+                    sbuilder.append(tagUndefinedTypeValueToString(tag));
+                } else {
+                    byte[] buf = new byte[tag.getComponentCount()];
+                    tag.getBytes(buf);
+                    for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                        if (i != 0)
+                            sbuilder.append(" ");
+                        sbuilder.append(buf[i]);
+                    }
+                }
+                break;
+            case ExifTag.TYPE_ASCII:
+                byte[] buf = tag.getStringByte();
+                for (int i = 0, n = buf.length; i < n; i++) {
+                    byte code = buf[i];
+                    if (code == 0) {
+                        // Treat some tag as undefined type data.
+                        if (id == ExifInterface.TAG_COPYRIGHT
+                                || id == ExifInterface.TAG_GPS_DATE_STAMP) {
+                            continue;
+                        } else {
+                            break;
+                        }
+                    }
+                    if (code > 31 && code < 127) {
+                        sbuilder.append((char) code);
+                    } else {
+                        sbuilder.append('.');
+                    }
+                }
+                break;
+            case ExifTag.TYPE_UNSIGNED_LONG:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
+                    sbuilder.append(tag.getValueAt(i));
+                }
+                break;
+            case ExifTag.TYPE_RATIONAL:
+            case ExifTag.TYPE_UNSIGNED_RATIONAL:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    Rational r = tag.getRational(i);
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
+                    sbuilder.append(r.getNumerator()).append("/").append(r.getDenominator());
+                }
+                break;
+            case ExifTag.TYPE_UNSIGNED_SHORT:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
+                    sbuilder.append((int) tag.getValueAt(i));
+                }
+                break;
+            case ExifTag.TYPE_LONG:
+                for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+                    if (i != 0) {
+                        sbuilder.append(" ");
+                    }
+                    sbuilder.append((int) tag.getValueAt(i));
+                }
+                break;
+        }
+        return sbuilder.toString();
+    }
+
+    public static String valueToString(Object obj) {
+        if (obj instanceof int[]) {
+            return Arrays.toString((int[]) obj);
+        } else if (obj instanceof Integer[]) {
+            return Arrays.toString((Integer[]) obj);
+        } else if (obj instanceof long[]) {
+            return Arrays.toString((long[]) obj);
+        } else if (obj instanceof Long[]) {
+            return Arrays.toString((Long[]) obj);
+        } else if (obj instanceof Rational) {
+            return ((Rational) obj).toString();
+        } else if (obj instanceof Rational[]) {
+            return Arrays.toString((Rational[]) obj);
+        } else if (obj instanceof byte[]) {
+            return Arrays.toString((byte[]) obj);
+        } else if (obj != null) {
+            return obj.toString();
+        }
+        return "";
+    }
+}
diff --git a/tests/src/com/android/gallery3d/functional/CameraTest.java b/tests/src/com/android/gallery3d/functional/CameraTest.java
new file mode 100644
index 0000000..c293c0d
--- /dev/null
+++ b/tests/src/com/android/gallery3d/functional/CameraTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.functional;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Process;
+import android.provider.MediaStore;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class CameraTest extends InstrumentationTestCase {
+    @LargeTest
+    public void testVideoCaptureIntentFdLeak() throws Exception {
+        Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.parse("file://"
+                + Environment.getExternalStorageDirectory().toString()
+                + "test_fd_leak.3gp"));
+        getInstrumentation().startActivitySync(intent).finish();
+        // Test if the fd is closed.
+        for (File f: new File("/proc/" + Process.myPid() + "/fd").listFiles()) {
+            assertEquals(-1, f.getCanonicalPath().indexOf("test_fd_leak.3gp"));
+        }
+    }
+
+    @LargeTest
+    public void testActivityLeak() throws Exception {
+        checkActivityLeak(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
+        checkActivityLeak(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+    }
+
+    private void checkActivityLeak(String action) throws Exception {
+        final int TEST_COUNT = 5;
+        Intent intent = new Intent(action);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setClass(getInstrumentation().getTargetContext(),
+                CameraActivity.class);
+        ArrayList<WeakReference<Activity>> refs =
+                new ArrayList<WeakReference<Activity>>();
+        for (int i = 0; i < TEST_COUNT; i++) {
+            Activity activity = getInstrumentation().startActivitySync(intent);
+            refs.add(new WeakReference<Activity>(activity));
+            activity.finish();
+            getInstrumentation().waitForIdleSync();
+            activity = null;
+        }
+        Runtime.getRuntime().gc();
+        Runtime.getRuntime().runFinalization();
+        Runtime.getRuntime().gc();
+        int refCount = 0;
+        for (WeakReference<Activity> c: refs) {
+            if (c.get() != null) refCount++;
+        }
+        // If applications are leaking activity, every reference is reachable.
+        assertTrue(refCount != TEST_COUNT);
+    }
+}
diff --git a/tests/src/com/android/gallery3d/functional/ImageCaptureIntentTest.java b/tests/src/com/android/gallery3d/functional/ImageCaptureIntentTest.java
new file mode 100644
index 0000000..8d394b5
--- /dev/null
+++ b/tests/src/com/android/gallery3d/functional/ImageCaptureIntentTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.functional;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+
+public class ImageCaptureIntentTest extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private Intent mIntent;
+
+    public ImageCaptureIntentTest() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+    }
+
+    @LargeTest
+    public void testNoExtraOutput() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        takePicture();
+        pressDone();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_OK, getActivity().getResultCode());
+        Intent resultData = getActivity().getResultData();
+        Bitmap bitmap = (Bitmap) resultData.getParcelableExtra("data");
+        assertNotNull(bitmap);
+        assertTrue(bitmap.getWidth() > 0);
+        assertTrue(bitmap.getHeight() > 0);
+    }
+
+    @LargeTest
+    public void testExtraOutput() throws Exception {
+        File file = new File(Environment.getExternalStorageDirectory(),
+            "test.jpg");
+        BufferedInputStream stream = null;
+        byte[] jpegData;
+
+        try {
+            mIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
+            setActivityIntent(mIntent);
+            getActivity();
+
+            takePicture();
+            pressDone();
+
+            assertTrue(getActivity().isFinishing());
+            assertEquals(Activity.RESULT_OK, getActivity().getResultCode());
+
+            // Verify the jpeg file
+            int fileLength = (int) file.length();
+            assertTrue(fileLength > 0);
+            jpegData = new byte[fileLength];
+            stream = new BufferedInputStream(new FileInputStream(file));
+            stream.read(jpegData);
+        } finally {
+            if (stream != null) stream.close();
+            file.delete();
+        }
+
+        Bitmap b = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
+        assertTrue(b.getWidth() > 0);
+        assertTrue(b.getHeight() > 0);
+    }
+
+    @LargeTest
+    public void testCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    @LargeTest
+    public void testSnapshotCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        takePicture();
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    private void takePicture() throws Exception {
+        getInstrumentation().sendKeySync(
+                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FOCUS));
+        getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+        Thread.sleep(4000);
+    }
+
+    private void pressDone() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_done).performClick();
+            }
+        });
+    }
+
+    private void pressCancel() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_cancel).performClick();
+            }
+        });
+    }
+}
diff --git a/tests/src/com/android/gallery3d/functional/VideoCaptureIntentTest.java b/tests/src/com/android/gallery3d/functional/VideoCaptureIntentTest.java
new file mode 100644
index 0000000..c8d7bbb
--- /dev/null
+++ b/tests/src/com/android/gallery3d/functional/VideoCaptureIntentTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.functional;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.R;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.io.File;
+
+public class VideoCaptureIntentTest extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private static final String TAG = "VideoCaptureIntentTest";
+    private Intent mIntent;
+    private Uri mVideoUri;
+    private File mFile, mFile2;
+
+    public VideoCaptureIntentTest() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mVideoUri != null) {
+            ContentResolver resolver = getActivity().getContentResolver();
+            Uri query = mVideoUri.buildUpon().build();
+            String[] projection = new String[] {VideoColumns.DATA};
+
+            Cursor cursor = null;
+            try {
+                cursor = resolver.query(query, projection, null, null, null);
+                if (cursor != null && cursor.moveToFirst()) {
+                    new File(cursor.getString(0)).delete();
+                }
+            } finally {
+                if (cursor != null) cursor.close();
+            }
+
+            resolver.delete(mVideoUri, null, null);
+        }
+        if (mFile != null) mFile.delete();
+        if (mFile2 != null) mFile2.delete();
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testNoExtraOutput() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        Intent resultData = getActivity().getResultData();
+        mVideoUri = resultData.getData();
+        assertNotNull(mVideoUri);
+        verify(getActivity(), mVideoUri);
+    }
+
+    @LargeTest
+    public void testExtraOutput() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        verify(getActivity(), uri);
+    }
+
+    @LargeTest
+    public void testCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    @LargeTest
+    public void testRecordCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    @LargeTest
+    public void testExtraSizeLimit() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+        final long sizeLimit = 500000;  // bytes
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, sizeLimit);
+        mIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);  // use low quality to speed up
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo(5000);
+        pressDone();
+
+        verify(getActivity(), uri);
+        long length = mFile.length();
+        Log.v(TAG, "Video size is " + length + " bytes.");
+        assertTrue(length > 0);
+        assertTrue("Actual size=" + length, length <= sizeLimit);
+    }
+
+    @LargeTest
+    public void testExtraDurationLimit() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+        final int durationLimit = 2;  // seconds
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, durationLimit);
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo(5000);
+        pressDone();
+
+        int duration = verify(getActivity(), uri);
+        // The duraion should be close to to the limit. The last video duration
+        // also has duration, so the total duration may exceeds the limit a
+        // little bit.
+        Log.v(TAG, "Video length is " + duration + " ms.");
+        assertTrue(duration  < (durationLimit + 1) * 1000);
+    }
+
+    @LargeTest
+    public void testExtraVideoQuality() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+        mFile2 = new File(Environment.getExternalStorageDirectory(), "video2.tmp");
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);  // low quality
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        verify(getActivity(), uri);
+        setActivity(null);
+
+        uri = Uri.fromFile(mFile2);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);  // high quality
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        verify(getActivity(), uri);
+        assertTrue(mFile.length() <= mFile2.length());
+    }
+
+    // Verify result code, result data, and the duration.
+    private int verify(CameraActivity activity, Uri uri) throws Exception {
+        assertTrue(activity.isFinishing());
+        assertEquals(Activity.RESULT_OK, activity.getResultCode());
+
+        // Verify the video file
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        retriever.setDataSource(activity, uri);
+        String duration = retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_DURATION);
+        assertNotNull(duration);
+        int durationValue = Integer.parseInt(duration);
+        Log.v(TAG, "Video duration is " + durationValue);
+        assertTrue(durationValue > 0);
+        return durationValue;
+    }
+
+    private void recordVideo(int ms) throws Exception {
+        getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+        Thread.sleep(ms);
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                // If recording is in progress, stop it. Run these atomically in
+                // UI thread.
+                CameraActivity activity = getActivity();
+                if (activity.isRecording()) {
+                    activity.findViewById(R.id.shutter_button).performClick();
+                }
+            }
+        });
+    }
+
+    private void recordVideo() throws Exception {
+        recordVideo(2000);
+    }
+
+    private void pressDone() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_done).performClick();
+            }
+        });
+    }
+
+    private void pressCancel() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_cancel).performClick();
+            }
+        });
+    }
+}
diff --git a/tests/src/com/android/gallery3d/glrenderer/GLCanvasMock.java b/tests/src/com/android/gallery3d/glrenderer/GLCanvasMock.java
new file mode 100644
index 0000000..a57c188
--- /dev/null
+++ b/tests/src/com/android/gallery3d/glrenderer/GLCanvasMock.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.ui.GLCanvasStub;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class GLCanvasMock extends GLCanvasStub {
+    // fillRect
+    int mFillRectCalled;
+    float mFillRectWidth;
+    float mFillRectHeight;
+    int mFillRectColor;
+    // drawMixed
+    int mDrawMixedCalled;
+    float mDrawMixedRatio;
+    // drawTexture;
+    int mDrawTextureCalled;
+
+    private GL11 mGL;
+
+    public GLCanvasMock(GL11 gl) {
+        mGL = gl;
+    }
+
+    public GLCanvasMock() {
+        mGL = new GLStub();
+    }
+
+    @Override
+    public GL11 getGLInstance() {
+        return mGL;
+    }
+
+    @Override
+    public void fillRect(float x, float y, float width, float height, int color) {
+        mFillRectCalled++;
+        mFillRectWidth = width;
+        mFillRectHeight = height;
+        mFillRectColor = color;
+    }
+
+    @Override
+    public void drawTexture(
+                BasicTexture texture, int x, int y, int width, int height) {
+        mDrawTextureCalled++;
+    }
+
+    @Override
+    public void drawMixed(BasicTexture from, BasicTexture to,
+            float ratio, int x, int y, int w, int h) {
+        mDrawMixedCalled++;
+        mDrawMixedRatio = ratio;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/glrenderer/GLCanvasTest.java b/tests/src/com/android/gallery3d/glrenderer/GLCanvasTest.java
new file mode 100644
index 0000000..416c114
--- /dev/null
+++ b/tests/src/com/android/gallery3d/glrenderer/GLCanvasTest.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+public class GLCanvasTest extends TestCase {
+    private static final String TAG = "GLCanvasTest";
+
+    private static GLPaint newColorPaint(int color) {
+        GLPaint paint = new GLPaint();
+        paint.setColor(color);
+        return paint;
+    }
+
+    @SmallTest
+    public void testSetSize() {
+        GL11 glStub = new GLStub();
+        GLCanvas canvas = new GLES11Canvas(glStub);
+        canvas.setSize(100, 200);
+        canvas.setSize(1000, 100);
+        try {
+            canvas.setSize(-1, 100);
+            fail();
+        } catch (Throwable ex) {
+            // expected.
+        }
+    }
+
+    @SmallTest
+    public void testClearBuffer() {
+        new ClearBufferTest().run();
+    }
+
+    private static class ClearBufferTest extends GLMock {
+        void run() {
+            GLCanvas canvas = new GLES11Canvas(this);
+            assertEquals(0, mGLClearCalled);
+            canvas.clearBuffer();
+            assertEquals(GL10.GL_COLOR_BUFFER_BIT, mGLClearMask);
+            assertEquals(1, mGLClearCalled);
+        }
+    }
+
+    @SmallTest
+    public void testSetColor() {
+        new SetColorTest().run();
+    }
+
+    // This test assumes we use pre-multipled alpha blending and should
+    // set the blending function and color correctly.
+    private static class SetColorTest extends GLMock {
+        void run() {
+            int[] testColors = new int[] {
+                0, 0xFFFFFFFF, 0xFF000000, 0x00FFFFFF, 0x80FF8001,
+                0x7F010101, 0xFEFEFDFC, 0x017F8081, 0x027F8081, 0x2ADE4C4D
+            };
+
+            GLCanvas canvas = new GLES11Canvas(this);
+            canvas.setSize(400, 300);
+            // Test one color to make sure blend function is set.
+            assertEquals(0, mGLColorCalled);
+            canvas.drawLine(0, 0, 1, 1, newColorPaint(0x7F804020));
+            assertEquals(1, mGLColorCalled);
+            assertEquals(0x7F402010, mGLColor);
+            assertPremultipliedBlending(this);
+
+            // Test other colors to make sure premultiplication is right
+            for (int c : testColors) {
+                float a = (c >>> 24) / 255f;
+                float r = ((c >> 16) & 0xff) / 255f;
+                float g = ((c >> 8) & 0xff) / 255f;
+                float b = (c & 0xff) / 255f;
+                int pre = makeColor4f(a * r, a * g, a * b, a);
+
+                mGLColorCalled = 0;
+                canvas.drawLine(0, 0, 1, 1, newColorPaint(c));
+                assertEquals(1, mGLColorCalled);
+                assertEquals(pre, mGLColor);
+            }
+        }
+    }
+
+    @SmallTest
+    public void testSetGetMultiplyAlpha() {
+        GL11 glStub = new GLStub();
+        GLCanvas canvas = new GLES11Canvas(glStub);
+
+        canvas.setAlpha(1f);
+        assertEquals(1f, canvas.getAlpha());
+
+        canvas.setAlpha(0f);
+        assertEquals(0f, canvas.getAlpha());
+
+        canvas.setAlpha(0.5f);
+        assertEquals(0.5f, canvas.getAlpha());
+
+        canvas.multiplyAlpha(0.5f);
+        assertEquals(0.25f, canvas.getAlpha());
+
+        canvas.multiplyAlpha(0f);
+        assertEquals(0f, canvas.getAlpha());
+
+        try {
+            canvas.setAlpha(-0.01f);
+            fail();
+        } catch (Throwable ex) {
+            // expected.
+        }
+
+        try {
+            canvas.setAlpha(1.01f);
+            fail();
+        } catch (Throwable ex) {
+            // expected.
+        }
+    }
+
+    @SmallTest
+    public void testAlpha() {
+        new AlphaTest().run();
+    }
+
+    private static class AlphaTest extends GLMock {
+        void run() {
+            GLCanvas canvas = new GLES11Canvas(this);
+            canvas.setSize(400, 300);
+
+            assertEquals(0, mGLColorCalled);
+            canvas.setAlpha(0.48f);
+            canvas.drawLine(0, 0, 1, 1, newColorPaint(0xFF804020));
+            assertPremultipliedBlending(this);
+            assertEquals(1, mGLColorCalled);
+            assertEquals(0x7A3D1F0F, mGLColor);
+        }
+    }
+
+    @SmallTest
+    public void testDrawLine() {
+        new DrawLineTest().run();
+    }
+
+    // This test assumes the drawLine() function use glDrawArrays() with
+    // GL_LINE_STRIP mode to draw the line and the input coordinates are used
+    // directly.
+    private static class DrawLineTest extends GLMock {
+        private int mDrawArrayCalled = 0;
+        private final int[] mResult = new int[4];
+
+        @Override
+        public void glDrawArrays(int mode, int first, int count) {
+            assertNotNull(mGLVertexPointer);
+            assertEquals(GL10.GL_LINE_STRIP, mode);
+            assertEquals(2, count);
+            mGLVertexPointer.bindByteBuffer();
+
+            double[] coord = new double[4];
+            mGLVertexPointer.getArrayElement(first, coord);
+            mResult[0] = (int) coord[0];
+            mResult[1] = (int) coord[1];
+            mGLVertexPointer.getArrayElement(first + 1, coord);
+            mResult[2] = (int) coord[0];
+            mResult[3] = (int) coord[1];
+            mDrawArrayCalled++;
+        }
+
+        void run() {
+            GLCanvas canvas = new GLES11Canvas(this);
+            canvas.setSize(400, 300);
+            canvas.drawLine(2, 7, 1, 8, newColorPaint(0) /* color */);
+            assertTrue(mGLVertexArrayEnabled);
+            assertEquals(1, mDrawArrayCalled);
+
+            Log.v(TAG, "result = " + Arrays.toString(mResult));
+            int[] answer = new int[] {2, 7, 1, 8};
+            for (int i = 0; i < answer.length; i++) {
+                assertEquals(answer[i], mResult[i]);
+            }
+        }
+    }
+
+    @SmallTest
+    public void testFillRect() {
+        new FillRectTest().run();
+    }
+
+    // This test assumes the drawLine() function use glDrawArrays() with
+    // GL_TRIANGLE_STRIP mode to draw the line and the input coordinates
+    // are used directly.
+    private static class FillRectTest extends GLMock {
+        private int mDrawArrayCalled = 0;
+        private final int[] mResult = new int[8];
+
+        @Override
+        public void glDrawArrays(int mode, int first, int count) {
+            assertNotNull(mGLVertexPointer);
+            assertEquals(GL10.GL_TRIANGLE_STRIP, mode);
+            assertEquals(4, count);
+            mGLVertexPointer.bindByteBuffer();
+
+            double[] coord = new double[4];
+            for (int i = 0; i < 4; i++) {
+                mGLVertexPointer.getArrayElement(first + i, coord);
+                mResult[i * 2 + 0] = (int) coord[0];
+                mResult[i * 2 + 1] = (int) coord[1];
+            }
+
+            mDrawArrayCalled++;
+        }
+
+        void run() {
+            GLCanvas canvas = new GLES11Canvas(this);
+            canvas.setSize(400, 300);
+            canvas.fillRect(2, 7, 1, 8, 0 /* color */);
+            assertTrue(mGLVertexArrayEnabled);
+            assertEquals(1, mDrawArrayCalled);
+            Log.v(TAG, "result = " + Arrays.toString(mResult));
+
+            // These are the four vertics that should be used.
+            int[] answer = new int[] {
+                2, 7,
+                3, 7,
+                3, 15,
+                2, 15};
+            int count[] = new int[4];
+
+            // Count the number of appearances for each vertex.
+            for (int i = 0; i < 4; i++) {
+                for (int j = 0; j < 4; j++) {
+                    if (answer[i * 2] == mResult[j * 2] &&
+                        answer[i * 2 + 1] == mResult[j * 2 + 1]) {
+                        count[i]++;
+                    }
+                }
+            }
+
+            // Each vertex should appear exactly once.
+            for (int i = 0; i < 4; i++) {
+                assertEquals(1, count[i]);
+            }
+        }
+    }
+
+    @SmallTest
+    public void testTransform() {
+        new TransformTest().run();
+    }
+
+    // This test assumes glLoadMatrixf is used to load the model view matrix,
+    // and glOrthof is used to load the projection matrix.
+    //
+    // The projection matrix is set to an orthogonal projection which is the
+    // inverse of viewport transform. So the model view matrix maps input
+    // directly to screen coordinates (default no scaling, and the y-axis is
+    // reversed).
+    //
+    // The matrix here are all listed in column major order.
+    //
+    private static class TransformTest extends GLMock {
+        private final float[] mModelViewMatrixUsed = new float[16];
+        private final float[] mProjectionMatrixUsed = new float[16];
+
+        @Override
+        public void glDrawArrays(int mode, int first, int count) {
+            copy(mModelViewMatrixUsed, mGLModelViewMatrix);
+            copy(mProjectionMatrixUsed, mGLProjectionMatrix);
+        }
+
+        private void copy(float[] dest, float[] src) {
+            System.arraycopy(src, 0, dest, 0, 16);
+        }
+
+        void run() {
+            GLCanvas canvas = new GLES11Canvas(this);
+            canvas.setSize(40, 50);
+            int color = 0;
+
+            // Initial matrix
+            canvas.drawLine(0, 0, 1, 1, newColorPaint(color));
+            assertMatrixEq(new float[] {
+                    1,  0, 0, 0,
+                    0, -1, 0, 0,
+                    0,  0, 1, 0,
+                    0, 50, 0, 1
+                    }, mModelViewMatrixUsed);
+
+            assertMatrixEq(new float[] {
+                    2f / 40,       0,  0, 0,
+                          0, 2f / 50,  0, 0,
+                          0,       0, -1, 0,
+                         -1,      -1,  0, 1
+                    }, mProjectionMatrixUsed);
+
+            // Translation
+            canvas.translate(3, 4, 5);
+            canvas.drawLine(0, 0, 1, 1, newColorPaint(color));
+            assertMatrixEq(new float[] {
+                    1,  0, 0, 0,
+                    0, -1, 0, 0,
+                    0,  0, 1, 0,
+                    3, 46, 5, 1
+                    }, mModelViewMatrixUsed);
+            canvas.save();
+
+            // Scaling
+            canvas.scale(0.7f, 0.6f, 0.5f);
+            canvas.drawLine(0, 0, 1, 1, newColorPaint(color));
+            assertMatrixEq(new float[] {
+                    0.7f,     0,    0, 0,
+                    0,    -0.6f,    0, 0,
+                    0,        0, 0.5f, 0,
+                    3,       46,    5, 1
+                    }, mModelViewMatrixUsed);
+
+            // Rotation
+            canvas.rotate(90, 0, 0, 1);
+            canvas.drawLine(0, 0, 1, 1, newColorPaint(color));
+            assertMatrixEq(new float[] {
+                        0, -0.6f,    0, 0,
+                    -0.7f,     0,    0, 0,
+                        0,     0, 0.5f, 0,
+                        3,    46,    5, 1
+                    }, mModelViewMatrixUsed);
+            canvas.restore();
+
+            // After restoring to the point just after translation,
+            // do rotation again.
+            canvas.rotate(180, 1, 0, 0);
+            canvas.drawLine(0, 0, 1, 1, newColorPaint(color));
+            assertMatrixEq(new float[] {
+                    1,  0,  0, 0,
+                    0,  1,  0, 0,
+                    0,  0, -1, 0,
+                    3, 46,  5, 1
+                    }, mModelViewMatrixUsed);
+        }
+    }
+
+    private static void assertPremultipliedBlending(GLMock mock) {
+        assertTrue(mock.mGLBlendFuncCalled > 0);
+        assertTrue(mock.mGLBlendEnabled);
+        assertEquals(GL11.GL_ONE, mock.mGLBlendFuncSFactor);
+        assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, mock.mGLBlendFuncDFactor);
+    }
+
+    private static void assertMatrixEq(float[] expected, float[] actual) {
+        try {
+            for (int i = 0; i < 16; i++) {
+                assertFloatEq(expected[i], actual[i]);
+            }
+        } catch (Throwable t) {
+            Log.v(TAG, "expected = " + Arrays.toString(expected) +
+                    ", actual = " + Arrays.toString(actual));
+            fail();
+        }
+    }
+
+    public static void assertFloatEq(float expected, float actual) {
+        if (Math.abs(actual - expected) > 1e-6) {
+            Log.v(TAG, "expected: " + expected + ", actual: " + actual);
+            fail();
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/glrenderer/GLMock.java b/tests/src/com/android/gallery3d/glrenderer/GLMock.java
new file mode 100644
index 0000000..b242217
--- /dev/null
+++ b/tests/src/com/android/gallery3d/glrenderer/GLMock.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import com.android.gallery3d.ui.PointerInfo;
+
+import java.nio.Buffer;
+import java.util.HashMap;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+public class GLMock extends GLStub {
+    @SuppressWarnings("unused")
+    private static final String TAG = "GLMock";
+
+    // glClear
+    int mGLClearCalled;
+    int mGLClearMask;
+    // glBlendFunc
+    int mGLBlendFuncCalled;
+    int mGLBlendFuncSFactor;
+    int mGLBlendFuncDFactor;
+    // glColor4[fx]
+    int mGLColorCalled;
+    int mGLColor;
+    // glEnable, glDisable
+    boolean mGLBlendEnabled;
+    boolean mGLStencilEnabled;
+    // glEnableClientState
+    boolean mGLVertexArrayEnabled;
+    // glVertexPointer
+    PointerInfo mGLVertexPointer;
+    // glMatrixMode
+    int mGLMatrixMode = GL10.GL_MODELVIEW;
+    // glLoadMatrixf
+    float[] mGLModelViewMatrix = new float[16];
+    float[] mGLProjectionMatrix = new float[16];
+    // glBindTexture
+    int mGLBindTextureId;
+    // glTexEnvf
+    HashMap<Integer, Float> mGLTexEnv0 = new HashMap<Integer, Float>();
+    HashMap<Integer, Float> mGLTexEnv1 = new HashMap<Integer, Float>();
+    // glActiveTexture
+    int mGLActiveTexture = GL11.GL_TEXTURE0;
+
+    @Override
+    public void glClear(int mask) {
+        mGLClearCalled++;
+        mGLClearMask = mask;
+    }
+
+    @Override
+    public void glBlendFunc(int sfactor, int dfactor) {
+        mGLBlendFuncSFactor = sfactor;
+        mGLBlendFuncDFactor = dfactor;
+        mGLBlendFuncCalled++;
+    }
+
+    @Override
+    public void glColor4f(float red, float green, float blue,
+        float alpha) {
+        mGLColorCalled++;
+        mGLColor = makeColor4f(red, green, blue, alpha);
+    }
+
+    @Override
+    public void glColor4x(int red, int green, int blue, int alpha) {
+        mGLColorCalled++;
+        mGLColor = makeColor4x(red, green, blue, alpha);
+    }
+
+    @Override
+    public void glEnable(int cap) {
+        if (cap == GL11.GL_BLEND) {
+            mGLBlendEnabled = true;
+        } else if (cap == GL11.GL_STENCIL_TEST) {
+            mGLStencilEnabled = true;
+        }
+    }
+
+    @Override
+    public void glDisable(int cap) {
+        if (cap == GL11.GL_BLEND) {
+            mGLBlendEnabled = false;
+        } else if (cap == GL11.GL_STENCIL_TEST) {
+            mGLStencilEnabled = false;
+        }
+    }
+
+    @Override
+    public void glEnableClientState(int array) {
+        if (array == GL10.GL_VERTEX_ARRAY) {
+           mGLVertexArrayEnabled = true;
+        }
+    }
+
+    @Override
+    public void glVertexPointer(int size, int type, int stride, Buffer pointer) {
+        mGLVertexPointer = new PointerInfo(size, type, stride, pointer);
+    }
+
+    @Override
+    public void glMatrixMode(int mode) {
+        mGLMatrixMode = mode;
+    }
+
+    @Override
+    public void glLoadMatrixf(float[] m, int offset) {
+        if (mGLMatrixMode == GL10.GL_MODELVIEW) {
+            System.arraycopy(m, offset, mGLModelViewMatrix, 0, 16);
+        } else if (mGLMatrixMode == GL10.GL_PROJECTION) {
+            System.arraycopy(m, offset, mGLProjectionMatrix, 0, 16);
+        }
+    }
+
+    @Override
+    public void glOrthof(
+        float left, float right, float bottom, float top,
+        float zNear, float zFar) {
+        float tx = -(right + left) / (right - left);
+        float ty = -(top + bottom) / (top - bottom);
+            float tz = - (zFar + zNear) / (zFar - zNear);
+            float[] m = new float[] {
+                    2 / (right - left), 0, 0,  0,
+                    0, 2 / (top - bottom), 0,  0,
+                    0, 0, -2 / (zFar - zNear), 0,
+                    tx, ty, tz, 1
+            };
+            glLoadMatrixf(m, 0);
+    }
+
+    @Override
+    public void glBindTexture(int target, int texture) {
+        if (target == GL11.GL_TEXTURE_2D) {
+            mGLBindTextureId = texture;
+        }
+    }
+
+    @Override
+    public void glTexEnvf(int target, int pname, float param) {
+        if (target == GL11.GL_TEXTURE_ENV) {
+            if (mGLActiveTexture == GL11.GL_TEXTURE0) {
+                mGLTexEnv0.put(pname, param);
+            } else if (mGLActiveTexture == GL11.GL_TEXTURE1) {
+                mGLTexEnv1.put(pname, param);
+            } else {
+                throw new AssertionError();
+            }
+        }
+    }
+
+    public int getTexEnvi(int pname) {
+        return getTexEnvi(mGLActiveTexture, pname);
+    }
+
+    public int getTexEnvi(int activeTexture, int pname) {
+        if (activeTexture == GL11.GL_TEXTURE0) {
+            return (int) mGLTexEnv0.get(pname).floatValue();
+        } else if (activeTexture == GL11.GL_TEXTURE1) {
+            return (int) mGLTexEnv1.get(pname).floatValue();
+        } else {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void glActiveTexture(int texture) {
+        mGLActiveTexture = texture;
+    }
+
+    public static int makeColor4f(float red, float green, float blue,
+            float alpha) {
+        return (Math.round(alpha * 255) << 24) |
+                (Math.round(red * 255) << 16) |
+                (Math.round(green * 255) << 8) |
+                Math.round(blue * 255);
+    }
+
+    public static int makeColor4x(int red, int green, int blue, int alpha) {
+        final float X = 65536f;
+        return makeColor4f(red / X, green / X, blue / X, alpha / X);
+    }
+}
diff --git a/tests/src/com/android/gallery3d/glrenderer/GLStub.java b/tests/src/com/android/gallery3d/glrenderer/GLStub.java
new file mode 100644
index 0000000..4b66040
--- /dev/null
+++ b/tests/src/com/android/gallery3d/glrenderer/GLStub.java
@@ -0,0 +1,1490 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import javax.microedition.khronos.opengles.GL;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL10Ext;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+public class GLStub implements GL, GL10, GL10Ext, GL11, GL11Ext {
+    @SuppressWarnings("unused")
+    private static final String TAG = "GLStub";
+
+    public void glActiveTexture(
+        int texture
+    ){}
+
+    public void glAlphaFunc(
+        int func,
+        float ref
+    ){}
+
+    public void glAlphaFuncx(
+        int func,
+        int ref
+    ){}
+
+    public void glBindTexture(
+        int target,
+        int texture
+    ){}
+
+    public void glBlendFunc(
+        int sfactor,
+        int dfactor
+    ){}
+
+    public void glClear(
+        int mask
+    ){}
+
+    public void glClearColor(
+        float red,
+        float green,
+        float blue,
+        float alpha
+    ){}
+
+    public void glClearColorx(
+        int red,
+        int green,
+        int blue,
+        int alpha
+    ){}
+
+    public void glClearDepthf(
+        float depth
+    ){}
+
+    public void glClearDepthx(
+        int depth
+    ){}
+
+    public void glClearStencil(
+        int s
+    ){}
+
+    public void glClientActiveTexture(
+        int texture
+    ){}
+
+    public void glColor4f(
+        float red,
+        float green,
+        float blue,
+        float alpha
+    ){}
+
+    public void glColor4x(
+        int red,
+        int green,
+        int blue,
+        int alpha
+    ){}
+
+    public void glColorMask(
+        boolean red,
+        boolean green,
+        boolean blue,
+        boolean alpha
+    ){}
+
+    public void glColorPointer(
+        int size,
+        int type,
+        int stride,
+        java.nio.Buffer pointer
+    ){}
+
+    public void glCompressedTexImage2D(
+        int target,
+        int level,
+        int internalformat,
+        int width,
+        int height,
+        int border,
+        int imageSize,
+        java.nio.Buffer data
+    ){}
+
+    public void glCompressedTexSubImage2D(
+        int target,
+        int level,
+        int xoffset,
+        int yoffset,
+        int width,
+        int height,
+        int format,
+        int imageSize,
+        java.nio.Buffer data
+    ){}
+
+    public void glCopyTexImage2D(
+        int target,
+        int level,
+        int internalformat,
+        int x,
+        int y,
+        int width,
+        int height,
+        int border
+    ){}
+
+    public void glCopyTexSubImage2D(
+        int target,
+        int level,
+        int xoffset,
+        int yoffset,
+        int x,
+        int y,
+        int width,
+        int height
+    ){}
+
+    public void glCullFace(
+        int mode
+    ){}
+
+    public void glDeleteTextures(
+        int n,
+        int[] textures,
+        int offset
+    ){}
+
+    public void glDeleteTextures(
+        int n,
+        java.nio.IntBuffer textures
+    ){}
+
+    public void glDepthFunc(
+        int func
+    ){}
+
+    public void glDepthMask(
+        boolean flag
+    ){}
+
+    public void glDepthRangef(
+        float zNear,
+        float zFar
+    ){}
+
+    public void glDepthRangex(
+        int zNear,
+        int zFar
+    ){}
+
+    public void glDisable(
+        int cap
+    ){}
+
+    public void glDisableClientState(
+        int array
+    ){}
+
+    public void glDrawArrays(
+        int mode,
+        int first,
+        int count
+    ){}
+
+    public void glDrawElements(
+        int mode,
+        int count,
+        int type,
+        java.nio.Buffer indices
+    ){}
+
+    public void glEnable(
+        int cap
+    ){}
+
+    public void glEnableClientState(
+        int array
+    ){}
+
+    public void glFinish(
+    ){}
+
+    public void glFlush(
+    ){}
+
+    public void glFogf(
+        int pname,
+        float param
+    ){}
+
+    public void glFogfv(
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glFogfv(
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glFogx(
+        int pname,
+        int param
+    ){}
+
+    public void glFogxv(
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glFogxv(
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glFrontFace(
+        int mode
+    ){}
+
+    public void glFrustumf(
+        float left,
+        float right,
+        float bottom,
+        float top,
+        float zNear,
+        float zFar
+    ){}
+
+    public void glFrustumx(
+        int left,
+        int right,
+        int bottom,
+        int top,
+        int zNear,
+        int zFar
+    ){}
+
+    public void glGenTextures(
+        int n,
+        int[] textures,
+        int offset
+    ){}
+
+    public void glGenTextures(
+        int n,
+        java.nio.IntBuffer textures
+    ){}
+
+    public int glGetError(
+    ){ throw new UnsupportedOperationException(); }
+
+    public void glGetIntegerv(
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetIntegerv(
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public String glGetString(
+        int name
+    ){ throw new UnsupportedOperationException(); }
+
+    public void glHint(
+        int target,
+        int mode
+    ){}
+
+    public void glLightModelf(
+        int pname,
+        float param
+    ){}
+
+    public void glLightModelfv(
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glLightModelfv(
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glLightModelx(
+        int pname,
+        int param
+    ){}
+
+    public void glLightModelxv(
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glLightModelxv(
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glLightf(
+        int light,
+        int pname,
+        float param
+    ){}
+
+    public void glLightfv(
+        int light,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glLightfv(
+        int light,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glLightx(
+        int light,
+        int pname,
+        int param
+    ){}
+
+    public void glLightxv(
+        int light,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glLightxv(
+        int light,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glLineWidth(
+        float width
+    ){}
+
+    public void glLineWidthx(
+        int width
+    ){}
+
+    public void glLoadIdentity(
+    ){}
+
+    public void glLoadMatrixf(
+        float[] m,
+        int offset
+    ){}
+
+    public void glLoadMatrixf(
+        java.nio.FloatBuffer m
+    ){}
+
+    public void glLoadMatrixx(
+        int[] m,
+        int offset
+    ){}
+
+    public void glLoadMatrixx(
+        java.nio.IntBuffer m
+    ){}
+
+    public void glLogicOp(
+        int opcode
+    ){}
+
+    public void glMaterialf(
+        int face,
+        int pname,
+        float param
+    ){}
+
+    public void glMaterialfv(
+        int face,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glMaterialfv(
+        int face,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glMaterialx(
+        int face,
+        int pname,
+        int param
+    ){}
+
+    public void glMaterialxv(
+        int face,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glMaterialxv(
+        int face,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glMatrixMode(
+        int mode
+    ){}
+
+    public void glMultMatrixf(
+        float[] m,
+        int offset
+    ){}
+
+    public void glMultMatrixf(
+        java.nio.FloatBuffer m
+    ){}
+
+    public void glMultMatrixx(
+        int[] m,
+        int offset
+    ){}
+
+    public void glMultMatrixx(
+        java.nio.IntBuffer m
+    ){}
+
+    public void glMultiTexCoord4f(
+        int target,
+        float s,
+        float t,
+        float r,
+        float q
+    ){}
+
+    public void glMultiTexCoord4x(
+        int target,
+        int s,
+        int t,
+        int r,
+        int q
+    ){}
+
+    public void glNormal3f(
+        float nx,
+        float ny,
+        float nz
+    ){}
+
+    public void glNormal3x(
+        int nx,
+        int ny,
+        int nz
+    ){}
+
+    public void glNormalPointer(
+        int type,
+        int stride,
+        java.nio.Buffer pointer
+    ){}
+
+    public void glOrthof(
+        float left,
+        float right,
+        float bottom,
+        float top,
+        float zNear,
+        float zFar
+    ){}
+
+    public void glOrthox(
+        int left,
+        int right,
+        int bottom,
+        int top,
+        int zNear,
+        int zFar
+    ){}
+
+    public void glPixelStorei(
+        int pname,
+        int param
+    ){}
+
+    public void glPointSize(
+        float size
+    ){}
+
+    public void glPointSizex(
+        int size
+    ){}
+
+    public void glPolygonOffset(
+        float factor,
+        float units
+    ){}
+
+    public void glPolygonOffsetx(
+        int factor,
+        int units
+    ){}
+
+    public void glPopMatrix(
+    ){}
+
+    public void glPushMatrix(
+    ){}
+
+    public void glReadPixels(
+        int x,
+        int y,
+        int width,
+        int height,
+        int format,
+        int type,
+        java.nio.Buffer pixels
+    ){}
+
+    public void glRotatef(
+        float angle,
+        float x,
+        float y,
+        float z
+    ){}
+
+    public void glRotatex(
+        int angle,
+        int x,
+        int y,
+        int z
+    ){}
+
+    public void glSampleCoverage(
+        float value,
+        boolean invert
+    ){}
+
+    public void glSampleCoveragex(
+        int value,
+        boolean invert
+    ){}
+
+    public void glScalef(
+        float x,
+        float y,
+        float z
+    ){}
+
+    public void glScalex(
+        int x,
+        int y,
+        int z
+    ){}
+
+    public void glScissor(
+        int x,
+        int y,
+        int width,
+        int height
+    ){}
+
+    public void glShadeModel(
+        int mode
+    ){}
+
+    public void glStencilFunc(
+        int func,
+        int ref,
+        int mask
+    ){}
+
+    public void glStencilMask(
+        int mask
+    ){}
+
+    public void glStencilOp(
+        int fail,
+        int zfail,
+        int zpass
+    ){}
+
+    public void glTexCoordPointer(
+        int size,
+        int type,
+        int stride,
+        java.nio.Buffer pointer
+    ){}
+
+    public void glTexEnvf(
+        int target,
+        int pname,
+        float param
+    ){}
+
+    public void glTexEnvfv(
+        int target,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glTexEnvfv(
+        int target,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glTexEnvx(
+        int target,
+        int pname,
+        int param
+    ){}
+
+    public void glTexEnvxv(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glTexEnvxv(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glTexImage2D(
+        int target,
+        int level,
+        int internalformat,
+        int width,
+        int height,
+        int border,
+        int format,
+        int type,
+        java.nio.Buffer pixels
+    ){}
+
+    public void glTexParameterf(
+        int target,
+        int pname,
+        float param
+    ){}
+
+    public void glTexParameterx(
+        int target,
+        int pname,
+        int param
+    ){}
+
+    public void glTexSubImage2D(
+        int target,
+        int level,
+        int xoffset,
+        int yoffset,
+        int width,
+        int height,
+        int format,
+        int type,
+        java.nio.Buffer pixels
+    ){}
+
+    public void glTranslatef(
+        float x,
+        float y,
+        float z
+    ){}
+
+    public void glTranslatex(
+        int x,
+        int y,
+        int z
+    ){}
+
+    public void glVertexPointer(
+        int size,
+        int type,
+        int stride,
+        java.nio.Buffer pointer
+    ){}
+
+    public void glViewport(
+        int x,
+        int y,
+        int width,
+        int height
+    ){}
+
+    public int glQueryMatrixxOES(
+        int[] mantissa,
+        int mantissaOffset,
+        int[] exponent,
+        int exponentOffset
+    ){ throw new UnsupportedOperationException(); }
+
+    public int glQueryMatrixxOES(
+        java.nio.IntBuffer mantissa,
+        java.nio.IntBuffer exponent
+    ){ throw new UnsupportedOperationException(); }
+
+    public void glGetPointerv(int pname, java.nio.Buffer[] params){}
+    public void glBindBuffer(
+        int target,
+        int buffer
+    ){}
+
+    public void glBufferData(
+        int target,
+        int size,
+        java.nio.Buffer data,
+        int usage
+    ){}
+
+    public void glBufferSubData(
+        int target,
+        int offset,
+        int size,
+        java.nio.Buffer data
+    ){}
+
+    public void glClipPlanef(
+        int plane,
+        float[] equation,
+        int offset
+    ){}
+
+    public void glClipPlanef(
+        int plane,
+        java.nio.FloatBuffer equation
+    ){}
+
+    public void glClipPlanex(
+        int plane,
+        int[] equation,
+        int offset
+    ){}
+
+    public void glClipPlanex(
+        int plane,
+        java.nio.IntBuffer equation
+    ){}
+
+    public void glColor4ub(
+        byte red,
+        byte green,
+        byte blue,
+        byte alpha
+    ){}
+
+    public void glColorPointer(
+        int size,
+        int type,
+        int stride,
+        int offset
+    ){}
+
+    public void glDeleteBuffers(
+        int n,
+        int[] buffers,
+        int offset
+    ){}
+
+    public void glDeleteBuffers(
+        int n,
+        java.nio.IntBuffer buffers
+    ){}
+
+    public void glDrawElements(
+        int mode,
+        int count,
+        int type,
+        int offset
+    ){}
+
+    public void glGenBuffers(
+        int n,
+        int[] buffers,
+        int offset
+    ){}
+
+    public void glGenBuffers(
+        int n,
+        java.nio.IntBuffer buffers
+    ){}
+
+    public void glGetBooleanv(
+        int pname,
+        boolean[] params,
+        int offset
+    ){}
+
+    public void glGetBooleanv(
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetBufferParameteriv(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetBufferParameteriv(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetClipPlanef(
+        int pname,
+        float[] eqn,
+        int offset
+    ){}
+
+    public void glGetClipPlanef(
+        int pname,
+        java.nio.FloatBuffer eqn
+    ){}
+
+    public void glGetClipPlanex(
+        int pname,
+        int[] eqn,
+        int offset
+    ){}
+
+    public void glGetClipPlanex(
+        int pname,
+        java.nio.IntBuffer eqn
+    ){}
+
+    public void glGetFixedv(
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetFixedv(
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetFloatv(
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glGetFloatv(
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glGetLightfv(
+        int light,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glGetLightfv(
+        int light,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glGetLightxv(
+        int light,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetLightxv(
+        int light,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetMaterialfv(
+        int face,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glGetMaterialfv(
+        int face,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glGetMaterialxv(
+        int face,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetMaterialxv(
+        int face,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetTexEnviv(
+        int env,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetTexEnviv(
+        int env,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetTexEnvxv(
+        int env,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetTexEnvxv(
+        int env,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetTexParameterfv(
+        int target,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glGetTexParameterfv(
+        int target,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glGetTexParameteriv(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetTexParameteriv(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetTexParameterxv(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetTexParameterxv(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public boolean glIsBuffer(
+        int buffer
+    ){ throw new UnsupportedOperationException(); }
+
+    public boolean glIsEnabled(
+        int cap
+    ){ throw new UnsupportedOperationException(); }
+
+    public boolean glIsTexture(
+        int texture
+    ){ throw new UnsupportedOperationException(); }
+
+    public void glNormalPointer(
+        int type,
+        int stride,
+        int offset
+    ){}
+
+    public void glPointParameterf(
+        int pname,
+        float param
+    ){}
+
+    public void glPointParameterfv(
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glPointParameterfv(
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glPointParameterx(
+        int pname,
+        int param
+    ){}
+
+    public void glPointParameterxv(
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glPointParameterxv(
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glPointSizePointerOES(
+        int type,
+        int stride,
+        java.nio.Buffer pointer
+    ){}
+
+    public void glTexCoordPointer(
+        int size,
+        int type,
+        int stride,
+        int offset
+    ){}
+
+    public void glTexEnvi(
+        int target,
+        int pname,
+        int param
+    ){}
+
+    public void glTexEnviv(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glTexEnviv(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glTexParameterfv(
+        int target,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glTexParameterfv(
+        int target,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glTexParameteri(
+        int target,
+        int pname,
+        int param
+    ){}
+
+    public void glTexParameteriv(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glTexParameteriv(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glTexParameterxv(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glTexParameterxv(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glVertexPointer(
+        int size,
+        int type,
+        int stride,
+        int offset
+    ){}
+
+    public void glCurrentPaletteMatrixOES(
+        int matrixpaletteindex
+    ){}
+
+    public void glDrawTexfOES(
+        float x,
+        float y,
+        float z,
+        float width,
+        float height
+    ){}
+
+    public void glDrawTexfvOES(
+        float[] coords,
+        int offset
+    ){}
+
+    public void glDrawTexfvOES(
+        java.nio.FloatBuffer coords
+    ){}
+
+    public void glDrawTexiOES(
+        int x,
+        int y,
+        int z,
+        int width,
+        int height
+    ){}
+
+    public void glDrawTexivOES(
+        int[] coords,
+        int offset
+    ){}
+
+    public void glDrawTexivOES(
+        java.nio.IntBuffer coords
+    ){}
+
+    public void glDrawTexsOES(
+        short x,
+        short y,
+        short z,
+        short width,
+        short height
+    ){}
+
+    public void glDrawTexsvOES(
+        short[] coords,
+        int offset
+    ){}
+
+    public void glDrawTexsvOES(
+        java.nio.ShortBuffer coords
+    ){}
+
+    public void glDrawTexxOES(
+        int x,
+        int y,
+        int z,
+        int width,
+        int height
+    ){}
+
+    public void glDrawTexxvOES(
+        int[] coords,
+        int offset
+    ){}
+
+    public void glDrawTexxvOES(
+        java.nio.IntBuffer coords
+    ){}
+
+    public void glLoadPaletteFromModelViewMatrixOES(
+    ){}
+
+    public void glMatrixIndexPointerOES(
+        int size,
+        int type,
+        int stride,
+        java.nio.Buffer pointer
+    ){}
+
+    public void glMatrixIndexPointerOES(
+        int size,
+        int type,
+        int stride,
+        int offset
+    ){}
+
+    public void glWeightPointerOES(
+        int size,
+        int type,
+        int stride,
+        java.nio.Buffer pointer
+    ){}
+
+    public void glWeightPointerOES(
+        int size,
+        int type,
+        int stride,
+        int offset
+    ){}
+
+    public void glBindFramebufferOES(
+        int target,
+        int framebuffer
+    ){}
+
+    public void glBindRenderbufferOES(
+        int target,
+        int renderbuffer
+    ){}
+
+    public void glBlendEquation(
+        int mode
+    ){}
+
+    public void glBlendEquationSeparate(
+        int modeRGB,
+        int modeAlpha
+    ){}
+
+    public void glBlendFuncSeparate(
+        int srcRGB,
+        int dstRGB,
+        int srcAlpha,
+        int dstAlpha
+    ){}
+
+    public int glCheckFramebufferStatusOES(
+        int target
+    ){ throw new UnsupportedOperationException(); }
+
+    public void glDeleteFramebuffersOES(
+        int n,
+        int[] framebuffers,
+        int offset
+    ){}
+
+    public void glDeleteFramebuffersOES(
+        int n,
+        java.nio.IntBuffer framebuffers
+    ){}
+
+    public void glDeleteRenderbuffersOES(
+        int n,
+        int[] renderbuffers,
+        int offset
+    ){}
+
+    public void glDeleteRenderbuffersOES(
+        int n,
+        java.nio.IntBuffer renderbuffers
+    ){}
+
+    public void glFramebufferRenderbufferOES(
+        int target,
+        int attachment,
+        int renderbuffertarget,
+        int renderbuffer
+    ){}
+
+    public void glFramebufferTexture2DOES(
+        int target,
+        int attachment,
+        int textarget,
+        int texture,
+        int level
+    ){}
+
+    public void glGenerateMipmapOES(
+        int target
+    ){}
+
+    public void glGenFramebuffersOES(
+        int n,
+        int[] framebuffers,
+        int offset
+    ){}
+
+    public void glGenFramebuffersOES(
+        int n,
+        java.nio.IntBuffer framebuffers
+    ){}
+
+    public void glGenRenderbuffersOES(
+        int n,
+        int[] renderbuffers,
+        int offset
+    ){}
+
+    public void glGenRenderbuffersOES(
+        int n,
+        java.nio.IntBuffer renderbuffers
+    ){}
+
+    public void glGetFramebufferAttachmentParameterivOES(
+        int target,
+        int attachment,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetFramebufferAttachmentParameterivOES(
+        int target,
+        int attachment,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetRenderbufferParameterivOES(
+        int target,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetRenderbufferParameterivOES(
+        int target,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetTexGenfv(
+        int coord,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glGetTexGenfv(
+        int coord,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glGetTexGeniv(
+        int coord,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetTexGeniv(
+        int coord,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glGetTexGenxv(
+        int coord,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glGetTexGenxv(
+        int coord,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public boolean glIsFramebufferOES(
+        int framebuffer
+    ){ throw new UnsupportedOperationException(); }
+
+    public boolean glIsRenderbufferOES(
+        int renderbuffer
+    ){ throw new UnsupportedOperationException(); }
+
+    public void glRenderbufferStorageOES(
+        int target,
+        int internalformat,
+        int width,
+        int height
+    ){}
+
+    public void glTexGenf(
+        int coord,
+        int pname,
+        float param
+    ){}
+
+    public void glTexGenfv(
+        int coord,
+        int pname,
+        float[] params,
+        int offset
+    ){}
+
+    public void glTexGenfv(
+        int coord,
+        int pname,
+        java.nio.FloatBuffer params
+    ){}
+
+    public void glTexGeni(
+        int coord,
+        int pname,
+        int param
+    ){}
+
+    public void glTexGeniv(
+        int coord,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glTexGeniv(
+        int coord,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+
+    public void glTexGenx(
+        int coord,
+        int pname,
+        int param
+    ){}
+
+    public void glTexGenxv(
+        int coord,
+        int pname,
+        int[] params,
+        int offset
+    ){}
+
+    public void glTexGenxv(
+        int coord,
+        int pname,
+        java.nio.IntBuffer params
+    ){}
+}
diff --git a/tests/src/com/android/gallery3d/glrenderer/TextureTest.java b/tests/src/com/android/gallery3d/glrenderer/TextureTest.java
new file mode 100644
index 0000000..9e79554
--- /dev/null
+++ b/tests/src/com/android/gallery3d/glrenderer/TextureTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.ColorTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.GLES11Canvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+
+import junit.framework.TestCase;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class TextureTest extends TestCase {
+    @SuppressWarnings("unused")
+    private static final String TAG = "TextureTest";
+
+    class MyBasicTexture extends BasicTexture {
+        int mOnBindCalled;
+        int mOpaqueCalled;
+
+        MyBasicTexture(GLCanvas canvas, int id) {
+            super(canvas, id, 0);
+        }
+
+        @Override
+        protected boolean onBind(GLCanvas canvas) {
+            mOnBindCalled++;
+            return true;
+        }
+
+        @Override
+        protected int getTarget() {
+            return GL11.GL_TEXTURE_2D;
+        }
+
+        @Override
+        public boolean isOpaque() {
+            mOpaqueCalled++;
+            return true;
+        }
+
+        void upload() {
+            mState = STATE_LOADED;
+        }
+    }
+
+    @SmallTest
+    public void testBasicTexture() {
+        GL11 glStub = new GLStub();
+        GLCanvas canvas = new GLES11Canvas(glStub);
+        MyBasicTexture texture = new MyBasicTexture(canvas, 47);
+
+        assertEquals(47, texture.getId());
+        texture.setSize(1, 1);
+        assertEquals(1, texture.getWidth());
+        assertEquals(1, texture.getHeight());
+        assertEquals(1, texture.getTextureWidth());
+        assertEquals(1, texture.getTextureHeight());
+        texture.setSize(3, 5);
+        assertEquals(3, texture.getWidth());
+        assertEquals(5, texture.getHeight());
+        assertEquals(4, texture.getTextureWidth());
+        assertEquals(8, texture.getTextureHeight());
+
+        assertFalse(texture.isLoaded());
+        texture.upload();
+        assertTrue(texture.isLoaded());
+
+        // For a different GL, it's not loaded.
+        GLCanvas canvas2 = new GLES11Canvas(glStub);
+        assertFalse(texture.isLoaded());
+
+        assertEquals(0, texture.mOnBindCalled);
+        assertEquals(0, texture.mOpaqueCalled);
+        texture.draw(canvas, 100, 200, 1, 1);
+        assertEquals(1, texture.mOnBindCalled);
+        assertEquals(1, texture.mOpaqueCalled);
+        texture.draw(canvas, 0, 0);
+        assertEquals(2, texture.mOnBindCalled);
+        assertEquals(2, texture.mOpaqueCalled);
+    }
+
+    @SmallTest
+    public void testColorTexture() {
+        GLCanvasMock canvas = new GLCanvasMock();
+        ColorTexture texture = new ColorTexture(0x12345678);
+
+        texture.setSize(42, 47);
+        assertEquals(texture.getWidth(), 42);
+        assertEquals(texture.getHeight(), 47);
+        assertEquals(0, canvas.mFillRectCalled);
+        texture.draw(canvas, 0, 0);
+        assertEquals(1, canvas.mFillRectCalled);
+        assertEquals(0x12345678, canvas.mFillRectColor);
+        assertEquals(42f, canvas.mFillRectWidth);
+        assertEquals(47f, canvas.mFillRectHeight);
+        assertFalse(texture.isOpaque());
+        assertTrue(new ColorTexture(0xFF000000).isOpaque());
+    }
+
+    private class MyUploadedTexture extends UploadedTexture {
+        int mGetCalled;
+        int mFreeCalled;
+        Bitmap mBitmap;
+        @Override
+        protected Bitmap onGetBitmap() {
+            mGetCalled++;
+            Config config = Config.ARGB_8888;
+            mBitmap = Bitmap.createBitmap(47, 42, config);
+            return mBitmap;
+        }
+        @Override
+        protected void onFreeBitmap(Bitmap bitmap) {
+            mFreeCalled++;
+            assertSame(mBitmap, bitmap);
+            mBitmap.recycle();
+            mBitmap = null;
+        }
+    }
+
+    @SmallTest
+    public void testUploadedTexture() {
+        GL11 glStub = new GLStub();
+        GLCanvas canvas = new GLES11Canvas(glStub);
+        MyUploadedTexture texture = new MyUploadedTexture();
+
+        // draw it and the bitmap should be fetched.
+        assertEquals(0, texture.mFreeCalled);
+        assertEquals(0, texture.mGetCalled);
+        texture.draw(canvas, 0, 0);
+        assertEquals(1, texture.mGetCalled);
+        assertTrue(texture.isLoaded());
+        assertTrue(texture.isContentValid());
+
+        // invalidate content and it should be freed.
+        texture.invalidateContent();
+        assertFalse(texture.isContentValid());
+        assertEquals(1, texture.mFreeCalled);
+        assertTrue(texture.isLoaded());  // But it's still loaded
+
+        // draw it again and the bitmap should be fetched again.
+        texture.draw(canvas, 0, 0);
+        assertEquals(2, texture.mGetCalled);
+        assertTrue(texture.isLoaded());
+        assertTrue(texture.isContentValid());
+
+        // recycle the texture and it should be freed again.
+        texture.recycle();
+        assertEquals(2, texture.mFreeCalled);
+        // TODO: these two are broken and waiting for fix.
+        //assertFalse(texture.isLoaded(canvas));
+        //assertFalse(texture.isContentValid(canvas));
+    }
+
+    class MyTextureForMixed extends BasicTexture {
+        MyTextureForMixed(GLCanvas canvas, int id) {
+            super(canvas, id, 0);
+        }
+
+        @Override
+        protected boolean onBind(GLCanvas canvas) {
+            return true;
+        }
+
+        @Override
+        protected int getTarget() {
+            return GL11.GL_TEXTURE_2D;
+        }
+
+        @Override
+        public boolean isOpaque() {
+            return true;
+        }
+    }
+
+    @SmallTest
+    public void testBitmapTexture() {
+        Config config = Config.ARGB_8888;
+        Bitmap bitmap = Bitmap.createBitmap(47, 42, config);
+        assertFalse(bitmap.isRecycled());
+        BitmapTexture texture = new BitmapTexture(bitmap);
+        texture.recycle();
+        assertFalse(bitmap.isRecycled());
+        bitmap.recycle();
+        assertTrue(bitmap.isRecycled());
+    }
+}
diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamReaderTest.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamReaderTest.java
new file mode 100644
index 0000000..ae60a91
--- /dev/null
+++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamReaderTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.tests.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+public class JpegStreamReaderTest extends JpegStreamTestCase {
+    public static final String TAG = "JpegStreamReaderTest";
+    private JPEGInputStream mStream;
+    private Bitmap mBitmap;
+
+    public JpegStreamReaderTest(int imageRes) {
+        super(imageRes);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mBitmap = BitmapFactory.decodeStream(getImageInputStream());
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        Utils.closeSilently(mStream);
+        mStream = null;
+        if (mBitmap != null) {
+            mBitmap.recycle();
+            mBitmap = null;
+        }
+    }
+
+    @MediumTest
+    public void testBasicReads() throws Exception {
+
+        // Setup input stream.
+        mStream = new JPEGInputStream(reopenFileStream(), JpegConfig.FORMAT_RGBA);
+        Point dimens = mStream.getDimensions();
+
+        // Read whole stream into array.
+        byte[] bytes = new byte[dimens.x * StreamUtils.pixelSize(JpegConfig.FORMAT_RGBA) * dimens.y];
+        assertTrue(mStream.read(bytes, 0, bytes.length) == bytes.length);
+
+        // Set pixels in bitmap
+        Bitmap test = Bitmap.createBitmap(dimens.x, dimens.y, Bitmap.Config.ARGB_8888);
+        ByteBuffer buf = ByteBuffer.wrap(bytes);
+        test.copyPixelsFromBuffer(buf);
+        assertTrue(test.getWidth() == mBitmap.getWidth() && test.getHeight() == mBitmap.getHeight());
+        assertTrue(mStream.read(bytes, 0, bytes.length) == -1);
+    }
+
+    // TODO : more tests
+}
diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestCase.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestCase.java
new file mode 100644
index 0000000..ed3b08a
--- /dev/null
+++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestCase.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+import android.content.res.Resources;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.InputStream;
+
+public class JpegStreamTestCase extends InstrumentationTestCase {
+    public static final String TAG = "JpegStreamTestCase";
+
+    private static final String RES_ID_TITLE = "Resource ID: %x";
+
+    private InputStream mImageInputStream;
+    private final int mImageResourceId;
+
+    public JpegStreamTestCase(int imageRes) {
+        mImageResourceId = imageRes;
+    }
+
+    protected InputStream getImageInputStream() {
+        return mImageInputStream;
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        Log.d(TAG, "doing setUp...");
+        mImageInputStream = reopenFileStream();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        Log.d(TAG, "doing tearDown...");
+        Utils.closeSilently(mImageInputStream);
+        mImageInputStream = null;
+    }
+
+    protected String getImageTitle() {
+        return String.format(RES_ID_TITLE, mImageResourceId);
+    }
+
+    protected InputStream reopenFileStream() throws Exception {
+        return openResource(mImageResourceId);
+    }
+
+    protected InputStream openResource(int resourceID) throws Exception {
+        try {
+            Resources res = getInstrumentation().getContext().getResources();
+            return res.openRawResource(resourceID);
+        } catch (Exception e) {
+            throw new Exception(getImageTitle(), e);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestRunner.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestRunner.java
new file mode 100644
index 0000000..2afaf39
--- /dev/null
+++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestRunner.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+import android.util.Log;
+
+import com.android.gallery3d.exif.ExifTestRunner;
+import com.android.gallery3d.tests.R;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class JpegStreamTestRunner extends InstrumentationTestRunner {
+    private static final String TAG = "JpegStreamTestRunner";
+
+    private static final int[] IMG_RESOURCE = {
+            R.raw.galaxy_nexus
+    };
+
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        addAllTestsFromTestCase(JpegStreamReaderTest.class, suite);
+        addAllTestsFromTestCase(JpegStreamWriterTest.class, suite);
+        return suite;
+    }
+
+    private void addAllTestsFromTestCase(Class<? extends JpegStreamTestCase> testClass,
+            TestSuite suite) {
+        for (Method method : testClass.getDeclaredMethods()) {
+            if (method.getName().startsWith("test") && method.getParameterTypes().length == 0) {
+                for (int i = 0; i < IMG_RESOURCE.length; i++) {
+                    TestCase test;
+                    try {
+                        test = testClass.getDeclaredConstructor(int.class).
+                                newInstance(IMG_RESOURCE[i]);
+                        test.setName(method.getName());
+                        suite.addTest(test);
+                    } catch (IllegalArgumentException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (InstantiationException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (IllegalAccessException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (InvocationTargetException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    } catch (NoSuchMethodException e) {
+                        Log.e(TAG, "Failed to create test case", e);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return ExifTestRunner.class.getClassLoader();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamWriterTest.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamWriterTest.java
new file mode 100644
index 0000000..befba4c
--- /dev/null
+++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamWriterTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2013 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.gallery3d.jpegstream;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.tests.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.ByteBuffer;
+
+public class JpegStreamWriterTest extends JpegStreamTestCase {
+    public static final String TAG = "JpegStreamWriterTest";
+    private JPEGOutputStream mStream;
+    private ByteArrayOutputStream mWrappedStream;
+    private Bitmap mBitmap;
+    private Bitmap mControl;
+
+    // galaxy_nexus.jpg image compressed with q=20
+    private static final int CONTROL_RID = R.raw.jpeg_control;
+
+    public JpegStreamWriterTest(int imageRes) {
+        super(imageRes);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mBitmap = BitmapFactory.decodeStream(getImageInputStream());
+        mControl = BitmapFactory.decodeStream(openResource(CONTROL_RID));
+        mWrappedStream = new ByteArrayOutputStream();
+
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        Utils.closeSilently(mStream);
+        Utils.closeSilently(mWrappedStream);
+        mWrappedStream = null;
+        mStream = null;
+        if (mBitmap != null) {
+            mBitmap.recycle();
+            mBitmap = null;
+        }
+        if (mControl != null) {
+            mControl.recycle();
+            mControl = null;
+        }
+    }
+
+    public void testBasicWrites() throws Exception {
+        assertTrue(mBitmap != null);
+        int width = mBitmap.getWidth();
+        int height = mBitmap.getHeight();
+        mStream = new JPEGOutputStream(mWrappedStream, width,
+                height, 20, JpegConfig.FORMAT_RGBA);
+
+        // Put bitmap pixels into a byte array (format is RGBA).
+        int rowLength = width * StreamUtils.pixelSize(JpegConfig.FORMAT_RGBA);
+        int size = height * rowLength;
+        byte[] byteArray = new byte[size];
+        ByteBuffer buf = ByteBuffer.wrap(byteArray);
+        mBitmap.copyPixelsToBuffer(buf);
+
+        // Write out whole array
+        mStream.write(byteArray, 0, byteArray.length);
+        mStream.close();
+
+        // Get compressed jpeg output
+        byte[] compressed = mWrappedStream.toByteArray();
+
+        // Check jpeg
+        ByteArrayInputStream inStream = new ByteArrayInputStream(compressed);
+        Bitmap test = BitmapFactory.decodeStream(inStream);
+        assertTrue(test != null);
+        assertTrue(test.sameAs(mControl));
+    }
+
+    public void testStreamingWrites() throws Exception {
+        assertTrue(mBitmap != null);
+        int width = mBitmap.getWidth();
+        int height = mBitmap.getHeight();
+        mStream = new JPEGOutputStream(mWrappedStream, width,
+                height, 20, JpegConfig.FORMAT_RGBA);
+
+        // Put bitmap pixels into a byte array (format is RGBA).
+        int rowLength = width * StreamUtils.pixelSize(JpegConfig.FORMAT_RGBA);
+        int size = height * rowLength;
+        byte[] byteArray = new byte[size];
+        ByteBuffer buf = ByteBuffer.wrap(byteArray);
+        mBitmap.copyPixelsToBuffer(buf);
+
+        // Write array in chunks
+        int chunkSize = rowLength / 3;
+        int written = 0;
+        while (written < size) {
+            if (written + chunkSize > size) {
+                chunkSize = size - written;
+            }
+            mStream.write(byteArray, written, chunkSize);
+            written += chunkSize;
+        }
+        mStream.close();
+
+        // Get compressed jpeg output
+        byte[] compressed = mWrappedStream.toByteArray();
+
+        // Check jpeg
+        ByteArrayInputStream inStream = new ByteArrayInputStream(compressed);
+        Bitmap test = BitmapFactory.decodeStream(inStream);
+        assertTrue(test != null);
+        assertTrue(test.sameAs(mControl));
+    }
+
+    // TODO : more tests
+}
diff --git a/tests/src/com/android/gallery3d/stress/CameraLatency.java b/tests/src/com/android/gallery3d/stress/CameraLatency.java
new file mode 100755
index 0000000..2cdc2f1
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/CameraLatency.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.stress;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Instrumentation;
+import android.os.Environment;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ */
+
+public class CameraLatency extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private String TAG = "CameraLatency";
+    private static final int TOTAL_NUMBER_OF_IMAGECAPTURE = 20;
+    private static final long WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN = 4000;
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+
+    private long mTotalAutoFocusTime;
+    private long mTotalShutterLag;
+    private long mTotalShutterToPictureDisplayedTime;
+    private long mTotalPictureDisplayedToJpegCallbackTime;
+    private long mTotalJpegCallbackFinishTime;
+    private long mAvgAutoFocusTime;
+    private long mAvgShutterLag = mTotalShutterLag;
+    private long mAvgShutterToPictureDisplayedTime;
+    private long mAvgPictureDisplayedToJpegCallbackTime;
+    private long mAvgJpegCallbackFinishTime;
+
+    public CameraLatency() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        getActivity();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    public void testImageCapture() {
+        Log.v(TAG, "start testImageCapture test");
+        Instrumentation inst = getInstrumentation();
+        inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+        try {
+            for (int i = 0; i < TOTAL_NUMBER_OF_IMAGECAPTURE; i++) {
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                //skip the first measurement
+                if (i != 0) {
+                    CameraActivity c = getActivity();
+
+                    // if any of the latency var accessor methods return -1 then the
+                    // camera is set to a different module other than PhotoModule so
+                    // skip the shot and try again
+                    if (c.getAutoFocusTime() != -1) {
+                        mTotalAutoFocusTime += c.getAutoFocusTime();
+                        mTotalShutterLag += c.getShutterLag();
+                        mTotalShutterToPictureDisplayedTime +=
+                                c.getShutterToPictureDisplayedTime();
+                        mTotalPictureDisplayedToJpegCallbackTime +=
+                                c.getPictureDisplayedToJpegCallbackTime();
+                        mTotalJpegCallbackFinishTime += c.getJpegCallbackFinishTime();
+                    }
+                    else {
+                        i--;
+                        continue;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+        }
+        //ToDO: yslau
+        //1) Need to get the baseline from the cupcake so that we can add the
+        //failure condition of the camera latency.
+        //2) Only count those number with succesful capture. Set the timer to invalid
+        //before capture and ignore them if the value is invalid
+        int numberofRun = TOTAL_NUMBER_OF_IMAGECAPTURE - 1;
+        mAvgAutoFocusTime = mTotalAutoFocusTime / numberofRun;
+        mAvgShutterLag = mTotalShutterLag / numberofRun;
+        mAvgShutterToPictureDisplayedTime =
+                mTotalShutterToPictureDisplayedTime / numberofRun;
+        mAvgPictureDisplayedToJpegCallbackTime =
+                mTotalPictureDisplayedToJpegCallbackTime / numberofRun;
+        mAvgJpegCallbackFinishTime =
+                mTotalJpegCallbackFinishTime / numberofRun;
+
+        try {
+            FileWriter fstream = null;
+            fstream = new FileWriter(CAMERA_TEST_OUTPUT_FILE, true);
+            BufferedWriter out = new BufferedWriter(fstream);
+            out.write("Camera Latency : \n");
+            out.write("Number of loop: " + TOTAL_NUMBER_OF_IMAGECAPTURE + "\n");
+            out.write("Avg AutoFocus = " + mAvgAutoFocusTime + "\n");
+            out.write("Avg mShutterLag = " + mAvgShutterLag + "\n");
+            out.write("Avg mShutterToPictureDisplayedTime = "
+                    + mAvgShutterToPictureDisplayedTime + "\n");
+            out.write("Avg mPictureDisplayedToJpegCallbackTime = "
+                    + mAvgPictureDisplayedToJpegCallbackTime + "\n");
+            out.write("Avg mJpegCallbackFinishTime = " +
+                    mAvgJpegCallbackFinishTime + "\n");
+            out.close();
+            fstream.close();
+        } catch (Exception e) {
+            fail("Camera Latency write output to file");
+        }
+        Log.v(TAG, "The Image capture wait time = " +
+            WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+        Log.v(TAG, "Avg AutoFocus = " + mAvgAutoFocusTime);
+        Log.v(TAG, "Avg mShutterLag = " + mAvgShutterLag);
+        Log.v(TAG, "Avg mShutterToPictureDisplayedTime = "
+                + mAvgShutterToPictureDisplayedTime);
+        Log.v(TAG, "Avg mPictureDisplayedToJpegCallbackTime = "
+                + mAvgPictureDisplayedToJpegCallbackTime);
+        Log.v(TAG, "Avg mJpegCallbackFinishTime = " + mAvgJpegCallbackFinishTime);
+    }
+}
+
diff --git a/tests/src/com/android/gallery3d/stress/CameraStartUp.java b/tests/src/com/android/gallery3d/stress/CameraStartUp.java
new file mode 100644
index 0000000..3ca1632
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/CameraStartUp.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.stress;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import java.io.FileWriter;
+import java.io.BufferedWriter;
+
+/**
+ * Test cases to measure the camera and video recorder startup time.
+ */
+public class CameraStartUp extends InstrumentationTestCase {
+
+    private static final int TOTAL_NUMBER_OF_STARTUP = 20;
+
+    private String TAG = "CameraStartUp";
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+    private static int WAIT_TIME_FOR_PREVIEW = 1500; //1.5 second
+
+    private long launchCamera() {
+        long startupTime = 0;
+        try {
+            Intent intent = new Intent(Intent.ACTION_MAIN);
+            intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            long beforeStart = System.currentTimeMillis();
+            Instrumentation inst = getInstrumentation();
+            Activity cameraActivity = inst.startActivitySync(intent);
+            long cameraStarted = System.currentTimeMillis();
+            Thread.sleep(WAIT_TIME_FOR_PREVIEW);
+            cameraActivity.finish();
+            startupTime = cameraStarted - beforeStart;
+            Thread.sleep(1000);
+            Log.v(TAG, "camera startup time: " + startupTime);
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+            fail("Fails to get the output file");
+        }
+        return startupTime;
+    }
+
+    private long launchVideo() {
+        long startupTime = 0;
+
+        try {
+            Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+            intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            long beforeStart = System.currentTimeMillis();
+            Instrumentation inst = getInstrumentation();
+            Activity recorderActivity = inst.startActivitySync(intent);
+            long cameraStarted = System.currentTimeMillis();
+            recorderActivity.finish();
+            startupTime = cameraStarted - beforeStart;
+            Log.v(TAG, "Video Startup Time = " + startupTime);
+            // wait for 1s to make sure it reach a clean stage
+            Thread.sleep(WAIT_TIME_FOR_PREVIEW);
+            Log.v(TAG, "video startup time: " + startupTime);
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+            fail("Fails to launch video output file");
+        }
+        return startupTime;
+    }
+
+    private void writeToOutputFile(long totalStartupTime,
+            String individualStartupTime, boolean firstStartUp, String Type) throws Exception {
+        // TODO (yslau) : Need to integrate the output data with central
+        // dashboard
+        try {
+            FileWriter fstream = null;
+            fstream = new FileWriter(CAMERA_TEST_OUTPUT_FILE, true);
+            BufferedWriter out = new BufferedWriter(fstream);
+            if (firstStartUp) {
+                out.write("First " + Type + " Startup: " + totalStartupTime + "\n");
+            } else {
+                long averageStartupTime = totalStartupTime / (TOTAL_NUMBER_OF_STARTUP -1);
+                out.write(Type + "startup time: " + "\n");
+                out.write("Number of loop: " + (TOTAL_NUMBER_OF_STARTUP -1)  + "\n");
+                out.write(individualStartupTime + "\n\n");
+                out.write(Type + " average startup time: " + averageStartupTime + " ms\n\n");
+            }
+            out.close();
+            fstream.close();
+        } catch (Exception e) {
+            fail("Camera write output to file");
+        }
+    }
+
+    public void testLaunchVideo() throws Exception {
+        String individualStartupTime;
+        individualStartupTime = "Individual Video Startup Time = ";
+        long totalStartupTime = 0;
+        long startupTime = 0;
+        for (int i = 0; i < TOTAL_NUMBER_OF_STARTUP; i++) {
+            if (i == 0) {
+                // Capture the first startup time individually
+                long firstStartUpTime = launchVideo();
+                writeToOutputFile(firstStartUpTime, "na", true, "Video");
+            } else {
+                startupTime = launchVideo();
+                totalStartupTime += startupTime;
+                individualStartupTime += startupTime + " ,";
+            }
+        }
+        Log.v(TAG, "totalStartupTime =" + totalStartupTime);
+        writeToOutputFile(totalStartupTime, individualStartupTime, false, "Video");
+    }
+
+    public void testLaunchCamera() throws Exception {
+        String individualStartupTime;
+        individualStartupTime = "Individual Camera Startup Time = ";
+        long totalStartupTime = 0;
+        long startupTime = 0;
+        for (int i = 0; i < TOTAL_NUMBER_OF_STARTUP; i++) {
+            if (i == 0) {
+                // Capture the first startup time individually
+                long firstStartUpTime = launchCamera();
+                writeToOutputFile(firstStartUpTime, "na", true, "Camera");
+            } else {
+                startupTime = launchCamera();
+                totalStartupTime += startupTime;
+                individualStartupTime += startupTime + " ,";
+            }
+        }
+        Log.v(TAG, "totalStartupTime =" + totalStartupTime);
+        writeToOutputFile(totalStartupTime,
+                individualStartupTime, false, "Camera");
+    }
+}
diff --git a/tests/src/com/android/gallery3d/stress/CameraStressTestRunner.java b/tests/src/com/android/gallery3d/stress/CameraStressTestRunner.java
new file mode 100755
index 0000000..d3fb10d
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/CameraStressTestRunner.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.stress;
+
+import android.os.Bundle;
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+import junit.framework.TestSuite;
+
+public class CameraStressTestRunner extends InstrumentationTestRunner {
+
+    // Default recorder settings
+    public static int mVideoDuration = 20000; // set default to 20 seconds
+    public static int mVideoIterations = 1; // set default to 1 video
+    public static int mImageIterations = 10; // set default to 10 images
+
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(ImageCapture.class);
+        suite.addTestSuite(VideoCapture.class);
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return CameraStressTestRunner.class.getClassLoader();
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        String video_iterations = (String) icicle.get("video_iterations");
+        String image_iterations = (String) icicle.get("image_iterations");
+        String video_duration = (String) icicle.get("video_duration");
+
+        if ( video_iterations != null ) {
+            mVideoIterations = Integer.parseInt(video_iterations);
+        }
+        if ( image_iterations != null) {
+            mImageIterations = Integer.parseInt(image_iterations);
+        }
+        if ( video_duration != null) {
+            mVideoDuration = Integer.parseInt(video_duration);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/stress/ImageCapture.java b/tests/src/com/android/gallery3d/stress/ImageCapture.java
new file mode 100755
index 0000000..5a9ee6a
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/ImageCapture.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.stress;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.stress.CameraStressTestRunner;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.app.Activity;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ * Running the test suite:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.camera.stress.ImageCapture \
+ *    -w com.google.android.camera.tests/android.test.InstrumentationTestRunner
+ *
+ */
+
+public class ImageCapture extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private String TAG = "ImageCapture";
+    private static final long WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN = 1500;   //1.5 sedconds
+    private static final long WAIT_FOR_SWITCH_CAMERA = 3000; //3 seconds
+
+    private TestUtil testUtil = new TestUtil();
+
+    // Private intent extras.
+    private final static String EXTRAS_CAMERA_FACING =
+        "android.intent.extras.CAMERA_FACING";
+
+    public ImageCapture() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        testUtil.prepareOutputFile();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        testUtil.closeOutputFile();
+        super.tearDown();
+    }
+
+    public void captureImages(String reportTag, Instrumentation inst) {
+        int total_num_of_images = CameraStressTestRunner.mImageIterations;
+        Log.v(TAG, "no of images = " + total_num_of_images);
+
+        //TODO(yslau): Need to integrate the outoput with the central dashboard,
+        //write to a txt file as a temp solution
+        boolean memoryResult = false;
+        KeyEvent focusEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FOCUS);
+
+        try {
+            testUtil.writeReportHeader(reportTag, total_num_of_images);
+            for (int i = 0; i < total_num_of_images; i++) {
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                inst.sendKeySync(focusEvent);
+                inst.sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                testUtil.writeResult(i);
+            }
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception: " + e.toString());
+            assertTrue("testImageCapture", false);
+        }
+    }
+
+    public void testBackImageCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent();
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureImages("Back Camera Image Capture\n", inst);
+        act.finish();
+    }
+
+    public void testFrontImageCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent();
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureImages("Front Camera Image Capture\n", inst);
+        act.finish();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/stress/ShotToShotLatency.java b/tests/src/com/android/gallery3d/stress/ShotToShotLatency.java
new file mode 100644
index 0000000..0d5749e
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/ShotToShotLatency.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.stress;
+
+import android.app.Instrumentation;
+import android.os.Environment;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+import com.android.camera.CameraActivity;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Junit / Instrumentation test case for measuring camera shot to shot latency
+ */
+public class ShotToShotLatency extends ActivityInstrumentationTestCase2<CameraActivity> {
+    private String TAG = "ShotToShotLatency";
+    private static final int TOTAL_NUMBER_OF_SNAPSHOTS = 250;
+    private static final long SNAPSHOT_WAIT = 1000;
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+    private static final String CAMERA_IMAGE_DIRECTORY =
+            Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera/";
+
+    public ShotToShotLatency() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        getActivity();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private void cleanupLatencyImages() {
+        try {
+            File sdcard = new File(CAMERA_IMAGE_DIRECTORY);
+            File[] pics = null;
+            FilenameFilter filter = new FilenameFilter() {
+                public boolean accept(File dir, String name) {
+                    return name.endsWith(".jpg");
+                }
+            };
+            pics = sdcard.listFiles(filter);
+            for (File f : pics) {
+                f.delete();
+            }
+        } catch (SecurityException e) {
+            Log.e(TAG, "Security manager access violation: " + e.toString());
+        }
+    }
+
+    private void sleep(long time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Sleep InterruptedException " + e.toString());
+        }
+    }
+
+    public void testShotToShotLatency() {
+        long sigmaOfDiffFromMeanSquared = 0;
+        double mean = 0;
+        double standardDeviation = 0;
+        ArrayList<Long> captureTimes = new ArrayList<Long>();
+        ArrayList<Long> latencyTimes = new ArrayList<Long>();
+
+        Log.v(TAG, "start testShotToShotLatency test");
+        Instrumentation inst = getInstrumentation();
+
+        // Generate data points
+        for (int i = 0; i < TOTAL_NUMBER_OF_SNAPSHOTS; i++) {
+            inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+            sleep(SNAPSHOT_WAIT);
+            CameraActivity c = getActivity();
+            if (c.getCaptureStartTime() > 0) {
+                captureTimes.add(c.getCaptureStartTime());
+            }
+        }
+
+        // Calculate latencies
+        for (int j = 1; j < captureTimes.size(); j++) {
+            latencyTimes.add(captureTimes.get(j) - captureTimes.get(j - 1));
+        }
+
+        // Crunch numbers
+        for (long dataPoint : latencyTimes) {
+            mean += (double) dataPoint;
+        }
+        mean /= latencyTimes.size();
+
+        for (long dataPoint : latencyTimes) {
+            sigmaOfDiffFromMeanSquared += (dataPoint - mean) * (dataPoint - mean);
+        }
+        standardDeviation = Math.sqrt(sigmaOfDiffFromMeanSquared / latencyTimes.size());
+
+        // Report statistics
+        File outFile = new File(CAMERA_TEST_OUTPUT_FILE);
+        BufferedWriter output = null;
+        try {
+            output = new BufferedWriter(new FileWriter(outFile, true));
+            output.write("Shot to shot latency - mean: " + mean + "\n");
+            output.write("Shot to shot latency - standard deviation: " + standardDeviation + "\n");
+            cleanupLatencyImages();
+        } catch (IOException e) {
+            Log.e(TAG, "testShotToShotLatency IOException writing to log " + e.toString());
+        } finally {
+            try {
+                if (output != null) {
+                    output.close();
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Error closing file: " + e.toString());
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/stress/SwitchPreview.java b/tests/src/com/android/gallery3d/stress/SwitchPreview.java
new file mode 100755
index 0000000..3545f3b
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/SwitchPreview.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.stress;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ * Running the test suite:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.camera.stress.SwitchPreview \
+ *    -w com.android.camera.tests/com.android.camera.stress.CameraStressTestRunner
+ *
+ */
+public class SwitchPreview extends ActivityInstrumentationTestCase2 <CameraActivity>{
+    private String TAG = "SwitchPreview";
+    private static final int TOTAL_NUMBER_OF_SWITCHING = 200;
+    private static final long WAIT_FOR_PREVIEW = 4000;
+
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+    private BufferedWriter mOut;
+    private FileWriter mfstream;
+
+    public SwitchPreview() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        getActivity();
+        prepareOutputFile();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        getActivity().finish();
+        closeOutputFile();
+        super.tearDown();
+    }
+
+    private void prepareOutputFile(){
+        try{
+            mfstream = new FileWriter(CAMERA_TEST_OUTPUT_FILE, true);
+            mOut = new BufferedWriter(mfstream);
+        } catch (Exception e){
+            assertTrue("Camera Switch Mode", false);
+        }
+    }
+
+    private void closeOutputFile() {
+        try {
+            mOut.write("\n");
+            mOut.close();
+            mfstream.close();
+        } catch (Exception e) {
+            assertTrue("CameraSwitchMode close output", false);
+        }
+    }
+
+    public void testSwitchMode() {
+        //Switching the video and the video recorder mode
+        Instrumentation inst = getInstrumentation();
+        try{
+            mOut.write("Camera Switch Mode:\n");
+            mOut.write("No of loops :" + TOTAL_NUMBER_OF_SWITCHING + "\n");
+            mOut.write("loop: ");
+            for (int i=0; i< TOTAL_NUMBER_OF_SWITCHING; i++) {
+                Thread.sleep(WAIT_FOR_PREVIEW);
+                Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                intent.setClass(getInstrumentation().getTargetContext(),
+                        CameraActivity.class);
+                getActivity().startActivity(intent);
+                Thread.sleep(WAIT_FOR_PREVIEW);
+                intent = new Intent();
+                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                intent.setClass(getInstrumentation().getTargetContext(),
+                        CameraActivity.class);
+                getActivity().startActivity(intent);
+                mOut.write(" ," + i);
+                mOut.flush();
+            }
+        } catch (Exception e){
+            Log.v(TAG, "Got exception", e);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/stress/TestUtil.java b/tests/src/com/android/gallery3d/stress/TestUtil.java
new file mode 100644
index 0000000..56ab715
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/TestUtil.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011 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.gallery3d.stress;
+
+import android.os.Environment;
+import java.io.FileWriter;
+import java.io.BufferedWriter;
+
+
+/**
+ * Collection of utility functions used for the test.
+ */
+public class TestUtil {
+    public BufferedWriter mOut;
+    public FileWriter mfstream;
+
+    public TestUtil() {
+    }
+
+    public void prepareOutputFile() throws Exception {
+        String camera_test_output_file =
+                Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+        mfstream = new FileWriter(camera_test_output_file, true);
+        mOut = new BufferedWriter(mfstream);
+    }
+
+    public void closeOutputFile() throws Exception {
+        mOut.write("\n");
+        mOut.close();
+        mfstream.close();
+    }
+
+    public void writeReportHeader(String reportTag, int iteration) throws Exception {
+        mOut.write(reportTag);
+        mOut.write("No of loops :" + iteration + "\n");
+        mOut.write("loop: ");
+    }
+
+    public void writeResult(int iteration) throws Exception {
+        mOut.write(" ," + iteration);
+        mOut.flush();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/stress/VideoCapture.java b/tests/src/com/android/gallery3d/stress/VideoCapture.java
new file mode 100755
index 0000000..8211bad
--- /dev/null
+++ b/tests/src/com/android/gallery3d/stress/VideoCapture.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.stress;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.stress.TestUtil;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+
+import com.android.gallery3d.stress.CameraStressTestRunner;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ * Running the test suite:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.camera.stress.VideoCapture \
+ *    -w com.google.android.camera.tests/android.test.InstrumentationTestRunner
+ *
+ */
+
+public class VideoCapture extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private static final long WAIT_FOR_PREVIEW = 1500; //1.5 seconds
+    private static final long WAIT_FOR_SWITCH_CAMERA = 3000; //2 seconds
+
+    // Private intent extras which control the camera facing.
+    private final static String EXTRAS_CAMERA_FACING =
+        "android.intent.extras.CAMERA_FACING";
+
+    private TestUtil testUtil = new TestUtil();
+
+    public VideoCapture() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        testUtil.prepareOutputFile();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        testUtil.closeOutputFile();
+        super.tearDown();
+    }
+
+    public void captureVideos(String reportTag, Instrumentation inst) throws Exception{
+        boolean memoryResult = false;
+        int total_num_of_videos = CameraStressTestRunner.mVideoIterations;
+        int video_duration = CameraStressTestRunner.mVideoDuration;
+        testUtil.writeReportHeader(reportTag, total_num_of_videos);
+
+        for (int i = 0; i < total_num_of_videos; i++) {
+            Thread.sleep(WAIT_FOR_PREVIEW);
+            // record a video
+            inst.sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+            Thread.sleep(video_duration);
+            inst.sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+            testUtil.writeResult(i);
+        }
+    }
+
+    public void testBackVideoCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureVideos("Back Camera Video Capture\n", inst);
+        act.finish();
+    }
+
+    public void testFrontVideoCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureVideos("Front Camera Video Capture\n", inst);
+        act.finish();
+    }
+}
diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
new file mode 100644
index 0000000..01f0350
--- /dev/null
+++ b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.GLId;
+import com.android.gallery3d.glrenderer.GLPaint;
+import com.android.gallery3d.glrenderer.RawTexture;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class GLCanvasStub implements GLCanvas {
+    @Override
+    public void setSize(int width, int height) {}
+    @Override
+    public void clearBuffer() {}
+    @Override
+    public void clearBuffer(float[] argb) {}
+    public void setCurrentAnimationTimeMillis(long time) {}
+    public long currentAnimationTimeMillis() {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public void setAlpha(float alpha) {}
+    @Override
+    public float getAlpha() {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public void multiplyAlpha(float alpha) {}
+    @Override
+    public void translate(float x, float y, float z) {}
+    @Override
+    public void translate(float x, float y) {}
+    @Override
+    public void scale(float sx, float sy, float sz) {}
+    @Override
+    public void rotate(float angle, float x, float y, float z) {}
+    public boolean clipRect(int left, int top, int right, int bottom) {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public void save() {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public void save(int saveFlags) {
+        throw new UnsupportedOperationException();
+    }
+    public void setBlendEnabled(boolean enabled) {}
+    @Override
+    public void restore() {}
+    @Override
+    public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {}
+    @Override
+    public void drawRect(float x1, float y1, float x2, float y2, GLPaint paint) {}
+    @Override
+    public void fillRect(float x, float y, float width, float height, int color) {}
+    @Override
+    public void drawTexture(
+            BasicTexture texture, int x, int y, int width, int height) {}
+    @Override
+    public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+            int uvBuffer, int indexBuffer, int indexCount) {}
+    public void drawTexture(BasicTexture texture,
+            int x, int y, int width, int height, float alpha) {}
+    @Override
+    public void drawTexture(BasicTexture texture, RectF source, RectF target) {}
+    @Override
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+            int x, int y, int w, int h) {}
+    public void drawMixed(BasicTexture from, BasicTexture to,
+            float ratio, int x, int y, int w, int h) {}
+    @Override
+    public void drawMixed(BasicTexture from, int to,
+            float ratio, int x, int y, int w, int h) {}
+    public void drawMixed(BasicTexture from, BasicTexture to,
+            float ratio, int x, int y, int width, int height, float alpha) {}
+    public BasicTexture copyTexture(int x, int y, int width, int height) {
+        throw new UnsupportedOperationException();
+    }
+    public GL11 getGLInstance() {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public boolean unloadTexture(BasicTexture texture) {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public void deleteBuffer(int bufferId) {
+        throw new UnsupportedOperationException();
+    }
+    @Override
+    public void deleteRecycledResources() {}
+    @Override
+    public void multiplyMatrix(float[] mMatrix, int offset) {}
+    @Override
+    public void dumpStatisticsAndClear() {}
+    @Override
+    public void beginRenderTarget(RawTexture texture) {}
+    @Override
+    public void endRenderTarget() {}
+    @Override
+    public void drawMixed(BasicTexture from, int toColor,
+            float ratio, RectF src, RectF target) {}
+
+    @Override
+    public void setTextureParameters(BasicTexture texture) {
+    }
+    @Override
+    public void initializeTextureSize(BasicTexture texture, int format, int type) {
+    }
+    @Override
+    public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
+    }
+    @Override
+    public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,
+            int format, int type) {
+    }
+    @Override
+    public int uploadBuffer(ByteBuffer buffer) {
+        return 0;
+    }
+    @Override
+    public int uploadBuffer(FloatBuffer buffer) {
+        return 0;
+    }
+    @Override
+    public void recoverFromLightCycle() {
+    }
+    @Override
+    public void getBounds(Rect bounds, int x, int y, int width, int height) {
+    }
+    @Override
+    public GLId getGLId() {
+        return null;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/ui/GLRootMock.java b/tests/src/com/android/gallery3d/ui/GLRootMock.java
new file mode 100644
index 0000000..da78e14
--- /dev/null
+++ b/tests/src/com/android/gallery3d/ui/GLRootMock.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import com.android.gallery3d.anim.CanvasAnimation;
+
+public class GLRootMock implements GLRoot {
+    int mRequestRenderCalled;
+    int mRequestLayoutContentPaneCalled;
+
+    public void addOnGLIdleListener(OnGLIdleListener listener) {}
+    public void registerLaunchedAnimation(CanvasAnimation animation) {}
+    public void requestRenderForced() {
+        mRequestRenderCalled++;
+    }
+    public void requestRender() {
+        mRequestRenderCalled++;
+    }
+    public void requestLayoutContentPane() {
+        mRequestLayoutContentPaneCalled++;
+    }
+    public boolean hasStencil() { return true; }
+    public void lockRenderThread() {}
+    public void unlockRenderThread() {}
+    public void setContentPane(GLView content) {}
+    public void setOrientationSource(OrientationSource source) {}
+    public int getDisplayRotation() { return 0; }
+    public int getCompensation() { return 0; }
+    public Matrix getCompensationMatrix() { return null; }
+    public void freeze() {}
+    public void unfreeze() {}
+    public void setLightsOutMode(boolean enabled) {}
+    public Context getContext() { return null; }
+}
diff --git a/tests/src/com/android/gallery3d/ui/GLRootStub.java b/tests/src/com/android/gallery3d/ui/GLRootStub.java
new file mode 100644
index 0000000..25e7bca
--- /dev/null
+++ b/tests/src/com/android/gallery3d/ui/GLRootStub.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import com.android.gallery3d.anim.CanvasAnimation;
+
+public class GLRootStub implements GLRoot {
+    public void addOnGLIdleListener(OnGLIdleListener listener) {}
+    public void registerLaunchedAnimation(CanvasAnimation animation) {}
+    public void requestRenderForced() {}
+    public void requestRender() {}
+    public void requestLayoutContentPane() {}
+    public boolean hasStencil() { return true; }
+    public void lockRenderThread() {}
+    public void unlockRenderThread() {}
+    public void setContentPane(GLView content) {}
+    public void setOrientationSource(OrientationSource source) {}
+    public int getDisplayRotation() { return 0; }
+    public int getCompensation() { return 0; }
+    public Matrix getCompensationMatrix() { return null; }
+    public void freeze() {}
+    public void unfreeze() {}
+    public void setLightsOutMode(boolean enabled) {}
+    public Context getContext() { return null; }
+}
diff --git a/tests/src/com/android/gallery3d/ui/GLViewMock.java b/tests/src/com/android/gallery3d/ui/GLViewMock.java
new file mode 100644
index 0000000..9b7488f
--- /dev/null
+++ b/tests/src/com/android/gallery3d/ui/GLViewMock.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+class GLViewMock extends GLView {
+    // onAttachToRoot
+    int mOnAttachCalled;
+    GLRoot mRoot;
+    // onDetachFromRoot
+    int mOnDetachCalled;
+    // onVisibilityChanged
+    int mOnVisibilityChangedCalled;
+    // onLayout
+    int mOnLayoutCalled;
+    boolean mOnLayoutChangeSize;
+    // renderBackground
+    int mRenderBackgroundCalled;
+    // onMeasure
+    int mOnMeasureCalled;
+    int mOnMeasureWidthSpec;
+    int mOnMeasureHeightSpec;
+
+    @Override
+    public void onAttachToRoot(GLRoot root) {
+        mRoot = root;
+        mOnAttachCalled++;
+        super.onAttachToRoot(root);
+    }
+
+    @Override
+    public void onDetachFromRoot() {
+        mRoot = null;
+        mOnDetachCalled++;
+        super.onDetachFromRoot();
+    }
+
+    @Override
+    protected void onVisibilityChanged(int visibility) {
+        mOnVisibilityChangedCalled++;
+    }
+
+    @Override
+    protected void onLayout(boolean changeSize, int left, int top,
+            int right, int bottom) {
+        mOnLayoutCalled++;
+        mOnLayoutChangeSize = changeSize;
+        // call children's layout.
+        for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            GLView item = getComponent(i);
+            item.layout(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        mOnMeasureCalled++;
+        mOnMeasureWidthSpec = widthSpec;
+        mOnMeasureHeightSpec = heightSpec;
+        // call children's measure.
+        for (int i = 0, n = getComponentCount(); i < n; ++i) {
+            GLView item = getComponent(i);
+            item.measure(widthSpec, heightSpec);
+        }
+        setMeasuredSize(widthSpec, heightSpec);
+    }
+
+    @Override
+    protected void renderBackground(GLCanvas view) {
+        mRenderBackgroundCalled++;
+    }
+}
diff --git a/tests/src/com/android/gallery3d/ui/GLViewTest.java b/tests/src/com/android/gallery3d/ui/GLViewTest.java
new file mode 100644
index 0000000..b17b254
--- /dev/null
+++ b/tests/src/com/android/gallery3d/ui/GLViewTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.MotionEvent;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class GLViewTest extends TestCase {
+    @SuppressWarnings("unused")
+    private static final String TAG = "GLViewTest";
+
+    @SmallTest
+    public void testVisibility() {
+        GLViewMock a = new GLViewMock();
+        assertEquals(GLView.VISIBLE, a.getVisibility());
+        assertEquals(0, a.mOnVisibilityChangedCalled);
+        a.setVisibility(GLView.INVISIBLE);
+        assertEquals(GLView.INVISIBLE, a.getVisibility());
+        assertEquals(1, a.mOnVisibilityChangedCalled);
+        a.setVisibility(GLView.VISIBLE);
+        assertEquals(GLView.VISIBLE, a.getVisibility());
+        assertEquals(2, a.mOnVisibilityChangedCalled);
+    }
+
+    @SmallTest
+    public void testComponents() {
+        GLView view = new GLView();
+        assertEquals(0, view.getComponentCount());
+        try {
+            view.getComponent(0);
+            fail();
+        } catch (IndexOutOfBoundsException ex) {
+            // expected
+        }
+
+        GLView x = new GLView();
+        GLView y = new GLView();
+        view.addComponent(x);
+        view.addComponent(y);
+        assertEquals(2, view.getComponentCount());
+        assertSame(x, view.getComponent(0));
+        assertSame(y, view.getComponent(1));
+        view.removeComponent(x);
+        assertSame(y, view.getComponent(0));
+        try {
+            view.getComponent(1);
+            fail();
+        } catch (IndexOutOfBoundsException ex) {
+            // expected
+        }
+        try {
+            view.addComponent(y);
+            fail();
+        } catch (IllegalStateException ex) {
+            // expected
+        }
+        view.addComponent(x);
+        view.removeAllComponents();
+        assertEquals(0, view.getComponentCount());
+    }
+
+    @SmallTest
+    public void testBounds() {
+        GLView view = new GLView();
+
+        assertEquals(0, view.getWidth());
+        assertEquals(0, view.getHeight());
+
+        Rect b = view.bounds();
+        assertEquals(0, b.left);
+        assertEquals(0, b.top);
+        assertEquals(0, b.right);
+        assertEquals(0, b.bottom);
+
+        view.layout(10, 20, 30, 100);
+        assertEquals(20, view.getWidth());
+        assertEquals(80, view.getHeight());
+
+        b = view.bounds();
+        assertEquals(10, b.left);
+        assertEquals(20, b.top);
+        assertEquals(30, b.right);
+        assertEquals(100, b.bottom);
+    }
+
+    @SmallTest
+    public void testParent() {
+        GLView a = new GLView();
+        GLView b = new GLView();
+        assertNull(b.mParent);
+        a.addComponent(b);
+        assertSame(a, b.mParent);
+        a.removeComponent(b);
+        assertNull(b.mParent);
+    }
+
+    @SmallTest
+    public void testRoot() {
+        GLViewMock a = new GLViewMock();
+        GLViewMock b = new GLViewMock();
+        GLRoot r = new GLRootStub();
+        GLRoot r2 = new GLRootStub();
+        a.addComponent(b);
+
+        // Attach to root r
+        assertEquals(0, a.mOnAttachCalled);
+        assertEquals(0, b.mOnAttachCalled);
+        a.attachToRoot(r);
+        assertEquals(1, a.mOnAttachCalled);
+        assertEquals(1, b.mOnAttachCalled);
+        assertSame(r, a.getGLRoot());
+        assertSame(r, b.getGLRoot());
+
+        // Detach from r
+        assertEquals(0, a.mOnDetachCalled);
+        assertEquals(0, b.mOnDetachCalled);
+        a.detachFromRoot();
+        assertEquals(1, a.mOnDetachCalled);
+        assertEquals(1, b.mOnDetachCalled);
+
+        // Attach to another root r2
+        assertEquals(1, a.mOnAttachCalled);
+        assertEquals(1, b.mOnAttachCalled);
+        a.attachToRoot(r2);
+        assertEquals(2, a.mOnAttachCalled);
+        assertEquals(2, b.mOnAttachCalled);
+        assertSame(r2, a.getGLRoot());
+        assertSame(r2, b.getGLRoot());
+
+        // Detach from r2
+        assertEquals(1, a.mOnDetachCalled);
+        assertEquals(1, b.mOnDetachCalled);
+        a.detachFromRoot();
+        assertEquals(2, a.mOnDetachCalled);
+        assertEquals(2, b.mOnDetachCalled);
+    }
+
+    @SmallTest
+    public void testRoot2() {
+        GLView a = new GLViewMock();
+        GLViewMock b = new GLViewMock();
+        GLRoot r = new GLRootStub();
+
+        a.attachToRoot(r);
+
+        assertEquals(0, b.mOnAttachCalled);
+        a.addComponent(b);
+        assertEquals(1, b.mOnAttachCalled);
+
+        assertEquals(0, b.mOnDetachCalled);
+        a.removeComponent(b);
+        assertEquals(1, b.mOnDetachCalled);
+    }
+
+    @SmallTest
+    public void testInvalidate() {
+        GLView a = new GLView();
+        GLRootMock r = new GLRootMock();
+        a.attachToRoot(r);
+        assertEquals(0, r.mRequestRenderCalled);
+        a.invalidate();
+        assertEquals(1, r.mRequestRenderCalled);
+    }
+
+    @SmallTest
+    public void testRequestLayout() {
+        GLView a = new GLView();
+        GLView b = new GLView();
+        GLRootMock r = new GLRootMock();
+        a.attachToRoot(r);
+        a.addComponent(b);
+        assertEquals(0, r.mRequestLayoutContentPaneCalled);
+        b.requestLayout();
+        assertEquals(1, r.mRequestLayoutContentPaneCalled);
+    }
+
+    @SmallTest
+    public void testLayout() {
+        GLViewMock a = new GLViewMock();
+        GLViewMock b = new GLViewMock();
+        GLViewMock c = new GLViewMock();
+        GLRootMock r = new GLRootMock();
+
+        a.attachToRoot(r);
+        a.addComponent(b);
+        a.addComponent(c);
+
+        assertEquals(0, a.mOnLayoutCalled);
+        a.layout(10, 20, 60, 100);
+        assertEquals(1, a.mOnLayoutCalled);
+        assertEquals(1, b.mOnLayoutCalled);
+        assertEquals(1, c.mOnLayoutCalled);
+        assertTrue(a.mOnLayoutChangeSize);
+        assertTrue(b.mOnLayoutChangeSize);
+        assertTrue(c.mOnLayoutChangeSize);
+
+        // same size should not trigger onLayout
+        a.layout(10, 20, 60, 100);
+        assertEquals(1, a.mOnLayoutCalled);
+
+        // unless someone requested it, but only those on the path
+        // to the requester.
+        assertEquals(0, r.mRequestLayoutContentPaneCalled);
+        b.requestLayout();
+        a.layout(10, 20, 60, 100);
+        assertEquals(1, r.mRequestLayoutContentPaneCalled);
+        assertEquals(2, a.mOnLayoutCalled);
+        assertEquals(2, b.mOnLayoutCalled);
+        assertEquals(1, c.mOnLayoutCalled);
+    }
+
+    @SmallTest
+    public void testRender() {
+        GLViewMock a = new GLViewMock();
+        GLViewMock b = new GLViewMock();
+
+        a.addComponent(b);
+        GLCanvasStub canvas = new GLCanvasStub();
+        assertEquals(0, a.mRenderBackgroundCalled);
+        assertEquals(0, b.mRenderBackgroundCalled);
+        a.render(canvas);
+        assertEquals(1, a.mRenderBackgroundCalled);
+        assertEquals(1, b.mRenderBackgroundCalled);
+    }
+
+    @SmallTest
+    public void testMeasure() {
+        GLViewMock a = new GLViewMock();
+        GLViewMock b = new GLViewMock();
+        GLViewMock c = new GLViewMock();
+        GLRootMock r = new GLRootMock();
+
+        a.addComponent(b);
+        a.addComponent(c);
+        a.attachToRoot(r);
+
+        assertEquals(0, a.mOnMeasureCalled);
+        a.measure(100, 200);
+        assertEquals(1, a.mOnMeasureCalled);
+        assertEquals(1, b.mOnMeasureCalled);
+        assertEquals(100, a.mOnMeasureWidthSpec);
+        assertEquals(200, a.mOnMeasureHeightSpec);
+        assertEquals(100, b.mOnMeasureWidthSpec);
+        assertEquals(200, b.mOnMeasureHeightSpec);
+        assertEquals(100, a.getMeasuredWidth());
+        assertEquals(200, b.getMeasuredHeight());
+
+        // same spec should not trigger onMeasure
+        a.measure(100, 200);
+        assertEquals(1, a.mOnMeasureCalled);
+
+        // unless someone requested it, but only those on the path
+        // to the requester.
+        b.requestLayout();
+        a.measure(100, 200);
+        assertEquals(2, a.mOnMeasureCalled);
+        assertEquals(2, b.mOnMeasureCalled);
+        assertEquals(1, c.mOnMeasureCalled);
+    }
+
+    class MyGLView extends GLView {
+        private int mWidth;
+        int mOnTouchCalled;
+        int mOnTouchX;
+        int mOnTouchY;
+        int mOnTouchAction;
+
+        public MyGLView(int width) {
+            mWidth = width;
+        }
+
+        @Override
+        protected void onLayout(boolean changeSize, int left, int top,
+                int right, int bottom) {
+            // layout children from left to right
+            // call children's layout.
+            int x = 0;
+            for (int i = 0, n = getComponentCount(); i < n; ++i) {
+                GLView item = getComponent(i);
+                item.measure(0, 0);
+                int w = item.getMeasuredWidth();
+                int h = item.getMeasuredHeight();
+                item.layout(x, 0, x + w, h);
+                x += w;
+            }
+        }
+
+        @Override
+        protected void onMeasure(int widthSpec, int heightSpec) {
+            setMeasuredSize(mWidth, 100);
+        }
+
+        @Override
+        protected boolean onTouch(MotionEvent event) {
+            mOnTouchCalled++;
+            mOnTouchX = (int) event.getX();
+            mOnTouchY = (int) event.getY();
+            mOnTouchAction = event.getAction();
+            return true;
+        }
+    }
+
+    private MotionEvent NewMotionEvent(int action, int x, int y) {
+        return MotionEvent.obtain(0, 0, action, x, y, 0);
+    }
+
+    @SmallTest
+    public void testTouchEvent() {
+        // We construct a tree with four nodes. Only the x coordinate is used:
+        // A = [0..............................300)
+        // B = [0......100)
+        // C =             [100......200)
+        // D =             [100..150)
+
+        MyGLView a = new MyGLView(300);
+        MyGLView b = new MyGLView(100);
+        MyGLView c = new MyGLView(100);
+        MyGLView d = new MyGLView(50);
+        GLRoot r = new GLRootStub();
+
+        a.addComponent(b);
+        a.addComponent(c);
+        c.addComponent(d);
+        a.attachToRoot(r);
+        a.layout(0, 0, 300, 100);
+
+        int DOWN = MotionEvent.ACTION_DOWN;
+        int UP = MotionEvent.ACTION_UP;
+        int MOVE = MotionEvent.ACTION_MOVE;
+        int CANCEL = MotionEvent.ACTION_CANCEL;
+
+        // simple case
+        assertEquals(0, a.mOnTouchCalled);
+        a.dispatchTouchEvent(NewMotionEvent(DOWN, 250, 0));
+        assertEquals(DOWN, a.mOnTouchAction);
+        a.dispatchTouchEvent(NewMotionEvent(UP, 250, 0));
+        assertEquals(UP, a.mOnTouchAction);
+        assertEquals(2, a.mOnTouchCalled);
+
+        // pass to a child, check the location is offseted.
+        assertEquals(0, c.mOnTouchCalled);
+        a.dispatchTouchEvent(NewMotionEvent(DOWN, 175, 0));
+        a.dispatchTouchEvent(NewMotionEvent(UP, 175, 0));
+        assertEquals(75, c.mOnTouchX);
+        assertEquals(0, c.mOnTouchY);
+        assertEquals(2, c.mOnTouchCalled);
+        assertEquals(2, a.mOnTouchCalled);
+
+        // motion target cancel event
+        assertEquals(0, d.mOnTouchCalled);
+        a.dispatchTouchEvent(NewMotionEvent(DOWN, 125, 0));
+        assertEquals(1, d.mOnTouchCalled);
+        a.dispatchTouchEvent(NewMotionEvent(MOVE, 250, 0));
+        assertEquals(2, d.mOnTouchCalled);
+        a.dispatchTouchEvent(NewMotionEvent(MOVE, 50, 0));
+        assertEquals(3, d.mOnTouchCalled);
+        a.dispatchTouchEvent(NewMotionEvent(DOWN, 175, 0));
+        assertEquals(4, d.mOnTouchCalled);
+        assertEquals(CANCEL, d.mOnTouchAction);
+        assertEquals(3, c.mOnTouchCalled);
+        assertEquals(DOWN, c.mOnTouchAction);
+        a.dispatchTouchEvent(NewMotionEvent(UP, 175, 0));
+
+        // motion target is removed
+        assertEquals(4, d.mOnTouchCalled);
+        a.dispatchTouchEvent(NewMotionEvent(DOWN, 125, 0));
+        assertEquals(5, d.mOnTouchCalled);
+        a.removeComponent(c);
+        assertEquals(6, d.mOnTouchCalled);
+        assertEquals(CANCEL, d.mOnTouchAction);
+
+        // invisible component should not get events
+        assertEquals(2, a.mOnTouchCalled);
+        assertEquals(0, b.mOnTouchCalled);
+        b.setVisibility(GLView.INVISIBLE);
+        a.dispatchTouchEvent(NewMotionEvent(DOWN, 50, 0));
+        assertEquals(3, a.mOnTouchCalled);
+        assertEquals(0, b.mOnTouchCalled);
+    }
+}
diff --git a/tests/src/com/android/gallery3d/ui/PointerInfo.java b/tests/src/com/android/gallery3d/ui/PointerInfo.java
new file mode 100644
index 0000000..6c78556
--- /dev/null
+++ b/tests/src/com/android/gallery3d/ui/PointerInfo.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.ui;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.CharBuffer;
+import java.nio.DoubleBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.LongBuffer;
+import java.nio.ShortBuffer;
+
+import javax.microedition.khronos.opengles.GL10;
+
+public class PointerInfo {
+
+    /**
+     * The number of coordinates per vertex. 1..4
+     */
+    public int mSize;
+
+    /**
+     * The type of each coordinate.
+     */
+    public int mType;
+
+    /**
+     * The byte offset between consecutive vertices. 0 means mSize *
+     * sizeof(mType)
+     */
+    public int mStride;
+    public Buffer mPointer;
+    public ByteBuffer mTempByteBuffer;
+
+    public PointerInfo(int size, int type, int stride, Buffer pointer) {
+        mSize = size;
+        mType = type;
+        mStride = stride;
+        mPointer = pointer;
+    }
+
+    private int getStride() {
+        return mStride > 0 ? mStride : sizeof(mType) * mSize;
+    }
+
+    public void bindByteBuffer() {
+        mTempByteBuffer = mPointer == null ? null : toByteBuffer(-1, mPointer);
+    }
+
+    public void unbindByteBuffer() {
+        mTempByteBuffer = null;
+    }
+
+    private static int sizeof(int type) {
+        switch (type) {
+        case GL10.GL_UNSIGNED_BYTE:
+            return 1;
+        case GL10.GL_BYTE:
+            return 1;
+        case GL10.GL_SHORT:
+            return 2;
+        case GL10.GL_FIXED:
+            return 4;
+        case GL10.GL_FLOAT:
+            return 4;
+        default:
+            return 0;
+        }
+    }
+
+    private static ByteBuffer toByteBuffer(int byteCount, Buffer input) {
+        ByteBuffer result = null;
+        boolean convertWholeBuffer = (byteCount < 0);
+        if (input instanceof ByteBuffer) {
+            ByteBuffer input2 = (ByteBuffer) input;
+            int position = input2.position();
+            if (convertWholeBuffer) {
+                byteCount = input2.limit() - position;
+            }
+            result = ByteBuffer.allocate(byteCount).order(input2.order());
+            for (int i = 0; i < byteCount; i++) {
+                result.put(input2.get());
+            }
+            input2.position(position);
+        } else if (input instanceof CharBuffer) {
+            CharBuffer input2 = (CharBuffer) input;
+            int position = input2.position();
+            if (convertWholeBuffer) {
+                byteCount = (input2.limit() - position) * 2;
+            }
+            result = ByteBuffer.allocate(byteCount).order(input2.order());
+            CharBuffer result2 = result.asCharBuffer();
+            for (int i = 0; i < byteCount / 2; i++) {
+                result2.put(input2.get());
+            }
+            input2.position(position);
+        } else if (input instanceof ShortBuffer) {
+            ShortBuffer input2 = (ShortBuffer) input;
+            int position = input2.position();
+            if (convertWholeBuffer) {
+                byteCount = (input2.limit() - position)* 2;
+            }
+            result = ByteBuffer.allocate(byteCount).order(input2.order());
+            ShortBuffer result2 = result.asShortBuffer();
+            for (int i = 0; i < byteCount / 2; i++) {
+                result2.put(input2.get());
+            }
+            input2.position(position);
+        } else if (input instanceof IntBuffer) {
+            IntBuffer input2 = (IntBuffer) input;
+            int position = input2.position();
+            if (convertWholeBuffer) {
+                byteCount = (input2.limit() - position) * 4;
+            }
+            result = ByteBuffer.allocate(byteCount).order(input2.order());
+            IntBuffer result2 = result.asIntBuffer();
+            for (int i = 0; i < byteCount / 4; i++) {
+                result2.put(input2.get());
+            }
+            input2.position(position);
+        } else if (input instanceof FloatBuffer) {
+            FloatBuffer input2 = (FloatBuffer) input;
+            int position = input2.position();
+            if (convertWholeBuffer) {
+                byteCount = (input2.limit() - position) * 4;
+            }
+            result = ByteBuffer.allocate(byteCount).order(input2.order());
+            FloatBuffer result2 = result.asFloatBuffer();
+            for (int i = 0; i < byteCount / 4; i++) {
+                result2.put(input2.get());
+            }
+            input2.position(position);
+        } else if (input instanceof DoubleBuffer) {
+            DoubleBuffer input2 = (DoubleBuffer) input;
+            int position = input2.position();
+            if (convertWholeBuffer) {
+                byteCount = (input2.limit() - position) * 8;
+            }
+            result = ByteBuffer.allocate(byteCount).order(input2.order());
+            DoubleBuffer result2 = result.asDoubleBuffer();
+            for (int i = 0; i < byteCount / 8; i++) {
+                result2.put(input2.get());
+            }
+            input2.position(position);
+        } else if (input instanceof LongBuffer) {
+            LongBuffer input2 = (LongBuffer) input;
+            int position = input2.position();
+            if (convertWholeBuffer) {
+                byteCount = (input2.limit() - position) * 8;
+            }
+            result = ByteBuffer.allocate(byteCount).order(input2.order());
+            LongBuffer result2 = result.asLongBuffer();
+            for (int i = 0; i < byteCount / 8; i++) {
+                result2.put(input2.get());
+            }
+            input2.position(position);
+        } else {
+            throw new RuntimeException("Unimplemented Buffer subclass.");
+        }
+        result.rewind();
+        // The OpenGL API will interpret the result in hardware byte order,
+        // so we better do that as well:
+        result.order(ByteOrder.nativeOrder());
+        return result;
+    }
+
+    public void getArrayElement(int index, double[] result) {
+        if (mTempByteBuffer == null) {
+            throw new IllegalArgumentException("undefined pointer");
+        }
+        if (mStride < 0) {
+            throw new IllegalArgumentException("invalid stride");
+        }
+
+        int stride = getStride();
+        ByteBuffer byteBuffer = mTempByteBuffer;
+        int size = mSize;
+        int type = mType;
+        int sizeofType = sizeof(type);
+        int byteOffset = stride * index;
+
+        for (int i = 0; i < size; i++) {
+            switch (type) {
+            case GL10.GL_BYTE:
+            case GL10.GL_UNSIGNED_BYTE:
+                result[i] = byteBuffer.get(byteOffset);
+                break;
+            case GL10.GL_SHORT:
+                ShortBuffer shortBuffer = byteBuffer.asShortBuffer();
+                result[i] = shortBuffer.get(byteOffset / 2);
+                break;
+            case GL10.GL_FIXED:
+                IntBuffer intBuffer = byteBuffer.asIntBuffer();
+                result[i] = intBuffer.get(byteOffset / 4);
+                break;
+            case GL10.GL_FLOAT:
+                FloatBuffer floatBuffer = byteBuffer.asFloatBuffer();
+                result[i] = floatBuffer.get(byteOffset / 4);
+                break;
+            default:
+                throw new UnsupportedOperationException("unknown type");
+            }
+            byteOffset += sizeofType;
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/unittest/CameraUnitTest.java b/tests/src/com/android/gallery3d/unittest/CameraUnitTest.java
new file mode 100644
index 0000000..b8fb05f
--- /dev/null
+++ b/tests/src/com/android/gallery3d/unittest/CameraUnitTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.unittest;
+
+import com.android.camera.Util;
+
+import android.graphics.Matrix;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class CameraUnitTest extends TestCase {
+    public void testRoundOrientation() {
+        int h = Util.ORIENTATION_HYSTERESIS;
+        assertEquals(0, Util.roundOrientation(0, 0));
+        assertEquals(0, Util.roundOrientation(359, 0));
+        assertEquals(0, Util.roundOrientation(0 + 44 + h, 0));
+        assertEquals(90, Util.roundOrientation(0 + 45 + h, 0));
+        assertEquals(0, Util.roundOrientation(360 - 44 - h, 0));
+        assertEquals(270, Util.roundOrientation(360 - 45 - h, 0));
+
+        assertEquals(90, Util.roundOrientation(90, 90));
+        assertEquals(90, Util.roundOrientation(90 + 44 + h, 90));
+        assertEquals(180, Util.roundOrientation(90 + 45 + h, 90));
+        assertEquals(90, Util.roundOrientation(90 - 44 - h, 90));
+        assertEquals(0, Util.roundOrientation(90 - 45 - h, 90));
+
+        assertEquals(180, Util.roundOrientation(180, 180));
+        assertEquals(180, Util.roundOrientation(180 + 44 + h, 180));
+        assertEquals(270, Util.roundOrientation(180 + 45 + h, 180));
+        assertEquals(180, Util.roundOrientation(180 - 44 - h, 180));
+        assertEquals(90, Util.roundOrientation(180 - 45 - h, 180));
+
+        assertEquals(270, Util.roundOrientation(270, 270));
+        assertEquals(270, Util.roundOrientation(270 + 44 + h, 270));
+        assertEquals(0, Util.roundOrientation(270 + 45 + h, 270));
+        assertEquals(270, Util.roundOrientation(270 - 44 - h, 270));
+        assertEquals(180, Util.roundOrientation(270 - 45 - h, 270));
+
+        assertEquals(90, Util.roundOrientation(90, 0));
+        assertEquals(180, Util.roundOrientation(180, 0));
+        assertEquals(270, Util.roundOrientation(270, 0));
+
+        assertEquals(0, Util.roundOrientation(0, 90));
+        assertEquals(180, Util.roundOrientation(180, 90));
+        assertEquals(270, Util.roundOrientation(270, 90));
+
+        assertEquals(0, Util.roundOrientation(0, 180));
+        assertEquals(90, Util.roundOrientation(90, 180));
+        assertEquals(270, Util.roundOrientation(270, 180));
+
+        assertEquals(0, Util.roundOrientation(0, 270));
+        assertEquals(90, Util.roundOrientation(90, 270));
+        assertEquals(180, Util.roundOrientation(180, 270));
+    }
+
+    public void testPrepareMatrix() {
+        Matrix matrix = new Matrix();
+        float[] points;
+        int[] expected;
+
+        Util.prepareMatrix(matrix, false, 0, 800, 480);
+        points = new float[] {-1000, -1000, 0, 0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {0, 0, 400, 240, 800, 480, 400, 480, 100, 300};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+
+        Util.prepareMatrix(matrix, false, 90, 800, 480);
+        points = new float[] {-1000, -1000,   0,   0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {800, 0, 400, 240, 0, 480, 0, 240, 300, 60};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+
+        Util.prepareMatrix(matrix, false, 180, 800, 480);
+        points = new float[] {-1000, -1000, 0, 0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {800, 480, 400, 240, 0, 0, 400, 0, 700, 180};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+
+        Util.prepareMatrix(matrix, true, 180, 800, 480);
+        points = new float[] {-1000, -1000, 0, 0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {0, 480, 400, 240, 800, 0, 400, 0, 100, 180};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+    }
+
+    private void assertEquals(int expected[], float[] actual) {
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("Array index " + i + " mismatch", expected[i], Math.round(actual[i]));
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/util/IntArrayTest.java b/tests/src/com/android/gallery3d/util/IntArrayTest.java
new file mode 100644
index 0000000..83e6050
--- /dev/null
+++ b/tests/src/com/android/gallery3d/util/IntArrayTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.util;
+
+import com.android.gallery3d.util.IntArray;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+@SmallTest
+public class IntArrayTest extends TestCase {
+    private static final String TAG = "IntArrayTest";
+
+    public void testIntArray() {
+        IntArray a = new IntArray();
+        assertEquals(0, a.size());
+        assertTrue(Arrays.equals(new int[] {}, a.toArray(null)));
+
+        a.add(0);
+        assertEquals(1, a.size());
+        assertTrue(Arrays.equals(new int[] {0}, a.toArray(null)));
+
+        a.add(1);
+        assertEquals(2, a.size());
+        assertTrue(Arrays.equals(new int[] {0, 1}, a.toArray(null)));
+
+        int[] buf = new int[2];
+        int[] result = a.toArray(buf);
+        assertSame(buf, result);
+
+        IntArray b = new IntArray();
+        for (int i = 0; i < 100; i++) {
+            b.add(i * i);
+        }
+
+        assertEquals(100, b.size());
+        result = b.toArray(buf);
+        assertEquals(100, result.length);
+        for (int i = 0; i < 100; i++) {
+            assertEquals(i * i, result[i]);
+        }
+    }
+}
diff --git a/tests/src/com/android/gallery3d/util/ProfileTest.java b/tests/src/com/android/gallery3d/util/ProfileTest.java
new file mode 100644
index 0000000..798b905
--- /dev/null
+++ b/tests/src/com/android/gallery3d/util/ProfileTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.util;
+
+import com.android.gallery3d.util.Profile;
+
+import android.os.Environment;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+@SmallTest
+public class ProfileTest extends TestCase {
+    private static final String TAG = "ProfileTest";
+    private static final String TEST_FILE =
+            Environment.getExternalStorageDirectory().getPath() + "/test.dat";
+
+
+    public void testProfile() throws IOException {
+        ProfileData p = new ProfileData();
+        ParsedProfile q;
+        String[] A = {"A"};
+        String[] B = {"B"};
+        String[] AC = {"A", "C"};
+        String[] AD = {"A", "D"};
+
+        // Empty profile
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertTrue(q.mEntries.isEmpty());
+        assertTrue(q.mSymbols.isEmpty());
+
+        // Only one sample
+        p.addSample(A);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(1, q.mEntries.size());
+        assertEquals(1, q.mSymbols.size());
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+
+        // Two samples at the same place
+        p.addSample(A);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(1, q.mEntries.size());
+        assertEquals(1, q.mSymbols.size());
+        assertEquals(2, q.mEntries.get(0).sampleCount);
+
+        // Two samples at the different places
+        p.reset();
+        p.addSample(A);
+        p.addSample(B);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(2, q.mEntries.size());
+        assertEquals(2, q.mSymbols.size());
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+        assertEquals(1, q.mEntries.get(1).sampleCount);
+
+        // depth > 1
+        p.reset();
+        p.addSample(AC);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(1, q.mEntries.size());
+        assertEquals(2, q.mSymbols.size());
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+
+        // two samples (AC and AD)
+        p.addSample(AD);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(2, q.mEntries.size());
+        assertEquals(3, q.mSymbols.size());  // three symbols: A, C, D
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+
+        // Remove the test file
+        new File(TEST_FILE).delete();
+    }
+}
+
+class ParsedProfile {
+    public class Entry {
+        int sampleCount;
+        int stackId[];
+    }
+
+    ArrayList<Entry> mEntries = new ArrayList<Entry>();
+    HashMap<Integer, String> mSymbols = new HashMap<Integer, String>();
+    private DataInputStream mIn;
+    private byte[] mScratch = new byte[4];  // scratch buffer for readInt
+
+    public ParsedProfile(String filename) throws IOException {
+        mIn = new DataInputStream(new FileInputStream(filename));
+
+        Entry entry = parseOneEntry();
+        checkIsFirstEntry(entry);
+
+        while (true) {
+            entry = parseOneEntry();
+            if (entry.sampleCount == 0) {
+                checkIsLastEntry(entry);
+                break;
+            }
+            mEntries.add(entry);
+        }
+
+        // Read symbol table
+        while (true) {
+            String line = mIn.readLine();
+            if (line == null) break;
+            String[] fields = line.split(" +");
+            checkIsValidSymbolLine(fields);
+            mSymbols.put(Integer.decode(fields[0]), fields[1]);
+        }
+    }
+
+    private void checkIsFirstEntry(Entry entry) {
+        Assert.assertEquals(0, entry.sampleCount);
+        Assert.assertEquals(3, entry.stackId.length);
+        Assert.assertEquals(1, entry.stackId[0]);
+        Assert.assertTrue(entry.stackId[1] > 0);  // sampling period
+        Assert.assertEquals(0, entry.stackId[2]);  // padding
+    }
+
+    private void checkIsLastEntry(Entry entry) {
+        Assert.assertEquals(0, entry.sampleCount);
+        Assert.assertEquals(1, entry.stackId.length);
+        Assert.assertEquals(0, entry.stackId[0]);
+    }
+
+    private void checkIsValidSymbolLine(String[] fields) {
+        Assert.assertEquals(2, fields.length);
+        Assert.assertTrue(fields[0].startsWith("0x"));
+    }
+
+    private Entry parseOneEntry() throws IOException {
+        int sampleCount = readInt();
+        int depth = readInt();
+        Entry e = new Entry();
+        e.sampleCount = sampleCount;
+        e.stackId = new int[depth];
+        for (int i = 0; i < depth; i++) {
+            e.stackId[i] = readInt();
+        }
+        return e;
+    }
+
+    private int readInt() throws IOException {
+        mIn.read(mScratch, 0, 4);
+        return (mScratch[0] & 0xff) |
+                ((mScratch[1] & 0xff) << 8) |
+                ((mScratch[2] & 0xff) << 16) |
+                ((mScratch[3] & 0xff) << 24);
+    }
+}
diff --git a/tests/src/com/android/photos/data/DataTestRunner.java b/tests/src/com/android/photos/data/DataTestRunner.java
new file mode 100644
index 0000000..10618d6
--- /dev/null
+++ b/tests/src/com/android/photos/data/DataTestRunner.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+
+import com.android.photos.data.TestHelper.TestInitialization;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+public class DataTestRunner extends InstrumentationTestRunner {
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(PhotoDatabaseTest.class);
+        suite.addTestSuite(PhotoProviderTest.class);
+        TestHelper.addTests(MediaCacheTest.class, suite, new TestInitialization() {
+            @Override
+            public void initialize(TestCase testCase) {
+                MediaCacheTest test = (MediaCacheTest) testCase;
+                test.setLocalContext(getContext());
+            }
+        });
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return DataTestRunner.class.getClassLoader();
+    }
+}
diff --git a/tests/src/com/android/photos/data/MediaCacheTest.java b/tests/src/com/android/photos/data/MediaCacheTest.java
new file mode 100644
index 0000000..9e71128
--- /dev/null
+++ b/tests/src/com/android/photos/data/MediaCacheTest.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.test.ProviderTestCase2;
+
+import com.android.gallery3d.tests.R;
+import com.android.photos.data.MediaCache.ImageReady;
+import com.android.photos.data.MediaCache.OriginalReady;
+import com.android.photos.data.MediaRetriever.MediaSize;
+import com.android.photos.data.PhotoProvider.Photos;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class MediaCacheTest extends ProviderTestCase2<PhotoProvider> {
+    @SuppressWarnings("unused")
+    private static final String TAG = MediaCacheTest.class.getSimpleName();
+
+    private File mDir;
+    private File mImage;
+    private File mCacheDir;
+    private Resources mResources;
+    private MediaCache mMediaCache;
+    private ReadyCollector mReady;
+
+    public static final long MAX_WAIT = 2000;
+
+    private static class ReadyCollector implements ImageReady, OriginalReady {
+        public File mOriginalFile;
+        public InputStream mInputStream;
+
+        @Override
+        public synchronized void originalReady(File originalFile) {
+            mOriginalFile = originalFile;
+            notifyAll();
+        }
+
+        @Override
+        public synchronized void imageReady(InputStream bitmapInputStream) {
+            mInputStream = bitmapInputStream;
+            notifyAll();
+        }
+
+        public synchronized boolean waitForNotification() {
+            long endWait = SystemClock.uptimeMillis() + MAX_WAIT;
+
+            try {
+                while (mInputStream == null && mOriginalFile == null
+                        && SystemClock.uptimeMillis() < endWait) {
+                    wait(endWait - SystemClock.uptimeMillis());
+                }
+            } catch (InterruptedException e) {
+            }
+            return mInputStream != null || mOriginalFile != null;
+        }
+    }
+
+    private static class DummyMediaRetriever implements MediaRetriever {
+        private boolean mNullUri = false;
+        @Override
+        public File getLocalFile(Uri contentUri) {
+            return null;
+        }
+
+        @Override
+        public MediaSize getFastImageSize(Uri contentUri, MediaSize size) {
+            return null;
+        }
+
+        @Override
+        public byte[] getTemporaryImage(Uri contentUri, MediaSize temporarySize) {
+            return null;
+        }
+
+        @Override
+        public boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile) {
+            return false;
+        }
+
+        @Override
+        public Uri normalizeUri(Uri contentUri, MediaSize size) {
+            if (mNullUri) {
+                return null;
+            } else {
+                return contentUri;
+            }
+        }
+
+        @Override
+        public MediaSize normalizeMediaSize(Uri contentUri, MediaSize size) {
+            return size;
+        }
+
+        public void setNullUri() {
+            mNullUri = true;
+        }
+    };
+
+    public MediaCacheTest() {
+        super(PhotoProvider.class, PhotoProvider.AUTHORITY);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mReady = new ReadyCollector();
+        File externalDir = Environment.getExternalStorageDirectory();
+        mDir = new File(externalDir, "test");
+        mDir.mkdirs();
+        mCacheDir = new File(externalDir, "test_cache");
+        mImage = new File(mDir, "original.jpg");
+        MediaCache.initialize(getMockContext());
+        MediaCache.getInstance().setCacheDir(mCacheDir);
+        mMediaCache = MediaCache.getInstance();
+        mMediaCache.addRetriever("file", "", new FileRetriever());
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mMediaCache.clearCacheDir();
+        MediaCache.shutdown();
+        mMediaCache = null;
+        mImage.delete();
+        mDir.delete();
+        mCacheDir.delete();
+    }
+
+    public void setLocalContext(Context context) {
+        mResources = context.getResources();
+    }
+
+    public void testRetrieveOriginal() throws IOException {
+        copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+        mMediaCache.retrieveOriginal(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        assertNull(mReady.mInputStream);
+        assertEquals(mImage, mReady.mOriginalFile);
+    }
+
+    public void testRetrievePreview() throws IOException {
+        copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+        mMediaCache.retrievePreview(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        assertNotNull(mReady.mInputStream);
+        assertNull(mReady.mOriginalFile);
+        Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+        mReady.mInputStream.close();
+        assertNotNull(bitmap);
+        Bitmap original = BitmapFactory.decodeFile(mImage.getPath());
+        assertTrue(bitmap.getWidth() < original.getWidth());
+        assertTrue(bitmap.getHeight() < original.getHeight());
+        int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+        int targetSize = MediaCacheUtils.getTargetSize(MediaSize.Preview);
+        assertTrue(maxDimension >= targetSize);
+        assertTrue(maxDimension < (targetSize * 2));
+    }
+
+    public void testRetrieveExifThumb() throws IOException {
+        copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+        ReadyCollector done = new ReadyCollector();
+        mMediaCache.retrieveThumbnail(uri, done, mReady);
+        assertTrue(mReady.waitForNotification());
+        assertNotNull(mReady.mInputStream);
+        assertNull(mReady.mOriginalFile);
+        Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+        mReady.mInputStream.close();
+        assertTrue(done.waitForNotification());
+        assertNotNull(done.mInputStream);
+        done.mInputStream.close();
+        assertNotNull(bitmap);
+        assertEquals(320, bitmap.getWidth());
+        assertEquals(240, bitmap.getHeight());
+    }
+
+    public void testRetrieveThumb() throws IOException {
+        copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+        long downsampleStart = SystemClock.uptimeMillis();
+        mMediaCache.retrieveThumbnail(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        long downsampleEnd = SystemClock.uptimeMillis();
+        assertNotNull(mReady.mInputStream);
+        assertNull(mReady.mOriginalFile);
+        Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+        mReady.mInputStream.close();
+        assertNotNull(bitmap);
+        Bitmap original = BitmapFactory.decodeFile(mImage.getPath());
+        assertTrue(bitmap.getWidth() < original.getWidth());
+        assertTrue(bitmap.getHeight() < original.getHeight());
+        int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+        int targetSize = MediaCacheUtils.getTargetSize(MediaSize.Thumbnail);
+        assertTrue(maxDimension >= targetSize);
+        assertTrue(maxDimension < (targetSize * 2));
+
+        // Retrieve cached thumb.
+        mReady = new ReadyCollector();
+        long start = SystemClock.uptimeMillis();
+        mMediaCache.retrieveThumbnail(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        mReady.mInputStream.close();
+        long end = SystemClock.uptimeMillis();
+        // Already cached. Wait shorter time.
+        assertTrue((end - start) < (downsampleEnd - downsampleStart) / 2);
+    }
+
+    public void testGetVideo() throws IOException {
+        mImage = new File(mDir, "original.mp4");
+        copyResourceToFile(R.raw.android_lawn, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+
+        mMediaCache.retrieveOriginal(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        assertNull(mReady.mInputStream);
+        assertNotNull(mReady.mOriginalFile);
+
+        mReady = new ReadyCollector();
+        mMediaCache.retrievePreview(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        assertNotNull(mReady.mInputStream);
+        assertNull(mReady.mOriginalFile);
+        Bitmap bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+        mReady.mInputStream.close();
+        int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+        int targetSize = MediaCacheUtils.getTargetSize(MediaSize.Preview);
+        assertTrue(maxDimension >= targetSize);
+        assertTrue(maxDimension < (targetSize * 2));
+
+        mReady = new ReadyCollector();
+        mMediaCache.retrieveThumbnail(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        assertNotNull(mReady.mInputStream);
+        assertNull(mReady.mOriginalFile);
+        bitmap = BitmapFactory.decodeStream(mReady.mInputStream);
+        mReady.mInputStream.close();
+        maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+        targetSize = MediaCacheUtils.getTargetSize(MediaSize.Thumbnail);
+        assertTrue(maxDimension >= targetSize);
+        assertTrue(maxDimension < (targetSize * 2));
+    }
+
+    public void testFastImage() throws IOException {
+        copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+        mMediaCache.retrieveThumbnail(uri, mReady, null);
+        mReady.waitForNotification();
+        mReady.mInputStream.close();
+
+        mMediaCache.retrieveOriginal(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        assertNotNull(mReady.mInputStream);
+        mReady.mInputStream.close();
+    }
+
+    public void testBadRetriever() {
+        Uri uri = Photos.CONTENT_URI;
+        try {
+            mMediaCache.retrieveOriginal(uri, mReady, null);
+            fail("Expected exception");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void testInsertIntoCache() throws IOException {
+        // FileRetriever inserts into the cache opportunistically with Videos
+        mImage = new File(mDir, "original.mp4");
+        copyResourceToFile(R.raw.android_lawn, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+
+        mMediaCache.retrieveThumbnail(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        mReady.mInputStream.close();
+        assertNotNull(mMediaCache.getCachedFile(uri, MediaSize.Preview));
+    }
+
+    public void testBadNormalizedUri() {
+        DummyMediaRetriever retriever = new DummyMediaRetriever();
+        Uri uri = Uri.fromParts("http", "world", "morestuff");
+        mMediaCache.addRetriever(uri.getScheme(), uri.getAuthority(), retriever);
+        retriever.setNullUri();
+        try {
+            mMediaCache.retrieveOriginal(uri, mReady, null);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void testClearOldCache() throws IOException {
+        copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+        Uri uri = Uri.fromFile(mImage);
+        mMediaCache.retrievePreview(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        mReady.mInputStream.close();
+        mMediaCache.setMaxCacheSize(mMediaCache.getCachedFile(uri, MediaSize.Preview).length());
+        assertNotNull(mMediaCache.getCachedFile(uri, MediaSize.Preview));
+
+        mReady = new ReadyCollector();
+        // This should kick the preview image out of the cache.
+        mMediaCache.retrieveThumbnail(uri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+        mReady.mInputStream.close();
+        assertNull(mMediaCache.getCachedFile(uri, MediaSize.Preview));
+        assertNotNull(mMediaCache.getCachedFile(uri, MediaSize.Thumbnail));
+    }
+
+    public void testClearLargeInCache() throws IOException {
+        copyResourceToFile(R.raw.galaxy_nexus, mImage.getPath());
+        Uri imageUri = Uri.fromFile(mImage);
+        mMediaCache.retrieveThumbnail(imageUri, mReady, null);
+        assertTrue(mReady.waitForNotification());
+            mReady.mInputStream.close();
+        assertNotNull(mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail));
+        long thumbSize = mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail).length();
+        mMediaCache.setMaxCacheSize(thumbSize * 10);
+
+        for (int i = 0; i < 9; i++) {
+            File tempImage = new File(mDir, "image" + i + ".jpg");
+            mImage.renameTo(tempImage);
+            Uri tempImageUri = Uri.fromFile(tempImage);
+            mReady = new ReadyCollector();
+            mMediaCache.retrieveThumbnail(tempImageUri, mReady, null);
+            assertTrue(mReady.waitForNotification());
+                mReady.mInputStream.close();
+            tempImage.renameTo(mImage);
+        }
+        assertNotNull(mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail));
+
+        for (int i = 0; i < 9; i++) {
+            File tempImage = new File(mDir, "image" + i + ".jpg");
+            mImage.renameTo(tempImage);
+            Uri tempImageUri = Uri.fromFile(tempImage);
+            mReady = new ReadyCollector();
+            mMediaCache.retrievePreview(tempImageUri, mReady, null);
+            assertTrue(mReady.waitForNotification());
+                mReady.mInputStream.close();
+            tempImage.renameTo(mImage);
+        }
+        assertNotNull(mMediaCache.getCachedFile(imageUri, MediaSize.Thumbnail));
+        Uri oldestUri = Uri.fromFile(new File(mDir, "image0.jpg"));
+        assertNull(mMediaCache.getCachedFile(oldestUri, MediaSize.Thumbnail));
+    }
+
+    private void copyResourceToFile(int resourceId, String path) throws IOException {
+        File outputDir = new File(path).getParentFile();
+        outputDir.mkdirs();
+
+        InputStream in = mResources.openRawResource(resourceId);
+        FileOutputStream out = new FileOutputStream(path);
+        byte[] buffer = new byte[1000];
+        int bytesRead;
+
+        while ((bytesRead = in.read(buffer)) >= 0) {
+            out.write(buffer, 0, bytesRead);
+        }
+
+        in.close();
+        out.close();
+    }
+}
diff --git a/tests/src/com/android/photos/data/PhotoDatabaseTest.java b/tests/src/com/android/photos/data/PhotoDatabaseTest.java
new file mode 100644
index 0000000..e7c1689
--- /dev/null
+++ b/tests/src/com/android/photos/data/PhotoDatabaseTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.test.InstrumentationTestCase;
+
+import com.android.photos.data.PhotoProvider.Accounts;
+import com.android.photos.data.PhotoProvider.Albums;
+import com.android.photos.data.PhotoProvider.Metadata;
+import com.android.photos.data.PhotoProvider.Photos;
+
+import java.io.File;
+import java.io.IOException;
+
+public class PhotoDatabaseTest extends InstrumentationTestCase {
+
+    private PhotoDatabase mDBHelper;
+    private static final String DB_NAME = "dummy.db";
+    private static final long PARENT_ID1 = 100;
+    private static final long PARENT_ID2 = 101;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        Context context = getInstrumentation().getTargetContext();
+        context.deleteDatabase(DB_NAME);
+        mDBHelper = new PhotoDatabase(context, DB_NAME);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDBHelper.close();
+        mDBHelper = null;
+        Context context = getInstrumentation().getTargetContext();
+        context.deleteDatabase(DB_NAME);
+        super.tearDown();
+    }
+
+    public void testCreateDatabase() throws IOException {
+        Context context = getInstrumentation().getTargetContext();
+        File dbFile = context.getDatabasePath(DB_NAME);
+        SQLiteDatabase db = getReadableDB();
+        db.beginTransaction();
+        db.endTransaction();
+        assertTrue(dbFile.exists());
+    }
+
+    public void testTables() {
+        validateTable(Metadata.TABLE, PhotoDatabaseUtils.PROJECTION_METADATA);
+        validateTable(Albums.TABLE, PhotoDatabaseUtils.PROJECTION_ALBUMS);
+        validateTable(Photos.TABLE, PhotoDatabaseUtils.PROJECTION_PHOTOS);
+    }
+
+    public void testAlbumsConstraints() {
+        SQLiteDatabase db = getWritableDB();
+        db.beginTransaction();
+        try {
+            long accountId = 100;
+            // Test NOT NULL constraint on name
+            assertFalse(PhotoDatabaseUtils.insertAlbum(db, null, null, Albums.VISIBILITY_PRIVATE,
+                    accountId));
+
+            // test NOT NULL constraint on privacy
+            assertFalse(PhotoDatabaseUtils.insertAlbum(db, null, "hello", null, accountId));
+
+            // test NOT NULL constraint on account_id
+            assertFalse(PhotoDatabaseUtils.insertAlbum(db, null, "hello",
+                    Albums.VISIBILITY_PRIVATE, null));
+
+            // Normal insert
+            assertTrue(PhotoDatabaseUtils.insertAlbum(db, PARENT_ID1, "hello",
+                    Albums.VISIBILITY_PRIVATE, accountId));
+
+            long albumId = PhotoDatabaseUtils.queryAlbumIdFromParentId(db, PARENT_ID1);
+
+            // Assign a valid child
+            assertTrue(PhotoDatabaseUtils.insertAlbum(db, PARENT_ID2, "hello",
+                    Albums.VISIBILITY_PRIVATE, accountId));
+
+            long otherAlbumId = PhotoDatabaseUtils.queryAlbumIdFromParentId(db, PARENT_ID2);
+            assertNotSame(albumId, otherAlbumId);
+
+            // This is a valid child of another album.
+            assertTrue(PhotoDatabaseUtils.insertAlbum(db, otherAlbumId, "hello",
+                    Albums.VISIBILITY_PRIVATE, accountId));
+
+            // This isn't allowed due to uniqueness constraint (parent_id/name)
+            assertFalse(PhotoDatabaseUtils.insertAlbum(db, otherAlbumId, "hello",
+                    Albums.VISIBILITY_PRIVATE, accountId));
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public void testPhotosConstraints() {
+        SQLiteDatabase db = getWritableDB();
+        db.beginTransaction();
+        try {
+            int width = 100;
+            int height = 100;
+            long dateTaken = System.currentTimeMillis();
+            String mimeType = "test/test";
+            long accountId = 100;
+
+            // Test NOT NULL mime-type
+            assertFalse(PhotoDatabaseUtils.insertPhoto(db, width, height, dateTaken, null, null,
+                    accountId));
+
+            // Test NOT NULL width
+            assertFalse(PhotoDatabaseUtils.insertPhoto(db, null, height, dateTaken, null, mimeType,
+                    accountId));
+
+            // Test NOT NULL height
+            assertFalse(PhotoDatabaseUtils.insertPhoto(db, width, null, dateTaken, null, mimeType,
+                    accountId));
+
+            // Test NOT NULL dateTaken
+            assertFalse(PhotoDatabaseUtils.insertPhoto(db, width, height, null, null, mimeType,
+                    accountId));
+
+            // Test NOT NULL accountId
+            assertFalse(PhotoDatabaseUtils.insertPhoto(db, width, height, dateTaken, null,
+                    mimeType, null));
+
+            // Test normal insert
+            assertTrue(PhotoDatabaseUtils.insertPhoto(db, width, height, dateTaken, null, mimeType,
+                    accountId));
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public void testMetadataConstraints() {
+        SQLiteDatabase db = getWritableDB();
+        db.beginTransaction();
+        try {
+            final String mimeType = "test/test";
+            PhotoDatabaseUtils.insertPhoto(db, 100, 100, 100L, PARENT_ID1, mimeType, 100L);
+            long photoId = PhotoDatabaseUtils.queryPhotoIdFromAlbumId(db, PARENT_ID1);
+
+            // Test NOT NULL PHOTO_ID constraint.
+            assertFalse(PhotoDatabaseUtils.insertMetadata(db, null, "foo", "bar"));
+
+            // Normal insert.
+            assertTrue(PhotoDatabaseUtils.insertMetadata(db, photoId, "foo", "bar"));
+
+            // Test uniqueness constraint.
+            assertFalse(PhotoDatabaseUtils.insertMetadata(db, photoId, "foo", "baz"));
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public void testAccountsConstraints() {
+        SQLiteDatabase db = getWritableDB();
+        db.beginTransaction();
+        try {
+            assertFalse(PhotoDatabaseUtils.insertAccount(db, null));
+            assertTrue(PhotoDatabaseUtils.insertAccount(db, "hello"));
+            assertTrue(PhotoDatabaseUtils.insertAccount(db, "hello"));
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    public void testUpgrade() {
+        SQLiteDatabase db = getWritableDB();
+        db.beginTransaction();
+        try {
+            assertTrue(PhotoDatabaseUtils.insertAccount(db, "Hello"));
+            assertTrue(PhotoDatabaseUtils.insertAlbum(db, PARENT_ID1, "hello",
+                    Albums.VISIBILITY_PRIVATE, 100L));
+            final String mimeType = "test/test";
+            assertTrue(PhotoDatabaseUtils.insertPhoto(db, 100, 100, 100L, PARENT_ID1, mimeType,
+                    100L));
+            // Normal insert.
+            assertTrue(PhotoDatabaseUtils.insertMetadata(db, 100L, "foo", "bar"));
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+        mDBHelper.close();
+        Context context = getInstrumentation().getTargetContext();
+        mDBHelper = new PhotoDatabase(context, DB_NAME, PhotoDatabase.DB_VERSION + 1);
+        db = getReadableDB();
+        assertEquals(0, DatabaseUtils.queryNumEntries(db, Accounts.TABLE));
+        assertEquals(0, DatabaseUtils.queryNumEntries(db, Photos.TABLE));
+        assertEquals(0, DatabaseUtils.queryNumEntries(db, Albums.TABLE));
+        assertEquals(0, DatabaseUtils.queryNumEntries(db, Metadata.TABLE));
+    }
+
+    private SQLiteDatabase getReadableDB() {
+        return mDBHelper.getReadableDatabase();
+    }
+
+    private SQLiteDatabase getWritableDB() {
+        return mDBHelper.getWritableDatabase();
+    }
+
+    private void validateTable(String table, String[] projection) {
+        SQLiteDatabase db = getReadableDB();
+        Cursor cursor = db.query(table, projection, null, null, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(cursor.getCount(), 0);
+        assertEquals(cursor.getColumnCount(), projection.length);
+        for (int i = 0; i < projection.length; i++) {
+            assertEquals(cursor.getColumnName(i), projection[i]);
+        }
+    }
+
+
+}
diff --git a/tests/src/com/android/photos/data/PhotoDatabaseUtils.java b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java
new file mode 100644
index 0000000..f7a46d4
--- /dev/null
+++ b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import com.android.photos.data.PhotoProvider.Accounts;
+import com.android.photos.data.PhotoProvider.Albums;
+import com.android.photos.data.PhotoProvider.Metadata;
+import com.android.photos.data.PhotoProvider.Photos;
+
+import junit.framework.AssertionFailedError;
+
+public class PhotoDatabaseUtils {
+    public static String[] PROJECTION_ALBUMS = {
+        Albums._ID,
+        Albums.ACCOUNT_ID,
+        Albums.PARENT_ID,
+        Albums.VISIBILITY,
+        Albums.LOCATION_STRING,
+        Albums.TITLE,
+        Albums.SUMMARY,
+        Albums.DATE_PUBLISHED,
+        Albums.DATE_MODIFIED,
+    };
+
+    public static String[] PROJECTION_METADATA = {
+        Metadata.PHOTO_ID,
+        Metadata.KEY,
+        Metadata.VALUE,
+    };
+
+    public static String[] PROJECTION_PHOTOS = {
+        Photos._ID,
+        Photos.ACCOUNT_ID,
+        Photos.WIDTH,
+        Photos.HEIGHT,
+        Photos.DATE_TAKEN,
+        Photos.ALBUM_ID,
+        Photos.MIME_TYPE,
+        Photos.TITLE,
+        Photos.DATE_MODIFIED,
+        Photos.ROTATION,
+    };
+
+    public static String[] PROJECTION_ACCOUNTS = {
+        Accounts._ID,
+        Accounts.ACCOUNT_NAME,
+    };
+
+    private static String SELECTION_ALBUM_PARENT_ID = Albums.PARENT_ID + " = ?";
+    private static String SELECTION_PHOTO_ALBUM_ID = Photos.ALBUM_ID + " = ?";
+    private static String SELECTION_ACCOUNT_ID = Accounts.ACCOUNT_NAME + " = ?";
+
+    public static long queryAlbumIdFromParentId(SQLiteDatabase db, long parentId) {
+        return queryId(db, Albums.TABLE, PROJECTION_ALBUMS, SELECTION_ALBUM_PARENT_ID, parentId);
+    }
+
+    public static long queryPhotoIdFromAlbumId(SQLiteDatabase db, long albumId) {
+        return queryId(db, Photos.TABLE, PROJECTION_PHOTOS, SELECTION_PHOTO_ALBUM_ID, albumId);
+    }
+
+    public static long queryAccountIdFromName(SQLiteDatabase db, String accountName) {
+        return queryId(db, Accounts.TABLE, PROJECTION_ACCOUNTS, SELECTION_ACCOUNT_ID, accountName);
+    }
+
+    public static long queryId(SQLiteDatabase db, String table, String[] projection,
+            String selection, Object parameter) {
+        String paramString = parameter == null ? null : parameter.toString();
+        String[] selectionArgs = {
+            paramString,
+        };
+        Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, null);
+        try {
+            if (cursor.getCount() != 1 || !cursor.moveToNext()) {
+                throw new AssertionFailedError("Couldn't find item in table");
+            }
+            long id = cursor.getLong(0);
+            return id;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    public static boolean insertPhoto(SQLiteDatabase db, Integer width, Integer height,
+            Long dateTaken, Long albumId, String mimeType, Long accountId) {
+        ContentValues values = new ContentValues();
+        values.put(Photos.WIDTH, width);
+        values.put(Photos.HEIGHT, height);
+        values.put(Photos.DATE_TAKEN, dateTaken);
+        values.put(Photos.ALBUM_ID, albumId);
+        values.put(Photos.MIME_TYPE, mimeType);
+        values.put(Photos.ACCOUNT_ID, accountId);
+        return db.insert(Photos.TABLE, null, values) != -1;
+    }
+
+    public static boolean insertAlbum(SQLiteDatabase db, Long parentId, String title,
+            Integer privacy, Long accountId) {
+        ContentValues values = new ContentValues();
+        values.put(Albums.PARENT_ID, parentId);
+        values.put(Albums.TITLE, title);
+        values.put(Albums.VISIBILITY, privacy);
+        values.put(Albums.ACCOUNT_ID, accountId);
+        return db.insert(Albums.TABLE, null, values) != -1;
+    }
+
+    public static boolean insertMetadata(SQLiteDatabase db, Long photosId, String key, String value) {
+        ContentValues values = new ContentValues();
+        values.put(Metadata.PHOTO_ID, photosId);
+        values.put(Metadata.KEY, key);
+        values.put(Metadata.VALUE, value);
+        return db.insert(Metadata.TABLE, null, values) != -1;
+    }
+
+    public static boolean insertAccount(SQLiteDatabase db, String name) {
+        ContentValues values = new ContentValues();
+        values.put(Accounts.ACCOUNT_NAME, name);
+        return db.insert(Accounts.TABLE, null, values) != -1;
+    }
+}
diff --git a/tests/src/com/android/photos/data/PhotoProviderTest.java b/tests/src/com/android/photos/data/PhotoProviderTest.java
new file mode 100644
index 0000000..685946e
--- /dev/null
+++ b/tests/src/com/android/photos/data/PhotoProviderTest.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.BaseColumns;
+import android.test.ProviderTestCase2;
+
+import com.android.photos.data.PhotoProvider.Accounts;
+import com.android.photos.data.PhotoProvider.Albums;
+import com.android.photos.data.PhotoProvider.Metadata;
+import com.android.photos.data.PhotoProvider.Photos;
+
+import java.util.ArrayList;
+
+public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> {
+    @SuppressWarnings("unused")
+    private static final String TAG = PhotoProviderTest.class.getSimpleName();
+
+    private static final String MIME_TYPE = "test/test";
+    private static final String ALBUM_TITLE = "My Album";
+    private static final long ALBUM_PARENT_ID = 100;
+    private static final String META_KEY = "mykey";
+    private static final String META_VALUE = "myvalue";
+    private static final String ACCOUNT_NAME = "foo@bar.com";
+
+    private static final Uri NO_TABLE_URI = PhotoProvider.BASE_CONTENT_URI;
+    private static final Uri BAD_TABLE_URI = Uri.withAppendedPath(PhotoProvider.BASE_CONTENT_URI,
+            "bad_table");
+
+    private static final String WHERE_METADATA_PHOTOS_ID = Metadata.PHOTO_ID + " = ?";
+    private static final String WHERE_METADATA = Metadata.PHOTO_ID + " = ? AND " + Metadata.KEY
+            + " = ?";
+
+    private long mAlbumId;
+    private long mPhotoId;
+    private long mMetadataId;
+    private long mAccountId;
+
+    private SQLiteOpenHelper mDBHelper;
+    private ContentResolver mResolver;
+    private NotificationWatcher mNotifications = new NotificationWatcher();
+
+    public PhotoProviderTest() {
+        super(PhotoProvider.class, PhotoProvider.AUTHORITY);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mResolver = getMockContentResolver();
+        PhotoProvider provider = (PhotoProvider) getProvider();
+        provider.setMockNotification(mNotifications);
+        mDBHelper = provider.getDatabaseHelper();
+        SQLiteDatabase db = mDBHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            PhotoDatabaseUtils.insertAccount(db, ACCOUNT_NAME);
+            mAccountId = PhotoDatabaseUtils.queryAccountIdFromName(db, ACCOUNT_NAME);
+            PhotoDatabaseUtils.insertAlbum(db, ALBUM_PARENT_ID, ALBUM_TITLE,
+                    Albums.VISIBILITY_PRIVATE, mAccountId);
+            mAlbumId = PhotoDatabaseUtils.queryAlbumIdFromParentId(db, ALBUM_PARENT_ID);
+            PhotoDatabaseUtils.insertPhoto(db, 100, 100, System.currentTimeMillis(), mAlbumId,
+                    MIME_TYPE, mAccountId);
+            mPhotoId = PhotoDatabaseUtils.queryPhotoIdFromAlbumId(db, mAlbumId);
+            PhotoDatabaseUtils.insertMetadata(db, mPhotoId, META_KEY, META_VALUE);
+            String[] projection = {
+                    BaseColumns._ID,
+            };
+            Cursor cursor = db.query(Metadata.TABLE, projection, null, null, null, null, null);
+            cursor.moveToNext();
+            mMetadataId = cursor.getLong(0);
+            cursor.close();
+            db.setTransactionSuccessful();
+            mNotifications.reset();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDBHelper.close();
+        mDBHelper = null;
+        super.tearDown();
+        getMockContext().deleteDatabase(PhotoProvider.DB_NAME);
+    }
+
+    public void testDelete() {
+        try {
+            mResolver.delete(NO_TABLE_URI, null, null);
+            fail("Exeption should be thrown when no table given");
+        } catch (Exception e) {
+            // expected exception
+        }
+        try {
+            mResolver.delete(BAD_TABLE_URI, null, null);
+            fail("Exeption should be thrown when deleting from a table that doesn't exist");
+        } catch (Exception e) {
+            // expected exception
+        }
+
+        String[] selectionArgs = {
+            String.valueOf(mPhotoId)
+        };
+        // Delete some metadata
+        assertEquals(1,
+                mResolver.delete(Metadata.CONTENT_URI, WHERE_METADATA_PHOTOS_ID, selectionArgs));
+        Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
+        assertEquals(1, mResolver.delete(photoUri, null, null));
+        Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId);
+        assertEquals(1, mResolver.delete(albumUri, null, null));
+        // now delete something that isn't there
+        assertEquals(0, mResolver.delete(photoUri, null, null));
+    }
+
+    public void testDeleteMetadataId() {
+        Uri metadataUri = ContentUris.withAppendedId(Metadata.CONTENT_URI, mMetadataId);
+        assertEquals(1, mResolver.delete(metadataUri, null, null));
+        Cursor cursor = mResolver.query(Metadata.CONTENT_URI, null, null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+    }
+
+    // Delete the album and ensure that the photos referring to the album are
+    // deleted.
+    public void testDeleteAlbumCascade() {
+        Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId);
+        mResolver.delete(albumUri, null, null);
+        assertTrue(mNotifications.isNotified(Photos.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(albumUri));
+        assertEquals(3, mNotifications.notificationCount());
+        Cursor cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS,
+                null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+    }
+
+    // Delete all albums and ensure that photos in any album are deleted.
+    public void testDeleteAlbumCascade2() {
+        mResolver.delete(Albums.CONTENT_URI, null, null);
+        assertTrue(mNotifications.isNotified(Photos.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(Albums.CONTENT_URI));
+        assertEquals(3, mNotifications.notificationCount());
+        Cursor cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS,
+                null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+    }
+
+    // Delete a photo and ensure that the metadata for that photo are deleted.
+    public void testDeletePhotoCascade() {
+        Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
+        mResolver.delete(photoUri, null, null);
+        assertTrue(mNotifications.isNotified(photoUri));
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+        assertEquals(2, mNotifications.notificationCount());
+        Cursor cursor = mResolver.query(Metadata.CONTENT_URI,
+                PhotoDatabaseUtils.PROJECTION_METADATA, null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+    }
+
+    public void testDeleteAccountCascade() {
+        Uri accountUri = ContentUris.withAppendedId(Accounts.CONTENT_URI, mAccountId);
+        SQLiteDatabase db = mDBHelper.getWritableDatabase();
+        db.beginTransaction();
+        PhotoDatabaseUtils.insertPhoto(db, 100, 100, System.currentTimeMillis(), null,
+                "image/jpeg", mAccountId);
+        PhotoDatabaseUtils.insertPhoto(db, 100, 100, System.currentTimeMillis(), null,
+                "image/jpeg", 0L);
+        PhotoDatabaseUtils.insertAlbum(db, null, "title", Albums.VISIBILITY_PRIVATE, 10630L);
+        db.setTransactionSuccessful();
+        db.endTransaction();
+        // ensure all pictures are there:
+        Cursor cursor = mResolver.query(Photos.CONTENT_URI, null, null, null, null);
+        assertEquals(3, cursor.getCount());
+        cursor.close();
+        // delete the account
+        assertEquals(1, mResolver.delete(accountUri, null, null));
+        // now ensure that all associated photos were deleted
+        cursor = mResolver.query(Photos.CONTENT_URI, null, null, null, null);
+        assertEquals(1, cursor.getCount());
+        cursor.close();
+        // now ensure all associated albums were deleted.
+        cursor = mResolver.query(Albums.CONTENT_URI, null, null, null, null);
+        assertEquals(1, cursor.getCount());
+        cursor.close();
+    }
+
+    public void testGetType() {
+        // We don't return types for albums
+        assertNull(mResolver.getType(Albums.CONTENT_URI));
+
+        Uri noImage = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId + 1);
+        assertNull(mResolver.getType(noImage));
+
+        Uri image = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
+        assertEquals(MIME_TYPE, mResolver.getType(image));
+    }
+
+    public void testInsert() {
+        ContentValues values = new ContentValues();
+        values.put(Albums.TITLE, "add me");
+        values.put(Albums.VISIBILITY, Albums.VISIBILITY_PRIVATE);
+        values.put(Albums.ACCOUNT_ID, 100L);
+        values.put(Albums.DATE_MODIFIED, 100L);
+        values.put(Albums.DATE_PUBLISHED, 100L);
+        values.put(Albums.LOCATION_STRING, "Home");
+        values.put(Albums.TITLE, "hello world");
+        values.putNull(Albums.PARENT_ID);
+        values.put(Albums.SUMMARY, "Nothing much to say about this");
+        Uri insertedUri = mResolver.insert(Albums.CONTENT_URI, values);
+        assertNotNull(insertedUri);
+        Cursor cursor = mResolver.query(insertedUri, PhotoDatabaseUtils.PROJECTION_ALBUMS, null,
+                null, null);
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.close();
+    }
+
+    public void testUpdate() {
+        ContentValues values = new ContentValues();
+        // Normal update -- use an album.
+        values.put(Albums.TITLE, "foo");
+        Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId);
+        assertEquals(1, mResolver.update(albumUri, values, null, null));
+        String[] projection = {
+            Albums.TITLE,
+        };
+        Cursor cursor = mResolver.query(albumUri, projection, null, null, null);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToNext());
+        assertEquals("foo", cursor.getString(0));
+        cursor.close();
+
+        // Update a row that doesn't exist.
+        Uri noAlbumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId + 1);
+        values.put(Albums.TITLE, "bar");
+        assertEquals(0, mResolver.update(noAlbumUri, values, null, null));
+
+        // Update a metadata value that exists.
+        ContentValues metadata = new ContentValues();
+        metadata.put(Metadata.PHOTO_ID, mPhotoId);
+        metadata.put(Metadata.KEY, META_KEY);
+        metadata.put(Metadata.VALUE, "new value");
+        assertEquals(1, mResolver.update(Metadata.CONTENT_URI, metadata, null, null));
+
+        projection = new String[] {
+            Metadata.VALUE,
+        };
+
+        String[] selectionArgs = {
+                String.valueOf(mPhotoId), META_KEY,
+        };
+
+        cursor = mResolver.query(Metadata.CONTENT_URI, projection, WHERE_METADATA, selectionArgs,
+                null);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToNext());
+        assertEquals("new value", cursor.getString(0));
+        cursor.close();
+
+        // Update a metadata value that doesn't exist.
+        metadata.put(Metadata.KEY, "other stuff");
+        assertEquals(1, mResolver.update(Metadata.CONTENT_URI, metadata, null, null));
+
+        selectionArgs[1] = "other stuff";
+        cursor = mResolver.query(Metadata.CONTENT_URI, projection, WHERE_METADATA, selectionArgs,
+                null);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToNext());
+        assertEquals("new value", cursor.getString(0));
+        cursor.close();
+
+        // Remove a metadata value using update.
+        metadata.putNull(Metadata.VALUE);
+        assertEquals(1, mResolver.update(Metadata.CONTENT_URI, metadata, null, null));
+        cursor = mResolver.query(Metadata.CONTENT_URI, projection, WHERE_METADATA, selectionArgs,
+                null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+    }
+
+    public void testQuery() {
+        // Query a photo that exists.
+        Cursor cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS,
+                null, null, null);
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToNext());
+        assertEquals(mPhotoId, cursor.getLong(0));
+        cursor.close();
+
+        // Query a photo that doesn't exist.
+        Uri noPhotoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId + 1);
+        cursor = mResolver.query(noPhotoUri, PhotoDatabaseUtils.PROJECTION_PHOTOS, null, null,
+                null);
+        assertNotNull(cursor);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+
+        // Query a photo that exists using selection arguments.
+        String[] selectionArgs = {
+            String.valueOf(mPhotoId),
+        };
+
+        cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS,
+                Photos._ID + " = ?", selectionArgs, null);
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToNext());
+        assertEquals(mPhotoId, cursor.getLong(0));
+        cursor.close();
+    }
+
+    public void testUpdatePhotoNotification() {
+        Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
+        ContentValues values = new ContentValues();
+        values.put(Photos.MIME_TYPE, "not-a/mime-type");
+        mResolver.update(photoUri, values, null, null);
+        assertTrue(mNotifications.isNotified(photoUri));
+    }
+
+    public void testUpdateMetadataNotification() {
+        ContentValues values = new ContentValues();
+        values.put(Metadata.PHOTO_ID, mPhotoId);
+        values.put(Metadata.KEY, META_KEY);
+        values.put(Metadata.VALUE, "hello world");
+        mResolver.update(Metadata.CONTENT_URI, values, null, null);
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+    }
+
+    public void testBatchTransaction() throws RemoteException, OperationApplicationException {
+        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
+        ContentProviderOperation.Builder insert = ContentProviderOperation
+                .newInsert(Photos.CONTENT_URI);
+        insert.withValue(Photos.WIDTH, 200L);
+        insert.withValue(Photos.HEIGHT, 100L);
+        insert.withValue(Photos.DATE_TAKEN, System.currentTimeMillis());
+        insert.withValue(Photos.ALBUM_ID, 1000L);
+        insert.withValue(Photos.MIME_TYPE, "image/jpg");
+        insert.withValue(Photos.ACCOUNT_ID, 1L);
+        operations.add(insert.build());
+        ContentProviderOperation.Builder update = ContentProviderOperation.newUpdate(Photos.CONTENT_URI);
+        update.withValue(Photos.DATE_MODIFIED, System.currentTimeMillis());
+        String[] whereArgs = {
+            "100",
+        };
+        String where = Photos.WIDTH + " = ?";
+        update.withSelection(where, whereArgs);
+        operations.add(update.build());
+        ContentProviderOperation.Builder delete = ContentProviderOperation
+                .newDelete(Photos.CONTENT_URI);
+        delete.withSelection(where, whereArgs);
+        operations.add(delete.build());
+        mResolver.applyBatch(PhotoProvider.AUTHORITY, operations);
+        assertEquals(3, mNotifications.notificationCount());
+        SQLiteDatabase db = mDBHelper.getReadableDatabase();
+        long id = PhotoDatabaseUtils.queryPhotoIdFromAlbumId(db, 1000L);
+        Uri uri = ContentUris.withAppendedId(Photos.CONTENT_URI, id);
+        assertTrue(mNotifications.isNotified(uri));
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(Photos.CONTENT_URI));
+    }
+
+}
diff --git a/tests/src/com/android/photos/data/TestHelper.java b/tests/src/com/android/photos/data/TestHelper.java
new file mode 100644
index 0000000..338e160
--- /dev/null
+++ b/tests/src/com/android/photos/data/TestHelper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.util.Log;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import java.lang.reflect.Method;
+
+public class TestHelper {
+    private static String TAG = TestHelper.class.getSimpleName();
+
+    public interface TestInitialization {
+        void initialize(TestCase testCase);
+    }
+
+    public static void addTests(Class<? extends TestCase> testClass, TestSuite suite,
+            TestInitialization initialization) {
+        for (Method method : testClass.getDeclaredMethods()) {
+            if (method.getName().startsWith("test") && method.getParameterTypes().length == 0) {
+                TestCase test;
+                try {
+                    test = testClass.newInstance();
+                    test.setName(method.getName());
+                    initialization.initialize(test);
+                    suite.addTest(test);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Failed to create test case", e);
+                } catch (InstantiationException e) {
+                    Log.e(TAG, "Failed to create test case", e);
+                } catch (IllegalAccessException e) {
+                    Log.e(TAG, "Failed to create test case", e);
+                }
+            }
+        }
+    }
+
+}
diff --git a/tests_camera/Android.mk b/tests_camera/Android.mk
new file mode 100644
index 0000000..f39533a
--- /dev/null
+++ b/tests_camera/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := 16
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CameraTests
+
+LOCAL_INSTRUMENTATION_FOR := Gallery2
+
+include $(BUILD_PACKAGE)
diff --git a/tests_camera/AndroidManifest.xml b/tests_camera/AndroidManifest.xml
new file mode 100644
index 0000000..164bbd5
--- /dev/null
+++ b/tests_camera/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?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">
+
+    <uses-permission android:name="android.permission.INJECT_EVENTS" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="com.android.camera.CameraLaunchPerformance"
+            android:targetPackage="com.android.camera"
+            android:label="Camera Launch Performance">
+    </instrumentation>
+
+    <instrumentation android:name="com.android.camera.stress.CameraStressTestRunner"
+            android:targetPackage="com.android.camera"
+            android:label="Camera stress test runner">
+    </instrumentation>
+
+    <instrumentation android:name="com.android.camera.CameraTestRunner"
+            android:targetPackage="com.android.camera"
+            android:label="Camera continuous test runner">
+    </instrumentation>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+             android:targetPackage="com.android.camera"
+             android:label="Tests for Camera application."/>
+</manifest>
diff --git a/tests_camera/src/com/android/camera/CameraLaunchPerformance.java b/tests_camera/src/com/android/camera/CameraLaunchPerformance.java
new file mode 100644
index 0000000..fe2b776
--- /dev/null
+++ b/tests_camera/src/com/android/camera/CameraLaunchPerformance.java
@@ -0,0 +1,47 @@
+/*
+ * 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.os.Bundle;
+import android.test.LaunchPerformanceBase;
+
+/**
+ * Instrumentation class for Camera launch performance testing.
+ */
+public class CameraLaunchPerformance extends LaunchPerformanceBase {
+    @SuppressWarnings("unused")
+    private static final String TAG = "CameraLaunchPerformance";
+
+    @Override
+    public void onCreate(Bundle arguments) {
+        super.onCreate(arguments);
+        mIntent.setClassName(getTargetContext(),
+                "com.android.camera.CameraActivity");
+        start();
+    }
+
+    /**
+     * Calls LaunchApp and finish.
+     */
+    @Override
+    public void onStart() {
+        super.onStart();
+        LaunchApp();
+        finish(Activity.RESULT_OK, mResults);
+    }
+}
diff --git a/tests_camera/src/com/android/camera/CameraTestRunner.java b/tests_camera/src/com/android/camera/CameraTestRunner.java
new file mode 100755
index 0000000..96c48a4
--- /dev/null
+++ b/tests_camera/src/com/android/camera/CameraTestRunner.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+
+import com.android.camera.activity.CameraActivityTest;
+import com.android.camera.functional.CameraTest;
+import com.android.camera.functional.ImageCaptureIntentTest;
+import com.android.camera.functional.VideoCaptureIntentTest;
+import com.android.camera.unittest.CameraUnitTest;
+
+import junit.framework.TestSuite;
+
+
+public class CameraTestRunner extends InstrumentationTestRunner {
+
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(CameraActivityTest.class);
+        suite.addTestSuite(CameraTest.class);
+        suite.addTestSuite(ImageCaptureIntentTest.class);
+        suite.addTestSuite(VideoCaptureIntentTest.class);
+        suite.addTestSuite(CameraUnitTest.class);
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return CameraTestRunner.class.getClassLoader();
+    }
+}
diff --git a/tests_camera/src/com/android/camera/StressTests.java b/tests_camera/src/com/android/camera/StressTests.java
new file mode 100755
index 0000000..7ed8317
--- /dev/null
+++ b/tests_camera/src/com/android/camera/StressTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import com.android.camera.stress.ImageCapture;
+import com.android.camera.stress.SwitchPreview;
+import com.android.camera.stress.CameraLatency;
+import com.android.camera.stress.CameraStartUp;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+
+/**
+ * Instrumentation Test Runner for all Camera tests.
+ *
+ * Running all tests:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.camera.StressTests \
+ *    -w com.android.camera.tests/com.android.camera.stress.CameraStressTestRunner
+ */
+
+public class StressTests extends TestSuite {
+    public static Test suite() {
+        TestSuite result = new TestSuite();
+        result.addTestSuite(SwitchPreview.class);
+        result.addTestSuite(ImageCapture.class);
+        result.addTestSuite(CameraLatency.class);
+        result.addTestSuite(CameraStartUp.class);
+        return result;
+    }
+}
diff --git a/tests_camera/src/com/android/camera/UnitTests.java b/tests_camera/src/com/android/camera/UnitTests.java
new file mode 100644
index 0000000..e56a907
--- /dev/null
+++ b/tests_camera/src/com/android/camera/UnitTests.java
@@ -0,0 +1,35 @@
+/*
+ * 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.test.suitebuilder.UnitTestSuiteBuilder;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * TestSuite for all Camera unit tests.
+ */
+public class UnitTests extends TestSuite {
+
+    public static Test suite() {
+        return new UnitTestSuiteBuilder(UnitTests.class)
+                .includePackages("com.android.camera.unittest")
+                .named("Camera Unit Tests")
+                .build();
+    }
+}
diff --git a/tests_camera/src/com/android/camera/activity/CameraActivityTest.java b/tests_camera/src/com/android/camera/activity/CameraActivityTest.java
new file mode 100644
index 0000000..eb027e9
--- /dev/null
+++ b/tests_camera/src/com/android/camera/activity/CameraActivityTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.activity;
+
+import android.hardware.Camera.Parameters;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.camera.CameraActivity;
+import com.android.camera.CameraHolder;
+import com.android.gallery3d.R;
+
+import static com.google.testing.littlemock.LittleMock.doReturn;
+
+public class CameraActivityTest extends CameraTestCase <CameraActivity> {
+    public CameraActivityTest() {
+        super(CameraActivity.class);
+    }
+
+    @LargeTest
+    public void testFailToConnect() throws Exception {
+        super.internalTestFailToConnect();
+    }
+
+    @LargeTest
+    public void testTakePicture() throws Exception {
+        CameraHolder.injectMockCamera(mCameraInfo, mOneMockCamera);
+
+        getActivity();
+        getInstrumentation().waitForIdleSync();
+
+        // Press shutter button to take a picture.
+        performClick(R.id.shutter_button);
+        getInstrumentation().waitForIdleSync();
+
+        // Force the activity to finish.
+        getActivity().finish();
+        getInstrumentation().waitForIdleSync();
+    }
+}
diff --git a/tests_camera/src/com/android/camera/activity/CameraTestCase.java b/tests_camera/src/com/android/camera/activity/CameraTestCase.java
new file mode 100644
index 0000000..27be3c7
--- /dev/null
+++ b/tests_camera/src/com/android/camera/activity/CameraTestCase.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2012 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.activity;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.hardware.Camera;
+import android.hardware.Camera.AutoFocusCallback;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.PictureCallback;
+import android.hardware.Camera.ShutterCallback;
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.camera.CameraHolder;
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+
+import static com.google.testing.littlemock.LittleMock.mock;
+import static com.google.testing.littlemock.LittleMock.doAnswer;
+import static com.google.testing.littlemock.LittleMock.doReturn;
+import static com.google.testing.littlemock.LittleMock.anyObject;
+import com.google.testing.littlemock.AppDataDirGuesser;
+import com.google.testing.littlemock.ArgumentCaptor;
+import com.google.testing.littlemock.Captor;
+import com.google.testing.littlemock.LittleMock;
+import com.google.testing.littlemock.Mock;
+
+import java.io.File;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+
+public class CameraTestCase<T extends Activity> extends ActivityInstrumentationTestCase2<T> {
+    protected CameraInfo mCameraInfo[];
+    protected CameraProxy mMockCamera[];
+    protected CameraInfo mOneCameraInfo[];
+    protected CameraProxy mOneMockCamera[];
+    private static Parameters mParameters;
+    private byte[] mBlankJpeg;
+    @Mock private CameraProxy mMockBackCamera;
+    @Mock private CameraProxy mMockFrontCamera;
+    @Captor private ArgumentCaptor<ShutterCallback> mShutterCallback;
+    @Captor private ArgumentCaptor<PictureCallback> mRawPictureCallback;
+    @Captor private ArgumentCaptor<PictureCallback> mJpegPictureCallback;
+    @Captor private ArgumentCaptor<AutoFocusCallback> mAutoFocusCallback;
+    Callable<Object> mAutoFocusCallable = new AutoFocusCallable();
+    Callable<Object> mTakePictureCallable = new TakePictureCallable();
+
+    private class TakePictureCallable implements Callable<Object> {
+        @Override
+        public Object call() throws Exception {
+            Runnable runnable = new Runnable() {
+                @Override
+                public void run() {
+                    readBlankJpeg();
+                    Camera camera = mOneMockCamera[0].getCamera();
+                    mShutterCallback.getValue().onShutter();
+                    mRawPictureCallback.getValue().onPictureTaken(null, camera);
+                    mJpegPictureCallback.getValue().onPictureTaken(mBlankJpeg, camera);
+                }
+            };
+            // Probably need some delay. Make sure shutter callback is called
+            // after onShutterButtonFocus(false).
+            getActivity().findViewById(R.id.gl_root_view).postDelayed(runnable, 50);
+            return null;
+        }
+   }
+
+    private class AutoFocusCallable implements Callable<Object> {
+        @Override
+        public Object call() throws Exception {
+            Runnable runnable = new Runnable() {
+                @Override
+                public void run() {
+                    Camera camera = mOneMockCamera[0].getCamera();
+                    mAutoFocusCallback.getValue().onAutoFocus(true, camera);
+                }
+            };
+            // Need some delay. Otherwise, focus callback will be run before
+            // onShutterButtonClick
+            getActivity().findViewById(R.id.gl_root_view).postDelayed(runnable, 50);
+            return null;
+        }
+   }
+
+    public CameraTestCase(Class<T> activityClass) {
+        super(activityClass);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        AppDataDirGuesser.setInstance(new AppDataDirGuesser() {
+            @Override
+            public File guessSuitableDirectoryForGeneratedClasses() {
+                return getInstrumentation().getTargetContext().getCacheDir();
+            }
+        });
+        AppDataDirGuesser.getsInstance().guessSuitableDirectoryForGeneratedClasses();
+        LittleMock.initMocks(this);
+        mCameraInfo = new CameraInfo[2];
+        mCameraInfo[0] = new CameraInfo();
+        mCameraInfo[0].facing = CameraInfo.CAMERA_FACING_BACK;
+        mCameraInfo[1] = new CameraInfo();
+        mCameraInfo[1].facing = CameraInfo.CAMERA_FACING_FRONT;
+        mMockCamera = new CameraProxy[2];
+        mMockCamera[0] = mMockBackCamera;
+        mMockCamera[1] = mMockFrontCamera;
+        doReturn(getParameters()).when(mMockCamera[0]).getParameters();
+        doReturn(getParameters()).when(mMockCamera[1]).getParameters();
+
+        mOneCameraInfo = new CameraInfo[1];
+        mOneCameraInfo[0] = new CameraInfo();
+        mOneCameraInfo[0].facing = CameraInfo.CAMERA_FACING_BACK;
+        mOneMockCamera = new CameraProxy[1];
+        mOneMockCamera[0] = mMockBackCamera;
+        doReturn(getParameters()).when(mOneMockCamera[0]).getParameters();
+
+        // Mock takePicture call.
+        doAnswer(mTakePictureCallable).when(mMockBackCamera).takePicture(
+                mShutterCallback.capture(), mRawPictureCallback.capture(),
+                (PictureCallback) anyObject(), mJpegPictureCallback.capture());
+
+        // Mock autoFocus call.
+        doAnswer(mAutoFocusCallable).when(mMockBackCamera).autoFocus(
+                mAutoFocusCallback.capture());
+    }
+
+    private void readBlankJpeg() {
+        InputStream ins = getActivity().getResources().openRawResource(R.raw.blank);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        int size = 0;
+
+        // Read the entire resource into a local byte buffer.
+        byte[] buffer = new byte[1024];
+        try {
+            while((size = ins.read(buffer, 0, 1024)) >= 0){
+                outputStream.write(buffer, 0, size);
+            }
+        } catch (IOException e) {
+            // ignore
+        } finally {
+            Util.closeSilently(ins);
+        }
+        mBlankJpeg = outputStream.toByteArray();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        CameraHolder.injectMockCamera(null,  null);
+    }
+
+    protected void internalTestFailToConnect() throws Exception {
+        CameraHolder.injectMockCamera(mCameraInfo, null);
+
+        getActivity();
+        Instrumentation inst = getInstrumentation();
+        inst.waitForIdleSync();
+        inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER); // close dialog
+    }
+
+    protected void performClick(final int id) {
+        Activity activity = getActivity();
+        getInstrumentation().waitForIdleSync();
+        assertNotNull(activity.findViewById(id));
+        Instrumentation inst = getInstrumentation();
+        inst.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                View v = getActivity().findViewById(id);
+                float x = (v.getLeft() + v.getRight()) / 2;
+                float y = (v.getTop() + v.getBottom()) / 2;
+                MotionEvent down = MotionEvent.obtain(0, 0,
+                        MotionEvent.ACTION_DOWN, x, y, 0, 0, 0, 0, 0, 0, 0);
+                MotionEvent up = MotionEvent.obtain(0, 0,
+                        MotionEvent.ACTION_UP, x, y, 0, 0, 0, 0, 0, 0, 0);
+                View parent = (View) v.getParent();
+                parent.dispatchTouchEvent(down);
+                parent.dispatchTouchEvent(up);
+            }
+        });
+        inst.waitForIdleSync();
+    }
+
+    protected void assertViewNotExist(int id) {
+        Activity activity = getActivity();
+        getInstrumentation().waitForIdleSync();
+        assertNull(activity.findViewById(id));
+    }
+
+    protected void assertViewNotVisible(int id) {
+        Activity activity = getActivity();
+        getInstrumentation().waitForIdleSync();
+        View view = activity.findViewById(id);
+        assertTrue(view.getVisibility() != View.VISIBLE);
+    }
+
+    protected static Parameters getParameters() {
+        synchronized (CameraTestCase.class) {
+            if (mParameters == null) {
+                mParameters = android.hardware.Camera.getEmptyParameters();
+                mParameters.unflatten("preview-format-values=yuv420sp,yuv420p,yuv422i-yuyv,yuv420p;" +
+                        "preview-format=yuv420sp;" +
+                        "preview-size-values=800x480;preview-size=800x480;" +
+                        "picture-size-values=320x240;picture-size=320x240;" +
+                        "jpeg-thumbnail-size-values=320x240,0x0;jpeg-thumbnail-width=320;jpeg-thumbnail-height=240;" +
+                        "jpeg-thumbnail-quality=60;jpeg-quality=95;" +
+                        "preview-frame-rate-values=30,15;preview-frame-rate=30;" +
+                        "focus-mode-values=continuous-video,auto,macro,infinity,continuous-picture;focus-mode=auto;" +
+                        "preview-fps-range-values=(15000,30000);preview-fps-range=15000,30000;" +
+                        "scene-mode-values=auto,action,night;scene-mode=auto;" +
+                        "flash-mode-values=off,on,auto,torch;flash-mode=off;" +
+                        "whitebalance-values=auto,daylight,fluorescent,incandescent;whitebalance=auto;" +
+                        "effect-values=none,mono,sepia;effect=none;" +
+                        "zoom-supported=true;zoom-ratios=100,200,400;max-zoom=2;" +
+                        "picture-format-values=jpeg;picture-format=jpeg;" +
+                        "min-exposure-compensation=-30;max-exposure-compensation=30;" +
+                        "exposure-compensation=0;exposure-compensation-step=0.1;" +
+                        "horizontal-view-angle=40;vertical-view-angle=40;");
+            }
+        }
+        return mParameters;
+    }
+}
diff --git a/tests_camera/src/com/android/camera/functional/CameraTest.java b/tests_camera/src/com/android/camera/functional/CameraTest.java
new file mode 100644
index 0000000..3fdebc0
--- /dev/null
+++ b/tests_camera/src/com/android/camera/functional/CameraTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 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.functional;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Process;
+import android.provider.MediaStore;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class CameraTest extends InstrumentationTestCase {
+    @LargeTest
+    public void testVideoCaptureIntentFdLeak() throws Exception {
+        Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.parse("file://"
+                + Environment.getExternalStorageDirectory().toString()
+                + "test_fd_leak.3gp"));
+        getInstrumentation().startActivitySync(intent).finish();
+        // Test if the fd is closed.
+        for (File f: new File("/proc/" + Process.myPid() + "/fd").listFiles()) {
+            assertEquals(-1, f.getCanonicalPath().indexOf("test_fd_leak.3gp"));
+        }
+    }
+
+    @LargeTest
+    public void testActivityLeak() throws Exception {
+        checkActivityLeak(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
+        checkActivityLeak(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+    }
+
+    private void checkActivityLeak(String action) throws Exception {
+        final int TEST_COUNT = 5;
+        Intent intent = new Intent(action);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setClass(getInstrumentation().getTargetContext(),
+                CameraActivity.class);
+        ArrayList<WeakReference<Activity>> refs =
+                new ArrayList<WeakReference<Activity>>();
+        for (int i = 0; i < TEST_COUNT; i++) {
+            Activity activity = getInstrumentation().startActivitySync(intent);
+            refs.add(new WeakReference<Activity>(activity));
+            activity.finish();
+            getInstrumentation().waitForIdleSync();
+            activity = null;
+        }
+        Runtime.getRuntime().gc();
+        Runtime.getRuntime().runFinalization();
+        Runtime.getRuntime().gc();
+        int refCount = 0;
+        for (WeakReference<Activity> c: refs) {
+            if (c.get() != null) refCount++;
+        }
+        // If applications are leaking activity, every reference is reachable.
+        assertTrue(refCount != TEST_COUNT);
+    }
+}
diff --git a/tests_camera/src/com/android/camera/functional/ImageCaptureIntentTest.java b/tests_camera/src/com/android/camera/functional/ImageCaptureIntentTest.java
new file mode 100644
index 0000000..54ac1b4
--- /dev/null
+++ b/tests_camera/src/com/android/camera/functional/ImageCaptureIntentTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2011 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.functional;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+
+public class ImageCaptureIntentTest extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private Intent mIntent;
+
+    public ImageCaptureIntentTest() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+    }
+
+    @LargeTest
+    public void testNoExtraOutput() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        takePicture();
+        pressDone();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_OK, getActivity().getResultCode());
+        Intent resultData = getActivity().getResultData();
+        Bitmap bitmap = (Bitmap) resultData.getParcelableExtra("data");
+        assertNotNull(bitmap);
+        assertTrue(bitmap.getWidth() > 0);
+        assertTrue(bitmap.getHeight() > 0);
+    }
+
+    @LargeTest
+    public void testExtraOutput() throws Exception {
+        File file = new File(Environment.getExternalStorageDirectory(),
+            "test.jpg");
+        BufferedInputStream stream = null;
+        byte[] jpegData;
+
+        try {
+            mIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
+            setActivityIntent(mIntent);
+            getActivity();
+
+            takePicture();
+            pressDone();
+
+            assertTrue(getActivity().isFinishing());
+            assertEquals(Activity.RESULT_OK, getActivity().getResultCode());
+
+            // Verify the jpeg file
+            int fileLength = (int) file.length();
+            assertTrue(fileLength > 0);
+            jpegData = new byte[fileLength];
+            stream = new BufferedInputStream(new FileInputStream(file));
+            stream.read(jpegData);
+        } finally {
+            if (stream != null) stream.close();
+            file.delete();
+        }
+
+        Bitmap b = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
+        assertTrue(b.getWidth() > 0);
+        assertTrue(b.getHeight() > 0);
+    }
+
+    @LargeTest
+    public void testCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    @LargeTest
+    public void testSnapshotCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        takePicture();
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    private void takePicture() throws Exception {
+        getInstrumentation().sendKeySync(
+                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FOCUS));
+        getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+        Thread.sleep(4000);
+    }
+
+    private void pressDone() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_done).performClick();
+            }
+        });
+    }
+
+    private void pressCancel() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_cancel).performClick();
+            }
+        });
+    }
+}
diff --git a/tests_camera/src/com/android/camera/functional/VideoCaptureIntentTest.java b/tests_camera/src/com/android/camera/functional/VideoCaptureIntentTest.java
new file mode 100644
index 0000000..43e91ca
--- /dev/null
+++ b/tests_camera/src/com/android/camera/functional/VideoCaptureIntentTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2011 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.functional;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.R;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.io.File;
+
+public class VideoCaptureIntentTest extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private static final String TAG = "VideoCaptureIntentTest";
+    private Intent mIntent;
+    private Uri mVideoUri;
+    private File mFile, mFile2;
+
+    public VideoCaptureIntentTest() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mVideoUri != null) {
+            ContentResolver resolver = getActivity().getContentResolver();
+            Uri query = mVideoUri.buildUpon().build();
+            String[] projection = new String[] {VideoColumns.DATA};
+
+            Cursor cursor = null;
+            try {
+                cursor = resolver.query(query, projection, null, null, null);
+                if (cursor != null && cursor.moveToFirst()) {
+                    new File(cursor.getString(0)).delete();
+                }
+            } finally {
+                if (cursor != null) cursor.close();
+            }
+
+            resolver.delete(mVideoUri, null, null);
+        }
+        if (mFile != null) mFile.delete();
+        if (mFile2 != null) mFile2.delete();
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testNoExtraOutput() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        Intent resultData = getActivity().getResultData();
+        mVideoUri = resultData.getData();
+        assertNotNull(mVideoUri);
+        verify(getActivity(), mVideoUri);
+    }
+
+    @LargeTest
+    public void testExtraOutput() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        verify(getActivity(), uri);
+    }
+
+    @LargeTest
+    public void testCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    @LargeTest
+    public void testRecordCancel() throws Exception {
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressCancel();
+
+        assertTrue(getActivity().isFinishing());
+        assertEquals(Activity.RESULT_CANCELED, getActivity().getResultCode());
+    }
+
+    @LargeTest
+    public void testExtraSizeLimit() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+        final long sizeLimit = 500000;  // bytes
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, sizeLimit);
+        mIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);  // use low quality to speed up
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo(5000);
+        pressDone();
+
+        verify(getActivity(), uri);
+        long length = mFile.length();
+        Log.v(TAG, "Video size is " + length + " bytes.");
+        assertTrue(length > 0);
+        assertTrue("Actual size=" + length, length <= sizeLimit);
+    }
+
+    @LargeTest
+    public void testExtraDurationLimit() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+        final int durationLimit = 2;  // seconds
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, durationLimit);
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo(5000);
+        pressDone();
+
+        int duration = verify(getActivity(), uri);
+        // The duraion should be close to to the limit. The last video duration
+        // also has duration, so the total duration may exceeds the limit a
+        // little bit.
+        Log.v(TAG, "Video length is " + duration + " ms.");
+        assertTrue(duration  < (durationLimit + 1) * 1000);
+    }
+
+    @LargeTest
+    public void testExtraVideoQuality() throws Exception {
+        mFile = new File(Environment.getExternalStorageDirectory(), "video.tmp");
+        mFile2 = new File(Environment.getExternalStorageDirectory(), "video2.tmp");
+
+        Uri uri = Uri.fromFile(mFile);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);  // low quality
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        verify(getActivity(), uri);
+        setActivity(null);
+
+        uri = Uri.fromFile(mFile2);
+        mIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+        mIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);  // high quality
+        setActivityIntent(mIntent);
+        getActivity();
+
+        recordVideo();
+        pressDone();
+
+        verify(getActivity(), uri);
+        assertTrue(mFile.length() <= mFile2.length());
+    }
+
+    // Verify result code, result data, and the duration.
+    private int verify(CameraActivity activity, Uri uri) throws Exception {
+        assertTrue(activity.isFinishing());
+        assertEquals(Activity.RESULT_OK, activity.getResultCode());
+
+        // Verify the video file
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        retriever.setDataSource(activity, uri);
+        String duration = retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_DURATION);
+        assertNotNull(duration);
+        int durationValue = Integer.parseInt(duration);
+        Log.v(TAG, "Video duration is " + durationValue);
+        assertTrue(durationValue > 0);
+        return durationValue;
+    }
+
+    private void recordVideo(int ms) throws Exception {
+        getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+        Thread.sleep(ms);
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                // If recording is in progress, stop it. Run these atomically in
+                // UI thread.
+                CameraActivity activity = getActivity();
+                if (activity.isRecording()) {
+                    activity.findViewById(R.id.shutter_button).performClick();
+                }
+            }
+        });
+    }
+
+    private void recordVideo() throws Exception {
+        recordVideo(2000);
+    }
+
+    private void pressDone() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_done).performClick();
+            }
+        });
+    }
+
+    private void pressCancel() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().findViewById(R.id.btn_cancel).performClick();
+            }
+        });
+    }
+}
diff --git a/tests_camera/src/com/android/camera/power/ImageAndVideoCapture.java b/tests_camera/src/com/android/camera/power/ImageAndVideoCapture.java
new file mode 100755
index 0000000..b89b764
--- /dev/null
+++ b/tests_camera/src/com/android/camera/power/ImageAndVideoCapture.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.power;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Instrumentation;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.content.Intent;
+/**
+ * Junit / Instrumentation test case for camera power measurement
+ *
+ * Running the test suite:
+ *
+ * adb shell am instrument \
+ *    -e com.android.camera.power.ImageAndVideoCapture \
+ *    -w com.android.camera.tests/android.test.InstrumentationTestRunner
+ *
+ */
+
+public class ImageAndVideoCapture extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private String TAG = "ImageAndVideoCapture";
+    private static final int TOTAL_NUMBER_OF_IMAGECAPTURE = 5;
+    private static final int TOTAL_NUMBER_OF_VIDEOCAPTURE = 5;
+    private static final long WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN = 1500;   //1.5 sedconds
+    private static final long WAIT_FOR_VIDEO_CAPTURE_TO_BE_TAKEN = 10000; //10 seconds
+    private static final long WAIT_FOR_PREVIEW = 1500; //1.5 seconds
+    private static final long WAIT_FOR_STABLE_STATE = 2000; //2 seconds
+
+    public ImageAndVideoCapture() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        getActivity();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testLaunchCamera() {
+        // This test case capture the baseline for the image preview.
+        try {
+            Thread.sleep(WAIT_FOR_STABLE_STATE);
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+            assertTrue("testImageCaptureDoNothing", false);
+        }
+    }
+
+    @LargeTest
+    public void testCapture5Image() {
+        // This test case will use the default camera setting
+        Instrumentation inst = getInstrumentation();
+        try {
+            for (int i = 0; i < TOTAL_NUMBER_OF_IMAGECAPTURE; i++) {
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP);
+                inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+            }
+            Thread.sleep(WAIT_FOR_STABLE_STATE);
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+            assertTrue("testImageCapture", false);
+        }
+    }
+
+    @LargeTest
+    public void testCapture5Videos() {
+        // This test case will use the default camera setting
+        Instrumentation inst = getInstrumentation();
+        try {
+            // Switch to the video mode
+            Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+            intent.setClass(getInstrumentation().getTargetContext(),
+                    CameraActivity.class);
+            getActivity().startActivity(intent);
+            for (int i = 0; i < TOTAL_NUMBER_OF_VIDEOCAPTURE; i++) {
+                Thread.sleep(WAIT_FOR_PREVIEW);
+                // record a video
+                inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+                Thread.sleep(WAIT_FOR_VIDEO_CAPTURE_TO_BE_TAKEN);
+                inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+                Thread.sleep(WAIT_FOR_PREVIEW);
+            }
+            Thread.sleep(WAIT_FOR_STABLE_STATE);
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+            assertTrue("testVideoCapture", false);
+        }
+    }
+}
diff --git a/tests_camera/src/com/android/camera/stress/CameraLatency.java b/tests_camera/src/com/android/camera/stress/CameraLatency.java
new file mode 100755
index 0000000..35ff717
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/CameraLatency.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.stress;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Instrumentation;
+import android.os.Environment;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ */
+
+public class CameraLatency extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private String TAG = "CameraLatency";
+    private static final int TOTAL_NUMBER_OF_IMAGECAPTURE = 20;
+    private static final long WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN = 4000;
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+
+    private long mTotalAutoFocusTime;
+    private long mTotalShutterLag;
+    private long mTotalShutterToPictureDisplayedTime;
+    private long mTotalPictureDisplayedToJpegCallbackTime;
+    private long mTotalJpegCallbackFinishTime;
+    private long mAvgAutoFocusTime;
+    private long mAvgShutterLag = mTotalShutterLag;
+    private long mAvgShutterToPictureDisplayedTime;
+    private long mAvgPictureDisplayedToJpegCallbackTime;
+    private long mAvgJpegCallbackFinishTime;
+
+    public CameraLatency() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        getActivity();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testImageCapture() {
+        Log.v(TAG, "start testImageCapture test");
+        Instrumentation inst = getInstrumentation();
+        inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+        try {
+            for (int i = 0; i < TOTAL_NUMBER_OF_IMAGECAPTURE; i++) {
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                //skip the first measurement
+                if (i != 0) {
+                    CameraActivity c = getActivity();
+
+                    // if any of the latency var accessor methods return -1 then the
+                    // camera is set to a different module other than PhotoModule so
+                    // skip the shot and try again
+                    if (c.getAutoFocusTime() != -1) {
+                        mTotalAutoFocusTime += c.getAutoFocusTime();
+                        mTotalShutterLag += c.getShutterLag();
+                        mTotalShutterToPictureDisplayedTime +=
+                                c.getShutterToPictureDisplayedTime();
+                        mTotalPictureDisplayedToJpegCallbackTime +=
+                                c.getPictureDisplayedToJpegCallbackTime();
+                        mTotalJpegCallbackFinishTime += c.getJpegCallbackFinishTime();
+                    }
+                    else {
+                        i--;
+                        continue;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+        }
+        //ToDO: yslau
+        //1) Need to get the baseline from the cupcake so that we can add the
+        //failure condition of the camera latency.
+        //2) Only count those number with succesful capture. Set the timer to invalid
+        //before capture and ignore them if the value is invalid
+        int numberofRun = TOTAL_NUMBER_OF_IMAGECAPTURE - 1;
+        mAvgAutoFocusTime = mTotalAutoFocusTime / numberofRun;
+        mAvgShutterLag = mTotalShutterLag / numberofRun;
+        mAvgShutterToPictureDisplayedTime =
+                mTotalShutterToPictureDisplayedTime / numberofRun;
+        mAvgPictureDisplayedToJpegCallbackTime =
+                mTotalPictureDisplayedToJpegCallbackTime / numberofRun;
+        mAvgJpegCallbackFinishTime =
+                mTotalJpegCallbackFinishTime / numberofRun;
+
+        try {
+            FileWriter fstream = null;
+            fstream = new FileWriter(CAMERA_TEST_OUTPUT_FILE, true);
+            BufferedWriter out = new BufferedWriter(fstream);
+            out.write("Camera Latency : \n");
+            out.write("Number of loop: " + TOTAL_NUMBER_OF_IMAGECAPTURE + "\n");
+            out.write("Avg AutoFocus = " + mAvgAutoFocusTime + "\n");
+            out.write("Avg mShutterLag = " + mAvgShutterLag + "\n");
+            out.write("Avg mShutterToPictureDisplayedTime = "
+                    + mAvgShutterToPictureDisplayedTime + "\n");
+            out.write("Avg mPictureDisplayedToJpegCallbackTime = "
+                    + mAvgPictureDisplayedToJpegCallbackTime + "\n");
+            out.write("Avg mJpegCallbackFinishTime = " +
+                    mAvgJpegCallbackFinishTime + "\n");
+            out.close();
+            fstream.close();
+        } catch (Exception e) {
+            fail("Camera Latency write output to file");
+        }
+        Log.v(TAG, "The Image capture wait time = " +
+            WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+        Log.v(TAG, "Avg AutoFocus = " + mAvgAutoFocusTime);
+        Log.v(TAG, "Avg mShutterLag = " + mAvgShutterLag);
+        Log.v(TAG, "Avg mShutterToPictureDisplayedTime = "
+                + mAvgShutterToPictureDisplayedTime);
+        Log.v(TAG, "Avg mPictureDisplayedToJpegCallbackTime = "
+                + mAvgPictureDisplayedToJpegCallbackTime);
+        Log.v(TAG, "Avg mJpegCallbackFinishTime = " + mAvgJpegCallbackFinishTime);
+    }
+}
+
diff --git a/tests_camera/src/com/android/camera/stress/CameraStartUp.java b/tests_camera/src/com/android/camera/stress/CameraStartUp.java
new file mode 100644
index 0000000..94e9a94
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/CameraStartUp.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.stress;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import java.io.FileWriter;
+import java.io.BufferedWriter;
+
+/**
+ * Test cases to measure the camera and video recorder startup time.
+ */
+public class CameraStartUp extends InstrumentationTestCase {
+
+    private static final int TOTAL_NUMBER_OF_STARTUP = 20;
+
+    private String TAG = "CameraStartUp";
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+    private static int WAIT_TIME_FOR_PREVIEW = 1500; //1.5 second
+
+    private long launchCamera() {
+        long startupTime = 0;
+        try {
+            Intent intent = new Intent(Intent.ACTION_MAIN);
+            intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            long beforeStart = System.currentTimeMillis();
+            Instrumentation inst = getInstrumentation();
+            Activity cameraActivity = inst.startActivitySync(intent);
+            long cameraStarted = System.currentTimeMillis();
+            Thread.sleep(WAIT_TIME_FOR_PREVIEW);
+            cameraActivity.finish();
+            startupTime = cameraStarted - beforeStart;
+            Thread.sleep(1000);
+            Log.v(TAG, "camera startup time: " + startupTime);
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+            fail("Fails to get the output file");
+        }
+        return startupTime;
+    }
+
+    private long launchVideo() {
+        long startupTime = 0;
+
+        try {
+            Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+            intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            long beforeStart = System.currentTimeMillis();
+            Instrumentation inst = getInstrumentation();
+            Activity recorderActivity = inst.startActivitySync(intent);
+            long cameraStarted = System.currentTimeMillis();
+            recorderActivity.finish();
+            startupTime = cameraStarted - beforeStart;
+            Log.v(TAG, "Video Startup Time = " + startupTime);
+            // wait for 1s to make sure it reach a clean stage
+            Thread.sleep(WAIT_TIME_FOR_PREVIEW);
+            Log.v(TAG, "video startup time: " + startupTime);
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception", e);
+            fail("Fails to launch video output file");
+        }
+        return startupTime;
+    }
+
+    private void writeToOutputFile(long totalStartupTime,
+            String individualStartupTime, boolean firstStartUp, String Type) throws Exception {
+        // TODO (yslau) : Need to integrate the output data with central
+        // dashboard
+        try {
+            FileWriter fstream = null;
+            fstream = new FileWriter(CAMERA_TEST_OUTPUT_FILE, true);
+            BufferedWriter out = new BufferedWriter(fstream);
+            if (firstStartUp) {
+                out.write("First " + Type + " Startup: " + totalStartupTime + "\n");
+            } else {
+                long averageStartupTime = totalStartupTime / (TOTAL_NUMBER_OF_STARTUP -1);
+                out.write(Type + "startup time: " + "\n");
+                out.write("Number of loop: " + (TOTAL_NUMBER_OF_STARTUP -1)  + "\n");
+                out.write(individualStartupTime + "\n\n");
+                out.write(Type + " average startup time: " + averageStartupTime + " ms\n\n");
+            }
+            out.close();
+            fstream.close();
+        } catch (Exception e) {
+            fail("Camera write output to file");
+        }
+    }
+
+    @LargeTest
+    public void testLaunchVideo() throws Exception {
+        String individualStartupTime;
+        individualStartupTime = "Individual Video Startup Time = ";
+        long totalStartupTime = 0;
+        long startupTime = 0;
+        for (int i = 0; i < TOTAL_NUMBER_OF_STARTUP; i++) {
+            if (i == 0) {
+                // Capture the first startup time individually
+                long firstStartUpTime = launchVideo();
+                writeToOutputFile(firstStartUpTime, "na", true, "Video");
+            } else {
+                startupTime = launchVideo();
+                totalStartupTime += startupTime;
+                individualStartupTime += startupTime + " ,";
+            }
+        }
+        Log.v(TAG, "totalStartupTime =" + totalStartupTime);
+        writeToOutputFile(totalStartupTime, individualStartupTime, false, "Video");
+    }
+
+    @LargeTest
+    public void testLaunchCamera() throws Exception {
+        String individualStartupTime;
+        individualStartupTime = "Individual Camera Startup Time = ";
+        long totalStartupTime = 0;
+        long startupTime = 0;
+        for (int i = 0; i < TOTAL_NUMBER_OF_STARTUP; i++) {
+            if (i == 0) {
+                // Capture the first startup time individually
+                long firstStartUpTime = launchCamera();
+                writeToOutputFile(firstStartUpTime, "na", true, "Camera");
+            } else {
+                startupTime = launchCamera();
+                totalStartupTime += startupTime;
+                individualStartupTime += startupTime + " ,";
+            }
+        }
+        Log.v(TAG, "totalStartupTime =" + totalStartupTime);
+        writeToOutputFile(totalStartupTime,
+                individualStartupTime, false, "Camera");
+    }
+}
diff --git a/tests_camera/src/com/android/camera/stress/CameraStressTestRunner.java b/tests_camera/src/com/android/camera/stress/CameraStressTestRunner.java
new file mode 100755
index 0000000..4047da0
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/CameraStressTestRunner.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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.stress;
+
+import android.os.Bundle;
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+import junit.framework.TestSuite;
+
+public class CameraStressTestRunner extends InstrumentationTestRunner {
+
+    // Default recorder settings
+    public static int mVideoDuration = 20000; // set default to 20 seconds
+    public static int mVideoIterations = 100; // set default to 100 videos
+    public static int mImageIterations = 100; // set default to 100 images
+
+    @Override
+    public TestSuite getAllTests() {
+        TestSuite suite = new InstrumentationTestSuite(this);
+        suite.addTestSuite(ImageCapture.class);
+        suite.addTestSuite(VideoCapture.class);
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return CameraStressTestRunner.class.getClassLoader();
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        String video_iterations = (String) icicle.get("video_iterations");
+        String image_iterations = (String) icicle.get("image_iterations");
+        String video_duration = (String) icicle.get("video_duration");
+
+        if ( video_iterations != null ) {
+            mVideoIterations = Integer.parseInt(video_iterations);
+        }
+        if ( image_iterations != null) {
+            mImageIterations = Integer.parseInt(image_iterations);
+        }
+        if ( video_duration != null) {
+            mVideoDuration = Integer.parseInt(video_duration);
+        }
+    }
+}
diff --git a/tests_camera/src/com/android/camera/stress/ImageCapture.java b/tests_camera/src/com/android/camera/stress/ImageCapture.java
new file mode 100755
index 0000000..ad06db1
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/ImageCapture.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.stress;
+
+import com.android.camera.CameraActivity;
+import com.android.camera.stress.CameraStressTestRunner;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.app.Activity;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ * Running the test suite:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.camera.stress.ImageCapture \
+ *    -w com.google.android.camera.tests/android.test.InstrumentationTestRunner
+ *
+ */
+
+public class ImageCapture extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private String TAG = "ImageCapture";
+    private static final long WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN = 1500;   //1.5 sedconds
+    private static final long WAIT_FOR_SWITCH_CAMERA = 3000; //3 seconds
+
+    private TestUtil testUtil = new TestUtil();
+
+    // Private intent extras.
+    private final static String EXTRAS_CAMERA_FACING =
+        "android.intent.extras.CAMERA_FACING";
+
+    public ImageCapture() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        testUtil.prepareOutputFile();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        testUtil.closeOutputFile();
+        super.tearDown();
+    }
+
+    public void captureImages(String reportTag, Instrumentation inst) {
+        int total_num_of_images = CameraStressTestRunner.mImageIterations;
+        Log.v(TAG, "no of images = " + total_num_of_images);
+
+        //TODO(yslau): Need to integrate the outoput with the central dashboard,
+        //write to a txt file as a temp solution
+        boolean memoryResult = false;
+        KeyEvent focusEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FOCUS);
+
+        try {
+            testUtil.writeReportHeader(reportTag, total_num_of_images);
+            for (int i = 0; i < total_num_of_images; i++) {
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                inst.sendKeySync(focusEvent);
+                inst.sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+                Thread.sleep(WAIT_FOR_IMAGE_CAPTURE_TO_BE_TAKEN);
+                testUtil.writeResult(i);
+            }
+        } catch (Exception e) {
+            Log.v(TAG, "Got exception: " + e.toString());
+            assertTrue("testImageCapture", false);
+        }
+    }
+
+    @LargeTest
+    public void testBackImageCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent();
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureImages("Back Camera Image Capture\n", inst);
+        act.finish();
+    }
+
+    @LargeTest
+    public void testFrontImageCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent();
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureImages("Front Camera Image Capture\n", inst);
+        act.finish();
+    }
+}
diff --git a/tests_camera/src/com/android/camera/stress/ShotToShotLatency.java b/tests_camera/src/com/android/camera/stress/ShotToShotLatency.java
new file mode 100644
index 0000000..0c1ef45
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/ShotToShotLatency.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2012 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.stress;
+
+import android.app.Instrumentation;
+import android.os.Environment;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+import com.android.camera.CameraActivity;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Junit / Instrumentation test case for measuring camera shot to shot latency
+ */
+public class ShotToShotLatency extends ActivityInstrumentationTestCase2<CameraActivity> {
+    private String TAG = "ShotToShotLatency";
+    private static final int TOTAL_NUMBER_OF_SNAPSHOTS = 250;
+    private static final long SNAPSHOT_WAIT = 1000;
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+    private static final String CAMERA_IMAGE_DIRECTORY =
+            Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera/";
+
+    public ShotToShotLatency() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        getActivity();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private void cleanupLatencyImages() {
+        try {
+            File sdcard = new File(CAMERA_IMAGE_DIRECTORY);
+            File[] pics = null;
+            FilenameFilter filter = new FilenameFilter() {
+                public boolean accept(File dir, String name) {
+                    return name.endsWith(".jpg");
+                }
+            };
+            pics = sdcard.listFiles(filter);
+            for (File f : pics) {
+                f.delete();
+            }
+        } catch (SecurityException e) {
+            Log.e(TAG, "Security manager access violation: " + e.toString());
+        }
+    }
+
+    private void sleep(long time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Sleep InterruptedException " + e.toString());
+        }
+    }
+
+    @LargeTest
+    public void testShotToShotLatency() {
+        long sigmaOfDiffFromMeanSquared = 0;
+        double mean = 0;
+        double standardDeviation = 0;
+        ArrayList<Long> captureTimes = new ArrayList<Long>();
+        ArrayList<Long> latencyTimes = new ArrayList<Long>();
+
+        Log.v(TAG, "start testShotToShotLatency test");
+        Instrumentation inst = getInstrumentation();
+
+        // Generate data points
+        for (int i = 0; i < TOTAL_NUMBER_OF_SNAPSHOTS; i++) {
+            inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+            sleep(SNAPSHOT_WAIT);
+            CameraActivity c = getActivity();
+            if (c.getCaptureStartTime() > 0) {
+                captureTimes.add(c.getCaptureStartTime());
+            }
+        }
+
+        // Calculate latencies
+        for (int j = 1; j < captureTimes.size(); j++) {
+            latencyTimes.add(captureTimes.get(j) - captureTimes.get(j - 1));
+        }
+
+        // Crunch numbers
+        for (long dataPoint : latencyTimes) {
+            mean += (double) dataPoint;
+        }
+        mean /= latencyTimes.size();
+
+        for (long dataPoint : latencyTimes) {
+            sigmaOfDiffFromMeanSquared += (dataPoint - mean) * (dataPoint - mean);
+        }
+        standardDeviation = Math.sqrt(sigmaOfDiffFromMeanSquared / latencyTimes.size());
+
+        // Report statistics
+        File outFile = new File(CAMERA_TEST_OUTPUT_FILE);
+        BufferedWriter output = null;
+        try {
+            output = new BufferedWriter(new FileWriter(outFile, true));
+            output.write("Shot to shot latency - mean: " + mean + "\n");
+            output.write("Shot to shot latency - standard deviation: " + standardDeviation + "\n");
+            cleanupLatencyImages();
+        } catch (IOException e) {
+            Log.e(TAG, "testShotToShotLatency IOException writing to log " + e.toString());
+        } finally {
+            try {
+                if (output != null) {
+                    output.close();
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Error closing file: " + e.toString());
+            }
+        }
+    }
+}
diff --git a/tests_camera/src/com/android/camera/stress/SwitchPreview.java b/tests_camera/src/com/android/camera/stress/SwitchPreview.java
new file mode 100755
index 0000000..86b1b5d
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/SwitchPreview.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.stress;
+
+import com.android.camera.CameraActivity;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ * Running the test suite:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.camera.stress.SwitchPreview \
+ *    -w com.android.camera.tests/com.android.camera.stress.CameraStressTestRunner
+ *
+ */
+public class SwitchPreview extends ActivityInstrumentationTestCase2 <CameraActivity>{
+    private String TAG = "SwitchPreview";
+    private static final int TOTAL_NUMBER_OF_SWITCHING = 200;
+    private static final long WAIT_FOR_PREVIEW = 4000;
+
+    private static final String CAMERA_TEST_OUTPUT_FILE =
+            Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+    private BufferedWriter mOut;
+    private FileWriter mfstream;
+
+    public SwitchPreview() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        getActivity();
+        prepareOutputFile();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        getActivity().finish();
+        closeOutputFile();
+        super.tearDown();
+    }
+
+    private void prepareOutputFile(){
+        try{
+            mfstream = new FileWriter(CAMERA_TEST_OUTPUT_FILE, true);
+            mOut = new BufferedWriter(mfstream);
+        } catch (Exception e){
+            assertTrue("Camera Switch Mode", false);
+        }
+    }
+
+    private void closeOutputFile() {
+        try {
+            mOut.write("\n");
+            mOut.close();
+            mfstream.close();
+        } catch (Exception e) {
+            assertTrue("CameraSwitchMode close output", false);
+        }
+    }
+
+    @LargeTest
+    public void testSwitchMode() {
+        //Switching the video and the video recorder mode
+        Instrumentation inst = getInstrumentation();
+        try{
+            mOut.write("Camera Switch Mode:\n");
+            mOut.write("No of loops :" + TOTAL_NUMBER_OF_SWITCHING + "\n");
+            mOut.write("loop: ");
+            for (int i=0; i< TOTAL_NUMBER_OF_SWITCHING; i++) {
+                Thread.sleep(WAIT_FOR_PREVIEW);
+                Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                intent.setClass(getInstrumentation().getTargetContext(),
+                        CameraActivity.class);
+                getActivity().startActivity(intent);
+                Thread.sleep(WAIT_FOR_PREVIEW);
+                intent = new Intent();
+                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                intent.setClass(getInstrumentation().getTargetContext(),
+                        CameraActivity.class);
+                getActivity().startActivity(intent);
+                mOut.write(" ," + i);
+                mOut.flush();
+            }
+        } catch (Exception e){
+            Log.v(TAG, "Got exception", e);
+        }
+    }
+}
diff --git a/tests_camera/src/com/android/camera/stress/TestUtil.java b/tests_camera/src/com/android/camera/stress/TestUtil.java
new file mode 100644
index 0000000..64e2039
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/TestUtil.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011 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.stress;
+
+import android.os.Environment;
+import java.io.FileWriter;
+import java.io.BufferedWriter;
+
+
+/**
+ * Collection of utility functions used for the test.
+ */
+public class TestUtil {
+    public BufferedWriter mOut;
+    public FileWriter mfstream;
+
+    public TestUtil() {
+    }
+
+    public void prepareOutputFile() throws Exception {
+        String camera_test_output_file =
+                Environment.getExternalStorageDirectory().toString() + "/mediaStressOut.txt";
+        mfstream = new FileWriter(camera_test_output_file, true);
+        mOut = new BufferedWriter(mfstream);
+    }
+
+    public void closeOutputFile() throws Exception {
+        mOut.write("\n");
+        mOut.close();
+        mfstream.close();
+    }
+
+    public void writeReportHeader(String reportTag, int iteration) throws Exception {
+        mOut.write(reportTag);
+        mOut.write("No of loops :" + iteration + "\n");
+        mOut.write("loop: ");
+    }
+
+    public void writeResult(int iteration) throws Exception {
+        mOut.write(" ," + iteration);
+        mOut.flush();
+    }
+}
diff --git a/tests_camera/src/com/android/camera/stress/VideoCapture.java b/tests_camera/src/com/android/camera/stress/VideoCapture.java
new file mode 100755
index 0000000..ec55ccc
--- /dev/null
+++ b/tests_camera/src/com/android/camera/stress/VideoCapture.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2010 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.stress;
+
+import com.android.camera.CameraActivity;
+import com.android.camera.stress.TestUtil;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.provider.MediaStore;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+
+import com.android.camera.stress.CameraStressTestRunner;
+
+/**
+ * Junit / Instrumentation test case for camera test
+ *
+ * Running the test suite:
+ *
+ * adb shell am instrument \
+ *    -e class com.android.camera.stress.VideoCapture \
+ *    -w com.google.android.camera.tests/android.test.InstrumentationTestRunner
+ *
+ */
+
+public class VideoCapture extends ActivityInstrumentationTestCase2 <CameraActivity> {
+    private static final long WAIT_FOR_PREVIEW = 1500; //1.5 seconds
+    private static final long WAIT_FOR_SWITCH_CAMERA = 3000; //2 seconds
+
+    // Private intent extras which control the camera facing.
+    private final static String EXTRAS_CAMERA_FACING =
+        "android.intent.extras.CAMERA_FACING";
+
+    private TestUtil testUtil = new TestUtil();
+
+    public VideoCapture() {
+        super(CameraActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        testUtil.prepareOutputFile();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        testUtil.closeOutputFile();
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void captureVideos(String reportTag, Instrumentation inst) throws Exception{
+        boolean memoryResult = false;
+        int total_num_of_videos = CameraStressTestRunner.mVideoIterations;
+        int video_duration = CameraStressTestRunner.mVideoDuration;
+        testUtil.writeReportHeader(reportTag, total_num_of_videos);
+
+        for (int i = 0; i < total_num_of_videos; i++) {
+            Thread.sleep(WAIT_FOR_PREVIEW);
+            // record a video
+            inst.sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+            Thread.sleep(video_duration);
+            inst.sendCharacterSync(KeyEvent.KEYCODE_CAMERA);
+            testUtil.writeResult(i);
+        }
+    }
+
+    @LargeTest
+    public void testBackVideoCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureVideos("Back Camera Video Capture\n", inst);
+        act.finish();
+    }
+
+    @LargeTest
+    public void testFrontVideoCapture() throws Exception {
+        Instrumentation inst = getInstrumentation();
+        Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
+
+        intent.setClass(getInstrumentation().getTargetContext(), CameraActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(EXTRAS_CAMERA_FACING,
+                android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
+        Activity act = inst.startActivitySync(intent);
+        Thread.sleep(WAIT_FOR_SWITCH_CAMERA);
+        captureVideos("Front Camera Video Capture\n", inst);
+        act.finish();
+    }
+}
diff --git a/tests_camera/src/com/android/camera/unittest/CameraUnitTest.java b/tests_camera/src/com/android/camera/unittest/CameraUnitTest.java
new file mode 100644
index 0000000..0b4fc80
--- /dev/null
+++ b/tests_camera/src/com/android/camera/unittest/CameraUnitTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 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.unittest;
+
+import com.android.camera.Util;
+
+import android.graphics.Matrix;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class CameraUnitTest extends TestCase {
+    public void testRoundOrientation() {
+        int h = Util.ORIENTATION_HYSTERESIS;
+        assertEquals(0, Util.roundOrientation(0, 0));
+        assertEquals(0, Util.roundOrientation(359, 0));
+        assertEquals(0, Util.roundOrientation(0 + 44 + h, 0));
+        assertEquals(90, Util.roundOrientation(0 + 45 + h, 0));
+        assertEquals(0, Util.roundOrientation(360 - 44 - h, 0));
+        assertEquals(270, Util.roundOrientation(360 - 45 - h, 0));
+
+        assertEquals(90, Util.roundOrientation(90, 90));
+        assertEquals(90, Util.roundOrientation(90 + 44 + h, 90));
+        assertEquals(180, Util.roundOrientation(90 + 45 + h, 90));
+        assertEquals(90, Util.roundOrientation(90 - 44 - h, 90));
+        assertEquals(0, Util.roundOrientation(90 - 45 - h, 90));
+
+        assertEquals(180, Util.roundOrientation(180, 180));
+        assertEquals(180, Util.roundOrientation(180 + 44 + h, 180));
+        assertEquals(270, Util.roundOrientation(180 + 45 + h, 180));
+        assertEquals(180, Util.roundOrientation(180 - 44 - h, 180));
+        assertEquals(90, Util.roundOrientation(180 - 45 - h, 180));
+
+        assertEquals(270, Util.roundOrientation(270, 270));
+        assertEquals(270, Util.roundOrientation(270 + 44 + h, 270));
+        assertEquals(0, Util.roundOrientation(270 + 45 + h, 270));
+        assertEquals(270, Util.roundOrientation(270 - 44 - h, 270));
+        assertEquals(180, Util.roundOrientation(270 - 45 - h, 270));
+
+        assertEquals(90, Util.roundOrientation(90, 0));
+        assertEquals(180, Util.roundOrientation(180, 0));
+        assertEquals(270, Util.roundOrientation(270, 0));
+
+        assertEquals(0, Util.roundOrientation(0, 90));
+        assertEquals(180, Util.roundOrientation(180, 90));
+        assertEquals(270, Util.roundOrientation(270, 90));
+
+        assertEquals(0, Util.roundOrientation(0, 180));
+        assertEquals(90, Util.roundOrientation(90, 180));
+        assertEquals(270, Util.roundOrientation(270, 180));
+
+        assertEquals(0, Util.roundOrientation(0, 270));
+        assertEquals(90, Util.roundOrientation(90, 270));
+        assertEquals(180, Util.roundOrientation(180, 270));
+    }
+
+    public void testPrepareMatrix() {
+        Matrix matrix = new Matrix();
+        float[] points;
+        int[] expected;
+
+        Util.prepareMatrix(matrix, false, 0, 800, 480);
+        points = new float[] {-1000, -1000, 0, 0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {0, 0, 400, 240, 800, 480, 400, 480, 100, 300};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+
+        Util.prepareMatrix(matrix, false, 90, 800, 480);
+        points = new float[] {-1000, -1000,   0,   0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {800, 0, 400, 240, 0, 480, 0, 240, 300, 60};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+
+        Util.prepareMatrix(matrix, false, 180, 800, 480);
+        points = new float[] {-1000, -1000, 0, 0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {800, 480, 400, 240, 0, 0, 400, 0, 700, 180};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+
+        Util.prepareMatrix(matrix, true, 180, 800, 480);
+        points = new float[] {-1000, -1000, 0, 0, 1000, 1000, 0, 1000, -750, 250};
+        expected = new int[] {0, 480, 400, 240, 800, 0, 400, 0, 100, 180};
+        matrix.mapPoints(points);
+        assertEquals(expected, points);
+    }
+
+    private void assertEquals(int expected[], float[] actual) {
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("Array index " + i + " mismatch", expected[i], Math.round(actual[i]));
+        }
+    }
+}