Merge "Add possibility for UX improvement measurements" into gb-ub-photos-bryce
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 508aefe..abe76d2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 
-<manifest android:versionCode="40002"
-        android:versionName="1.1.40002"
+<manifest android:versionCode="40003"
+        android:versionName="1.1.40003"
         xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.gallery3d">
 
     <original-package android:name="com.android.gallery3d" />
 
-    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="16" />
+    <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" />
 
     <permission android:name="com.android.gallery3d.permission.GALLERY_PROVIDER"
             android:protectionLevel="signatureOrSystem" />
@@ -88,8 +88,11 @@
              </intent-filter>
         </activity>
 
-        <activity android:name="com.android.gallery3d.app.Gallery" android:label="@string/app_name"
-                android:configChanges="keyboardHidden|orientation|screenSize">
+        <activity android:name="com.android.photos.GalleryActivity"
+                android:label="@string/app_name"
+                android:configChanges="keyboardHidden|orientation|screenSize"
+                android:theme="@style/Theme.Photos.Gallery"
+                android:uiOptions="splitActionBarWhenNarrow">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -164,16 +167,12 @@
             </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"
+        <activity android:name="com.android.photos.FullscreenViewer"
+                android:label="@string/app_name"
                 android:configChanges="keyboardHidden|orientation|screenSize"
-                android:label="@string/app_name">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-            </intent-filter>
-        </activity-alias>
+                android:theme="@style/Theme.Photos.Fullscreen"
+                android:parentActivityName="com.android.photos.GalleryActivity">
+        </activity>
 
          <!-- This activity receives USB_DEVICE_ATTACHED intents and allows importing
          media from attached MTP devices, like cameras and camera phones -->
@@ -262,6 +261,11 @@
                 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"
@@ -408,10 +412,10 @@
                 android:theme="@style/Theme.ProxyLauncher">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-                <category android:name="com.google.android.canvas.category.BROWSE_LAUNCHER" />
+                <category android:name="com.google.android.pano.category.BROWSE_LAUNCHER" />
             </intent-filter>
             <meta-data
-                android:name="com.google.android.canvas.data.launcher_info"
+                android:name="com.google.android.canvas.pano.launcher_info"
                 android:resource="@xml/canvas_info" />
         </activity>
     </application>
diff --git a/CleanSpec.mk b/CleanSpec.mk
index eff98bb..3a53b9c 100644
--- a/CleanSpec.mk
+++ b/CleanSpec.mk
@@ -48,6 +48,7 @@
 $(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*)
 
 # ************************************************
 # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
index 56adcb1..4200ec7 100644
--- a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
+++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
@@ -22,6 +22,7 @@
 import android.os.Build;
 import android.provider.MediaStore.MediaColumns;
 import android.view.View;
+import android.view.WindowManager;
 
 import java.lang.reflect.Field;
 
@@ -179,6 +180,9 @@
     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 int getIntFieldIfExists(Class<?> klass, String fieldName,
             Class<?> obj, int defaultVal) {
         try {
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/layout-land/camera_controls.xml b/res/layout-land/camera_controls.xml
new file mode 100644
index 0000000..f0f3ecc
--- /dev/null
+++ b/res/layout-land/camera_controls.xml
@@ -0,0 +1,61 @@
+<?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.RotatableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/camera_controls"
+    style="@style/CameraControls"
+    android:layout_gravity="center" >
+
+        <View
+            android:id="@+id/blocker"
+            android:layout_height="match_parent"
+            android:layout_width="@dimen/switcher_size"
+            android:clickable="true"
+            android:layout_gravity="right" />
+
+        <include layout="@layout/menu_indicators"
+            android:layout_width="80dip"
+            android:layout_height="80dip"
+            android:layout_marginTop="-5dip"
+            android:layout_marginRight="-2dip"
+            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" />
+</com.android.camera.ui.RotatableLayout>
diff --git a/res/layout-land/camera_shutter_switcher.xml b/res/layout-land/camera_shutter_switcher.xml
deleted file mode 100644
index 9c06749..0000000
--- a/res/layout-land/camera_shutter_switcher.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?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_shutter_switcher"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent" >
-
-    <View
-        android:id="@+id/controls"
-        style="@style/CameraControls"
-        android:layout_alignParentRight="true"
-        android:layout_centerVertical="true" />
-
-    <com.android.camera.ShutterButton
-        android:id="@+id/shutter_button"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentRight="true"
-        android:layout_centerVertical="true"
-        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" />
-
-    <com.android.camera.ui.CameraSwitcher
-        android:id="@+id/camera_switcher"
-        style="@style/SwitcherButton"
-        android:layout_alignBottom="@id/controls"
-        android:layout_alignParentRight="true"
-        android:layout_marginRight="2dip"
-        android:contentDescription="@string/accessibility_mode_picker" />
-
-</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout-land/filtershow_activity.xml b/res/layout-land/filtershow_activity.xml
new file mode 100644
index 0000000..8cef0ce
--- /dev/null
+++ b/res/layout-land/filtershow_activity.xml
@@ -0,0 +1,336 @@
+<?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:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:id="@+id/mainView">
+
+    <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" />
+
+        <com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet
+                android:id="@+id/imageTinyPlanet"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+        </LinearLayout>
+
+        <LinearLayout
+                android:id="@+id/mainPanel"
+                android:layout_width="650dip"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:orientation="vertical" >
+
+            <LinearLayout
+                    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>
+
+            <LinearLayout
+                    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" >
+                </ListView>
+
+                <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>
+
+
+            <FrameLayout
+                    android:layout_gravity="bottom"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1" >
+
+
+                <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
+                    xmlns:custom="http://schemas.android.com/apk/res/com.android.gallery3d"
+                    android:id="@+id/filtersPanel"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:background="@color/background_main_toolbar"
+                    custom:max_width="600dip"
+                    android:orientation="vertical">
+
+                <FrameLayout
+                        android:id="@+id/secondRowPanel"
+                        android:layout_width="fill_parent"
+                        android:layout_height="wrap_content" >
+
+                    <LinearLayout
+                            android:id="@+id/filterButtonsList"
+                            android:layout_width="fill_parent"
+                            android:layout_height="@dimen/thumbnail_size"
+                            android:background="@color/background_main_toolbar"
+                            android:orientation="horizontal"
+                            android:visibility="gone" >
+
+                        <FrameLayout
+                                android:layout_width="fill_parent"
+                                android:layout_height="fill_parent" >
+
+                            <LinearLayout
+                                    android:id="@+id/panelAccessoryViewList"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="fill_parent"
+                                    android:orientation="horizontal"
+                                    android:visibility="visible" />
+
+                            <Button
+                                    android:id="@+id/applyEffect"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="94dip"
+                                    android:layout_gravity="center"
+                                    android:layout_weight="1"
+                                    android:background="@android:color/transparent"
+                                    android:gravity="center"
+                                    android:text="@string/apply_effect"
+                                    android:textSize="18dip" />
+                        </FrameLayout>
+
+                    </LinearLayout>
+
+                    <HorizontalScrollView
+                            android:id="@+id/fxList"
+                            android:layout_width="match_parent"
+                            android:layout_height="@dimen/thumbnail_size"
+                            android:scrollbars="none" >
+
+                        <LinearLayout
+                                android:id="@+id/listFilters"
+                                android:layout_width="wrap_content"
+                                android:layout_height="match_parent"
+                                android:layout_marginLeft="@dimen/thumbnail_margin"
+                                android:orientation="horizontal" >
+                        </LinearLayout>
+                    </HorizontalScrollView>
+
+                    <HorizontalScrollView
+                            android:id="@+id/bordersList"
+                            android:layout_width="match_parent"
+                            android:layout_height="@dimen/thumbnail_size"
+                            android:visibility="gone"
+                            android:scrollbars="none" >
+
+                        <LinearLayout
+                                android:id="@+id/listBorders"
+                                android:layout_width="wrap_content"
+                                android:layout_height="match_parent"
+                                android:layout_marginLeft="@dimen/thumbnail_margin"
+                                android:orientation="horizontal" >
+                        </LinearLayout>
+                    </HorizontalScrollView>
+
+                    <HorizontalScrollView
+                            android:id="@+id/geometryList"
+                            android:layout_width="fill_parent"
+                            android:layout_height="@dimen/thumbnail_size"
+                            android:background="@color/background_main_toolbar"
+                            android:visibility="gone"
+                            android:scrollbars="none" >
+
+                        <LinearLayout
+                                android:id="@+id/listGeometry"
+                                android:layout_width="wrap_content"
+                                android:layout_height="fill_parent"
+                                android:layout_gravity="left"
+                                android:orientation="horizontal" />
+
+                    </HorizontalScrollView>
+
+                    <HorizontalScrollView
+                            android:id="@+id/colorsFxList"
+                            android:layout_width="fill_parent"
+                            android:layout_height="wrap_content"
+                            android:background="@color/background_main_toolbar"
+                            android:visibility="gone"
+                            android:scrollbars="none" >
+
+                        <LinearLayout
+                                android:id="@+id/listColorsFx"
+                                android:layout_width="wrap_content"
+                                android:layout_height="@dimen/thumbnail_size"
+                                android:background="@color/background_main_toolbar"
+                                android:layout_marginLeft="@dimen/thumbnail_margin"
+                                android:orientation="horizontal" >
+
+                        </LinearLayout>
+                    </HorizontalScrollView>
+                </FrameLayout>
+
+                <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="wrap_content"
+                        android:layout_gravity="center"
+                        custom:max_width="400dip"
+                        android:orientation="vertical">
+
+                    <LinearLayout
+                            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>
+
+                </com.android.gallery3d.filtershow.CenteredLinearLayout>
+
+            </com.android.gallery3d.filtershow.CenteredLinearLayout>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/res/layout-land/pano_module_capture.xml b/res/layout-land/pano_module_capture.xml
index 6cad0bf..26cbfb1 100644
--- a/res/layout-land/pano_module_capture.xml
+++ b/res/layout-land/pano_module_capture.xml
@@ -15,7 +15,7 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/camera_app_root"
+        android:id="@+id/camera_app"
         android:layout_height="match_parent"
         android:layout_width="match_parent"
         android:layout_gravity="center"
diff --git a/res/layout-land/photo_module_content.xml b/res/layout-land/photo_module_content.xml
deleted file mode 100644
index d734f83..0000000
--- a/res/layout-land/photo_module_content.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?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.
--->
-<!-- This layout is shared by phone and tablet in landscape orientation. -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/camera_app"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <include layout="@layout/preview_module_frame"/>
-
-    <FrameLayout
-        style="@style/CameraControls"
-        android:layout_gravity="center" >
-
-        <View
-            android:id="@+id/blocker"
-            android:layout_height="match_parent"
-            android:layout_width="@dimen/switcher_size"
-            android:background="@drawable/switcher_bg"
-            android:clickable="true"
-            android:layout_gravity="right" />
-
-        <include layout="@layout/menu_indicators"
-            android:layout_width="80dip"
-            android:layout_height="80dip"
-            android:layout_marginTop="-5dip"
-            android:layout_marginRight="-2dip"
-            android:layout_gravity="top|right"/>
-
-
-        <include layout="@layout/review_module_control"
-            android:layout_marginRight="2dip" />
-
-        <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" />
-
-    </FrameLayout>
-
-</FrameLayout>
diff --git a/res/layout-land/review_module_control.xml b/res/layout-land/review_module_control.xml
index e732a2c..9f8b0cd 100644
--- a/res/layout-land/review_module_control.xml
+++ b/res/layout-land/review_module_control.xml
@@ -13,13 +13,16 @@
      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_height="match_parent"
-        android:layout_width="match_parent">
-    <com.android.camera.ui.RotateImageView android:id="@+id/btn_done"
+<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" />
@@ -34,11 +37,12 @@
         android:background="@drawable/bg_pressed"
         android:src="@drawable/ic_btn_shutter_retake" />
 
-    <com.android.camera.ui.RotateImageView android:id="@+id/btn_cancel"
+    <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" />
-</FrameLayout>
+</com.android.camera.ui.RotatableLayout>
diff --git a/res/layout-land/switcher_popup.xml b/res/layout-land/switcher_popup.xml
index b949f96..fc2d7bc 100644
--- a/res/layout-land/switcher_popup.xml
+++ b/res/layout-land/switcher_popup.xml
@@ -18,8 +18,7 @@
     android:orientation="horizontal"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:layout_alignBottom="@id/camera_switcher"
-    android:layout_alignRight="@id/camera_switcher"
+    android:layout_gravity="bottom|right"
     android:layout_marginRight="8dip"
     android:layout_marginBottom="8dip"
     android:paddingLeft="8dip"
diff --git a/res/layout-land/video_module.xml b/res/layout-land/video_module.xml
deleted file mode 100644
index 972a7f9..0000000
--- a/res/layout-land/video_module.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?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.
--->
-<!-- This layout is shared by phone and tablet in landscape orientation. -->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/camera_app_root"
-    android:layout_height="match_parent"
-    android:layout_width="match_parent">
-    <include layout="@layout/preview_module_frame_video"/>
-
-    <RelativeLayout
-        style="@style/CameraControls"
-        android:layout_centerVertical="true" >
-
-        <View
-            android:id="@+id/blocker"
-            android:layout_width="@dimen/switcher_size"
-            android:layout_height="match_parent"
-            android:background="@drawable/switcher_bg"
-            android:layout_alignParentRight="true"
-            android:clickable="true" />
-
-        <include layout="@layout/menu_indicators"
-            android:layout_width="80dip"
-            android:layout_height="80dip"
-            android:layout_alignParentRight="true"
-            android:layout_alignParentTop="true"
-            android:layout_marginRight="-2dip"
-            android:layout_marginTop="-5dip" />
-
-        <include layout="@layout/bg_replacement_training_message" />
-
-        <include layout="@layout/review_module_control"
-            android:layout_marginRight="2dip" />
-
-        <com.android.camera.ui.PieMenuButton
-            android:id="@+id/menu"
-            style="@style/SwitcherButton"
-            android:layout_alignParentRight="true"
-            android:layout_alignParentTop="true"
-            android:layout_marginRight="2dip"
-            android:contentDescription="@string/accessibility_menu_button" />
-
-    </RelativeLayout>
-
-</RelativeLayout>
diff --git a/res/layout-port/camera_controls.xml b/res/layout-port/camera_controls.xml
new file mode 100644
index 0000000..aa15da1
--- /dev/null
+++ b/res/layout-port/camera_controls.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.
+-->
+<com.android.camera.ui.RotatableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/camera_controls"
+    style="@style/CameraControls"
+    android:layout_gravity="center" >
+
+        <View
+            android:id="@+id/blocker"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/switcher_size"
+            android:layout_gravity="bottom"
+            android:clickable="true" />
+
+        <include layout="@layout/menu_indicators"
+            android:layout_width="80dip"
+            android:layout_height="80dip"
+            android:layout_gravity="bottom|right"
+            android:layout_marginBottom="-2dip"
+            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" />
+
+</com.android.camera.ui.RotatableLayout>
\ No newline at end of file
diff --git a/res/layout-port/camera_shutter_switcher.xml b/res/layout-port/camera_shutter_switcher.xml
deleted file mode 100644
index db73fb0..0000000
--- a/res/layout-port/camera_shutter_switcher.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?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_shutter_switcher"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent" >
-
-    <View
-        android:id="@+id/controls"
-        style="@style/CameraControls"
-        android:layout_alignParentBottom="true"
-        android:layout_centerHorizontal="true" />
-
-    <com.android.camera.ShutterButton
-        android:id="@+id/shutter_button"
-        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:clickable="true"
-        android:contentDescription="@string/accessibility_shutter_button"
-        android:focusable="true"
-        android:scaleType="center"
-        android:src="@drawable/btn_new_shutter" />
-
-    <com.android.camera.ui.CameraSwitcher
-        android:id="@+id/camera_switcher"
-        style="@style/SwitcherButton"
-        android:layout_alignParentBottom="true"
-        android:layout_alignLeft="@id/controls"
-        android:layout_marginBottom="2dip"
-        android:contentDescription="@string/accessibility_mode_picker" />
-</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout-port/pano_module_capture.xml b/res/layout-port/pano_module_capture.xml
index 762447e..d9c9877 100644
--- a/res/layout-port/pano_module_capture.xml
+++ b/res/layout-port/pano_module_capture.xml
@@ -15,7 +15,7 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/camera_app_root"
+        android:id="@+id/camera_app"
         android:layout_height="match_parent"
         android:layout_width="match_parent"
         android:orientation="vertical">
diff --git a/res/layout-port/photo_module_content.xml b/res/layout-port/photo_module_content.xml
deleted file mode 100644
index a82a7a1..0000000
--- a/res/layout-port/photo_module_content.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-<?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.
--->
-<!-- This layout is shared by phone and tablet in landscape orientation. -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/camera_app"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <include layout="@layout/preview_module_frame"/>
-
-    <FrameLayout
-        style="@style/CameraControls"
-        android:layout_gravity="center" >
-
-        <View
-            android:id="@+id/blocker"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/switcher_size"
-            android:layout_gravity="bottom"
-            android:background="@drawable/switcher_bg"
-            android:clickable="true" />
-
-        <include layout="@layout/menu_indicators"
-            android:layout_width="80dip"
-            android:layout_height="80dip"
-            android:layout_gravity="bottom|right"
-            android:layout_marginBottom="-2dip"
-            android:layout_marginRight="-5dip" />
-
-        <include layout="@layout/review_module_control"
-            android:layout_marginBottom="2dip" />
-
-        <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" />
-
-    </FrameLayout>
-
-</FrameLayout>
diff --git a/res/layout-port/review_module_control.xml b/res/layout-port/review_module_control.xml
index 5497754..3c4280e 100644
--- a/res/layout-port/review_module_control.xml
+++ b/res/layout-port/review_module_control.xml
@@ -13,13 +13,16 @@
      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_height="match_parent"
-        android:layout_width="match_parent">
-    <com.android.camera.ui.RotateImageView android:id="@+id/btn_done"
+<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" />
@@ -34,11 +37,12 @@
         android:background="@drawable/bg_pressed"
         android:src="@drawable/ic_btn_shutter_retake" />
 
-    <com.android.camera.ui.RotateImageView android:id="@+id/btn_cancel"
+    <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" />
-</FrameLayout>
+</com.android.camera.ui.RotatableLayout>
diff --git a/res/layout-port/switcher_popup.xml b/res/layout-port/switcher_popup.xml
index b1481a3..8fe09a3 100644
--- a/res/layout-port/switcher_popup.xml
+++ b/res/layout-port/switcher_popup.xml
@@ -18,8 +18,7 @@
     android:orientation="vertical"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:layout_alignBottom="@id/camera_switcher"
-    android:layout_alignLeft="@id/camera_switcher"
+    android:layout_gravity="bottom|left"
     android:layout_marginLeft="8dip"
     android:layout_marginBottom="8dip"
     android:paddingLeft="16dip"
diff --git a/res/layout-port/video_module.xml b/res/layout-port/video_module.xml
deleted file mode 100644
index d8a6490..0000000
--- a/res/layout-port/video_module.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?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.
--->
-<!-- This layout is shared by phone and tablet in landscape orientation. -->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/camera_app_root"
-    android:layout_height="match_parent"
-    android:layout_width="match_parent">
-    <include layout="@layout/preview_module_frame_video"/>
-
-    <RelativeLayout
-        style="@style/CameraControls"
-        android:layout_centerHorizontal="true" >
-
-        <View
-            android:id="@+id/blocker"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/switcher_size"
-            android:background="@drawable/switcher_bg"
-            android:clickable="true"
-            android:layout_alignParentBottom="true" />
-
-        <include layout="@layout/menu_indicators"
-            android:layout_width="80dip"
-            android:layout_height="80dip"
-            android:layout_marginRight="-5dip"
-            android:layout_marginBottom="-2dip"
-            android:layout_alignParentBottom="true"
-            android:layout_alignParentRight="true" />
-
-        <include layout="@layout/bg_replacement_training_message"/>
-
-        <include layout="@layout/review_module_control"
-            android:layout_marginBottom="2dip" />
-
-        <com.android.camera.ui.PieMenuButton
-            android:id="@+id/menu"
-            style="@style/SwitcherButton"
-            android:contentDescription="@string/accessibility_menu_button"
-            android:layout_alignParentBottom="true"
-            android:layout_alignParentRight="true"
-            android:layout_marginBottom="2dip" />
-
-    </RelativeLayout>
-
-</RelativeLayout>
diff --git a/res/layout/album_set.xml b/res/layout/album_set.xml
new file mode 100644
index 0000000..5ff1d23
--- /dev/null
+++ b/res/layout/album_set.xml
@@ -0,0 +1,26 @@
+<?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="match_parent" >
+
+    <GridView
+        android:id="@id/android:list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:numColumns="auto_fit"
+        android:columnWidth="@dimen/album_set_item_width"
+        android:stretchMode="columnWidth"
+        android:drawSelectorOnTop="true"
+        android:padding="10dp"
+        android:horizontalSpacing="10dp"
+        android:verticalSpacing="10dp" />
+
+    <TextView
+        android:id="@id/android:empty"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="@string/empty_album" />
+
+</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..bdecd5f
--- /dev/null
+++ b/res/layout/album_set_item.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:background="#FFF" >
+
+    <TextView
+        android:id="@+id/album_set_item_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:layout_marginLeft="10dp"
+        android:layout_marginTop="10dp"
+        android:ellipsize="end"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <TextView
+        android:id="@+id/album_set_item_date"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignLeft="@+id/album_set_item_title"
+        android:layout_below="@+id/album_set_item_title"
+        android:layout_marginBottom="10dp"
+        android:textAppearance="?android:attr/textAppearanceSmall" />
+
+    <ImageView
+        android:id="@+id/album_set_item_image"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/album_set_item_image_height"
+        android:layout_below="@+id/album_set_item_date"
+        android:scaleType="centerCrop" />
+
+    <ProgressBar
+        android:id="@+id/album_set_item_upload_progress"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="invisible"
+        android:layout_alignParentBottom="true" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/res/layout/camera_main.xml b/res/layout/camera_main.xml
index f5240fe..710e69d 100644
--- a/res/layout/camera_main.xml
+++ b/res/layout/camera_main.xml
@@ -21,10 +21,12 @@
     <include layout="@layout/gl_root_group" />
 
     <FrameLayout
-        android:id="@+id/main_content"
+        android:id="@+id/camera_app_root"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
-    <include layout="@layout/camera_shutter_switcher" />
+    <include layout="@layout/camera_controls"
+        style="@style/CameraControls"
+        android:layout_centerInParent="true" />
 
 </RelativeLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_activity.xml b/res/layout/filtershow_activity.xml
index 9606d91..6bdd487 100644
--- a/res/layout/filtershow_activity.xml
+++ b/res/layout/filtershow_activity.xml
@@ -22,36 +22,16 @@
     android:id="@+id/mainView">
 
     <LinearLayout
-        android:id="@+id/imageStatePanel"
-        android:layout_width="200dip"
-        android:layout_height="match_parent"
-        android:layout_gravity="right"
-        android:orientation="vertical"
-        android:visibility="invisible" >
-
-        <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" />
+            android:layout_height="match_parent"
+            android:orientation="horizontal"
+            android:animateLayoutChanges="true">
 
-        <ListView
-            android:id="@+id/imageStateList"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_weight="1" >
-        </ListView>
-    </LinearLayout>
-
-    <LinearLayout
+        <LinearLayout
         android:id="@+id/mainPanel"
-        android:layout_width="match_parent"
+        android:layout_width="wrap_content"
         android:layout_height="match_parent"
+        android:layout_weight="1"
         android:orientation="vertical" >
 
         <FrameLayout
@@ -70,41 +50,11 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content" />
 
-            <com.android.gallery3d.filtershow.imageshow.ImageStraighten
-                android:id="@+id/imageStraighten"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:visibility="gone" />
-
-            <com.android.gallery3d.filtershow.imageshow.ImageCrop
-                android:id="@+id/imageCrop"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:visibility="gone" />
-
-            <com.android.gallery3d.filtershow.imageshow.ImageRotate
-                android:id="@+id/imageRotate"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:visibility="gone" />
-
-            <com.android.gallery3d.filtershow.imageshow.ImageFlip
-                android:id="@+id/imageFlip"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:visibility="gone" />
-
             <com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet
                 android:id="@+id/imageTinyPlanet"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content" />
 
-            <com.android.gallery3d.filtershow.imageshow.ImageDraw
-                android:id="@+id/imageDraw"
-                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"
@@ -209,37 +159,8 @@
                     android:layout_width="wrap_content"
                     android:layout_height="fill_parent"
                     android:layout_gravity="left"
-                    android:orientation="horizontal">
+                    android:orientation="horizontal" />
 
-                    <com.android.gallery3d.filtershow.ui.IconButton
-                        android:id="@+id/straightenButton"
-                        android:layout_height="match_parent"
-                        style="@style/IconButton"
-                        android:drawableTop="@drawable/filtershow_button_geometry_straighten"
-                        android:text="@string/straighten" />
-
-                    <com.android.gallery3d.filtershow.ui.IconButton
-                        android:id="@+id/cropButton"
-                        android:layout_height="match_parent"
-                        style="@style/IconButton"
-                        android:drawableTop="@drawable/filtershow_button_geometry_crop"
-                        android:text="@string/crop" />
-
-                    <com.android.gallery3d.filtershow.ui.IconButton
-                        android:id="@+id/rotateButton"
-                        android:layout_height="match_parent"
-                        style="@style/IconButton"
-                        android:drawableTop="@drawable/filtershow_button_geometry_rotate"
-                        android:text="@string/rotate" />
-
-                    <com.android.gallery3d.filtershow.ui.IconButton
-                        android:id="@+id/flipButton"
-                        android:layout_height="match_parent"
-                        style="@style/IconButton"
-                        android:drawableTop="@drawable/filtershow_button_geometry_flip"
-                        android:text="@string/mirror" />
-
-                </LinearLayout>
             </HorizontalScrollView>
 
             <HorizontalScrollView
@@ -323,6 +244,38 @@
         </com.android.gallery3d.filtershow.CenteredLinearLayout>
 
         </com.android.gallery3d.filtershow.CenteredLinearLayout>
+
+    </LinearLayout>
+
+        <LinearLayout
+                android:id="@+id/imageStatePanel"
+                android:layout_width="400dip"
+                android:layout_height="match_parent"
+                android:layout_gravity="right"
+                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/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>
+
     </LinearLayout>
 
     <LinearLayout
diff --git a/res/layout/filtershow_history_operation_row.xml b/res/layout/filtershow_history_operation_row.xml
index dd9b66e..4042f71 100644
--- a/res/layout/filtershow_history_operation_row.xml
+++ b/res/layout/filtershow_history_operation_row.xml
@@ -18,40 +18,31 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:orientation="horizontal"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:padding="10dip"
     android:background="@drawable/filtershow_button_background">
 
     <ImageView
-        android:id="@+id/selectedMark"
-        android:src="@drawable/camera_crop"
-        android:background="@android:color/transparent"
-        android:layout_width="32dip"
-        android:layout_height="match_parent"
-        android:scaleType="centerInside"
-        android:visibility="invisible"
-        >
-    </ImageView>
+            android:id="@+id/preview"
+            android:background="@android:color/transparent"
+            android:layout_width="match_parent"
+            android:layout_height="128dip"
+            android:scaleType="centerCrop"
+            android:cropToPadding="true"
+            android:paddingTop="10dip"
+            android:visibility="visible"
+            />
 
     <TextView
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/rowTextView"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:padding="10dip"
-        android:textSize="16dip" >
+            xmlns:android="http://schemas.android.com/apk/res/android"
+            android:id="@+id/rowTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center_horizontal"
+            android:padding="5dip"
+            android:textSize="16dip">
     </TextView>
 
-    <ImageView
-        android:id="@+id/typeMark"
-        android:src="@drawable/filtershow_button_origin"
-        android:background="@android:color/transparent"
-        android:layout_width="32dip"
-        android:layout_height="match_parent"
-        android:scaleType="centerInside"
-        android:paddingRight="4dip"
-        android:visibility="visible"
-        >
-    </ImageView>
-
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout/filtershow_imagestate_row.xml b/res/layout/filtershow_imagestate_row.xml
index 2e9b1bf..d62f54c 100644
--- a/res/layout/filtershow_imagestate_row.xml
+++ b/res/layout/filtershow_imagestate_row.xml
@@ -14,13 +14,25 @@
      limitations under the License.
 -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.gallery3d.filtershow.MovableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="128dip"
     android:orientation="horizontal"
     android:background="@drawable/filtershow_button_background">
 
+    <ImageView
+            android:id="@+id/selectedMark"
+            android:src="@drawable/camera_crop"
+            android:background="@android:color/transparent"
+            android:layout_width="32dip"
+            android:layout_height="match_parent"
+            android:scaleType="centerInside"
+            android:visibility="visible"
+            android:layout_weight="1"
+            >
+    </ImageView>
+
     <TextView
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/imagestate_label"
@@ -44,4 +56,4 @@
         android:textStyle="bold" >
     </TextView>
 
-</LinearLayout>
\ No newline at end of file
+</com.android.gallery3d.filtershow.MovableLinearLayout>
\ No newline at end of file
diff --git a/res/layout/panorama_module.xml b/res/layout/panorama_module.xml
index 901bb6b..9ecbd07 100644
--- a/res/layout/panorama_module.xml
+++ b/res/layout/panorama_module.xml
@@ -14,10 +14,10 @@
      limitations under the License.
 -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/pano_layout"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <include layout="@layout/pano_module_capture" />
     <include layout="@layout/pano_review" />
-</RelativeLayout>
+</merge>
diff --git a/res/layout/photo_module.xml b/res/layout/photo_module.xml
index b2ad702..abf094e 100644
--- a/res/layout/photo_module.xml
+++ b/res/layout/photo_module.xml
@@ -20,10 +20,20 @@
  need to be recreated in onConfigurationChanged from old photo_module to this
  layout. -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/camera_app_root"
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:layout_gravity="center">
     <include layout="@layout/count_down_to_capture"/>
-    <include layout="@layout/photo_module_content"/>
-</FrameLayout>
\ No newline at end of file
+
+    <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" />
+</merge>
\ No newline at end of file
diff --git a/res/layout/photo_set.xml b/res/layout/photo_set.xml
new file mode 100644
index 0000000..d929cad
--- /dev/null
+++ b/res/layout/photo_set.xml
@@ -0,0 +1,28 @@
+<?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="match_parent"
+    android:paddingLeft="8dp"
+    android:paddingRight="8dp" >
+
+    <GridView
+        android:id="@id/android:list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:drawSelectorOnTop="true"
+        android:numColumns="auto_fit"
+        android:stretchMode="columnWidth"
+        android:columnWidth="200dip"
+        android:horizontalSpacing="4dip"
+        android:verticalSpacing="4dip"
+        android:padding="4dip" />
+
+    <TextView
+        android:id="@id/android:empty"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="@string/empty_album" />
+
+</FrameLayout>
\ 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..b56184e
--- /dev/null
+++ b/res/layout/photo_set_item.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="200dip"
+    android:id="@+id/thumbnail">
+
+</ImageView>
\ No newline at end of file
diff --git a/res/layout/preview_module_frame.xml b/res/layout/preview_module_frame.xml
deleted file mode 100644
index 66094c9..0000000
--- a/res/layout/preview_module_frame.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?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.
--->
-
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/frame_layout"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_weight="1">
-    <com.android.camera.PreviewFrameLayout android:id="@+id/frame"
-            android:layout_centerInParent="true"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
-        <include layout="@layout/preview_surface_view"/>
-        <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" />
-    </com.android.camera.PreviewFrameLayout>
-    <ImageView android:id="@+id/capture_anim_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layerType="hardware"
-            android:visibility="gone"/>
-</RelativeLayout>
diff --git a/res/layout/preview_surface_view.xml b/res/layout/preview_surface_view.xml
deleted file mode 100644
index cdaf0ee..0000000
--- a/res/layout/preview_surface_view.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.PreviewSurfaceView xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/preview_surface_view"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="gone"/>
diff --git a/res/layout/preview_module_frame_video.xml b/res/layout/video_module.xml
similarity index 75%
rename from res/layout/preview_module_frame_video.xml
rename to res/layout/video_module.xml
index 3418faf..790f3eb 100644
--- a/res/layout/preview_module_frame_video.xml
+++ b/res/layout/video_module.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
+<!-- Copyright (C) 2012 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -13,16 +13,20 @@
      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/frame_layout"
-        android:layout_height="match_parent"
-        android:layout_width="match_parent"
-        android:layout_weight="1">
+<!-- This layout is shared by phone and tablet in landscape orientation. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/camera_app_root"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent">
     <com.android.camera.PreviewFrameLayout android:id="@+id/frame"
             android:layout_height="match_parent"
             android:layout_width="match_parent"
-            android:layout_centerInParent="true">
-        <include layout="@layout/preview_surface_view"/>
+            android:layout_gravity="center">
+        <com.android.camera.ui.PreviewSurfaceView
+                android:id="@+id/preview_surface_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:visibility="gone"/>
         <FrameLayout android:id="@+id/preview_border"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
@@ -41,7 +45,7 @@
                 android:layout_width="match_parent"
                 android:visibility="gone"
                 android:background="@android:color/black"/>
-        <com.android.camera.ui.RotateImageView
+        <ImageView
                 android:id="@+id/btn_play"
                 style="@style/ReviewControlIcon"
                 android:layout_centerInParent="true"
@@ -50,11 +54,4 @@
                 android:onClick="onReviewPlayClicked"/>
     </com.android.camera.PreviewFrameLayout>
 
-    <ImageView android:id="@+id/capture_anim_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layerType="hardware"
-            android:visibility="gone"/>
-
-</RelativeLayout>
-
+</merge>
diff --git a/res/menu/filtershow_activity_menu.xml b/res/menu/filtershow_activity_menu.xml
index 4ea7d17..f62bf39 100644
--- a/res/menu/filtershow_activity_menu.xml
+++ b/res/menu/filtershow_activity_menu.xml
@@ -2,18 +2,19 @@
     <item
         android:id="@+id/menu_share"
         android:actionProviderClass="android.widget.ShareActionProvider"
-        android:showAsAction="always"
+        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="never"
+        android:showAsAction="always"
         android:title="@string/filtershow_undo"/>
     <item
         android:id="@+id/redoButton"
         android:icon="@drawable/filtershow_button_redo"
-        android:showAsAction="never"
+        android:showAsAction="always"
         android:title="@string/filtershow_redo"/>
     <item
         android:id="@+id/resetHistoryButton"
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/values-af/filtershow_strings.xml b/res/values-af/filtershow_strings.xml
index 9f5751e..0797bfc 100644
--- a/res/values-af/filtershow_strings.xml
+++ b/res/values-af/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Stel terug"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Huidige prenttoestand"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 4dfbaf3..3b74cbd 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-am/filtershow_strings.xml b/res/values-am/filtershow_strings.xml
index cf72a53..87a13e8 100644
--- a/res/values-am/filtershow_strings.xml
+++ b/res/values-am/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"ዳግም አስጀምር"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"የአሁኑ ምስል ሁኔታ"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 85d320d..1b302e9 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-ar/filtershow_strings.xml b/res/values-ar/filtershow_strings.xml
index ee1bfca..9801326 100644
--- a/res/values-ar/filtershow_strings.xml
+++ b/res/values-ar/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"إعادة تعيين"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"حالة الصورة الحالية"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 98ebb65..bf643cf 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-be/filtershow_strings.xml b/res/values-be/filtershow_strings.xml
index 06af904..fd1caee 100644
--- a/res/values-be/filtershow_strings.xml
+++ b/res/values-be/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Скінуць"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Бягучы стан малюнка"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index fa6bd3f..d259512 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-bg/filtershow_strings.xml b/res/values-bg/filtershow_strings.xml
index cc4daae..1c907d4 100644
--- a/res/values-bg/filtershow_strings.xml
+++ b/res/values-bg/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Повторно задаване"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Текущо състояние на изображението"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 7f870a5..650aa22 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-ca/filtershow_strings.xml b/res/values-ca/filtershow_strings.xml
index 5f4b8e1..242464c 100644
--- a/res/values-ca/filtershow_strings.xml
+++ b/res/values-ca/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Restableix"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Estat de la imatge actual"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 1a9eb6a..e6fd830 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-cs/filtershow_strings.xml b/res/values-cs/filtershow_strings.xml
index e0c59f5..d8681f7 100644
--- a/res/values-cs/filtershow_strings.xml
+++ b/res/values-cs/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Obnovit"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Aktuální stav obrázku"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 6702e86..f5449de 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-da/filtershow_strings.xml b/res/values-da/filtershow_strings.xml
index bc5ed33..38f9698 100644
--- a/res/values-da/filtershow_strings.xml
+++ b/res/values-da/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Nulstil"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Nuværende billedtilstand"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index de3dbaa..e182e96 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -387,4 +387,8 @@
     <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>
 </resources>
diff --git a/res/values-de/filtershow_strings.xml b/res/values-de/filtershow_strings.xml
index 0c660bc..555d53f 100644
--- a/res/values-de/filtershow_strings.xml
+++ b/res/values-de/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Zurücksetzen"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Aktueller Bildstatus"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 79860a2..be51b2c 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-el/filtershow_strings.xml b/res/values-el/filtershow_strings.xml
index 40acfdc..b22363f 100644
--- a/res/values-el/filtershow_strings.xml
+++ b/res/values-el/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Επαναφορά"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Κατάσταση τρέχουσας εικόνας"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 08c39d4..33e654a 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-en-rGB/filtershow_strings.xml b/res/values-en-rGB/filtershow_strings.xml
index ad6e4e0..fcaf45c 100644
--- a/res/values-en-rGB/filtershow_strings.xml
+++ b/res/values-en-rGB/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Reset"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Current Image State"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 587aa54..4dc3868 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-es-rUS/filtershow_strings.xml b/res/values-es-rUS/filtershow_strings.xml
index e3a9a8e..4efffad 100644
--- a/res/values-es-rUS/filtershow_strings.xml
+++ b/res/values-es-rUS/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Restablecer"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Estado actual de la imagen"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 610aa59..e216ece 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-es/filtershow_strings.xml b/res/values-es/filtershow_strings.xml
index e3820fa..ebd8c2c 100644
--- a/res/values-es/filtershow_strings.xml
+++ b/res/values-es/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Restablecer"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Estado de imagen actual"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 47480cb..1c22b31 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-et/filtershow_strings.xml b/res/values-et/filtershow_strings.xml
index d7be6e5..161ac90 100644
--- a/res/values-et/filtershow_strings.xml
+++ b/res/values-et/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Lähtesta"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Pildi praegune olek"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index a4b5a91..a56bd38 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-fa/filtershow_strings.xml b/res/values-fa/filtershow_strings.xml
index 3051612..db3dae4 100644
--- a/res/values-fa/filtershow_strings.xml
+++ b/res/values-fa/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"بازنشانی"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"وضعیت کنونی تصویر"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index a2e0eb7..1ed9d0f 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-fi/filtershow_strings.xml b/res/values-fi/filtershow_strings.xml
index 1c9491c..92892a6 100644
--- a/res/values-fi/filtershow_strings.xml
+++ b/res/values-fi/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Palauta"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Kuvan nykyinen tila"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index d769c32..f60f787 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-fr/filtershow_strings.xml b/res/values-fr/filtershow_strings.xml
index 868e61a..24685c0 100644
--- a/res/values-fr/filtershow_strings.xml
+++ b/res/values-fr/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Réinitialiser"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"État actuel de l\'image"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 19014f8..dfa310b 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-hi/filtershow_strings.xml b/res/values-hi/filtershow_strings.xml
index 88ac1a4..6029cd1 100644
--- a/res/values-hi/filtershow_strings.xml
+++ b/res/values-hi/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"रीसेट करें"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"वर्तमान चित्र स्थिति"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 307430d..27e3a11 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-hr/filtershow_strings.xml b/res/values-hr/filtershow_strings.xml
index ee4b51d..459a08e 100644
--- a/res/values-hr/filtershow_strings.xml
+++ b/res/values-hr/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Poništi"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Trenutačno stanje slike"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index cff01be..88065e5 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-hu/filtershow_strings.xml b/res/values-hu/filtershow_strings.xml
index cb0a78e..504eabc 100644
--- a/res/values-hu/filtershow_strings.xml
+++ b/res/values-hu/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Visszaállítás"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Aktuális fényképállapot"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 79cc854..d7bdc0a 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-in/filtershow_strings.xml b/res/values-in/filtershow_strings.xml
index eb30676..f9a118b 100644
--- a/res/values-in/filtershow_strings.xml
+++ b/res/values-in/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Setel Ulang"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Status Gambar Saat Ini"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 7e9c58a..273c88d 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-it/filtershow_strings.xml b/res/values-it/filtershow_strings.xml
index 8de522c..6f8fff2 100644
--- a/res/values-it/filtershow_strings.xml
+++ b/res/values-it/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Reimposta"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Stato immagine attuale"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 4aa7c58..1edf32d 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-iw/filtershow_strings.xml b/res/values-iw/filtershow_strings.xml
index 36f7a51..e707495 100644
--- a/res/values-iw/filtershow_strings.xml
+++ b/res/values-iw/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"אפס"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"מצב תמונה נוכחית"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index f5d6888..843fc2c 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-ja/filtershow_strings.xml b/res/values-ja/filtershow_strings.xml
index 8ad69e1..65b53a1 100644
--- a/res/values-ja/filtershow_strings.xml
+++ b/res/values-ja/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"リセット"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"現在の画像ステータス"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 3ecb475..e3403cc 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-ko/filtershow_strings.xml b/res/values-ko/filtershow_strings.xml
index 9a7005b..902649f 100644
--- a/res/values-ko/filtershow_strings.xml
+++ b/res/values-ko/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"초기화"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"현재 이미지 상태"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 9141dfc..23b1462 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-lt/filtershow_strings.xml b/res/values-lt/filtershow_strings.xml
index 06e6d28..2f91fb6 100644
--- a/res/values-lt/filtershow_strings.xml
+++ b/res/values-lt/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Nust. iš naujo"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Dabartinė vaizdo būsena"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index f7e1dde..5d7eebd 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-lv/filtershow_strings.xml b/res/values-lv/filtershow_strings.xml
index 9ac6f7f..155784c 100644
--- a/res/values-lv/filtershow_strings.xml
+++ b/res/values-lv/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Atiestatīt"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Pašreizējais attēla statuss"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index f26bdce..5b188c88 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-ms/filtershow_strings.xml b/res/values-ms/filtershow_strings.xml
index ee7287c..3f583b3 100644
--- a/res/values-ms/filtershow_strings.xml
+++ b/res/values-ms/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Tetapkan semula"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Keadaan Imej Semasa"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index f17d3bb..31e10c6 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-nb/filtershow_strings.xml b/res/values-nb/filtershow_strings.xml
index 3f0f06a..a7b4396 100644
--- a/res/values-nb/filtershow_strings.xml
+++ b/res/values-nb/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Tilbakestill"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Nåværende bildetilstand"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 22acbaa..41c0297 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -387,4 +387,8 @@
     <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>
 </resources>
diff --git a/res/values-nl/filtershow_strings.xml b/res/values-nl/filtershow_strings.xml
index 8bbbe3b..302cc22 100644
--- a/res/values-nl/filtershow_strings.xml
+++ b/res/values-nl/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Opnieuw instellen"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Huidige afbeeldingsstatus"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 50f83ce..e9ba640 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-pl/filtershow_strings.xml b/res/values-pl/filtershow_strings.xml
index 48a45cc..880cf82 100644
--- a/res/values-pl/filtershow_strings.xml
+++ b/res/values-pl/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Resetuj"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Obecny stan obrazu"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 4e595e3..bc9f628 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-pt-rPT/filtershow_strings.xml b/res/values-pt-rPT/filtershow_strings.xml
index e94bf45..389d63a 100644
--- a/res/values-pt-rPT/filtershow_strings.xml
+++ b/res/values-pt-rPT/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Repor"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Estado da Imagem Atual"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 20e3f7b..56a1311 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-pt/filtershow_strings.xml b/res/values-pt/filtershow_strings.xml
index 8c80270..14a7520 100644
--- a/res/values-pt/filtershow_strings.xml
+++ b/res/values-pt/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Restaurar"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Estado atual da imagem"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 05409b1..b60e59a 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
index 6fd007e..18e4bef 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -654,4 +654,12 @@
     <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 />
 </resources>
diff --git a/res/values-ro/filtershow_strings.xml b/res/values-ro/filtershow_strings.xml
index 46be094..68b067c 100644
--- a/res/values-ro/filtershow_strings.xml
+++ b/res/values-ro/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Resetaţi"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Starea curentă a imaginii"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 71c155a..51059a8 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-ru/filtershow_strings.xml b/res/values-ru/filtershow_strings.xml
index 42d6db5..cb18bed 100644
--- a/res/values-ru/filtershow_strings.xml
+++ b/res/values-ru/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Сброс"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Состояние изображения"</string>
+    <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>
@@ -45,7 +46,7 @@
     <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>
+    <string name="aspectNone_effect" msgid="6263330561046574134">"Вручную"</string>
     <!-- no translation found for aspectOriginal_effect (5678516555493036594) -->
     <skip />
     <string name="Fixed" msgid="8017376448916924565">"Постоянное"</string>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 3d77c35..13e6ef8 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-sk/filtershow_strings.xml b/res/values-sk/filtershow_strings.xml
index a49bd3c..b999871 100644
--- a/res/values-sk/filtershow_strings.xml
+++ b/res/values-sk/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Obnoviť"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Aktuálny stav obrázka"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 3d924da..ab42509 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-sl/filtershow_strings.xml b/res/values-sl/filtershow_strings.xml
index e0539e3..61bd61e 100644
--- a/res/values-sl/filtershow_strings.xml
+++ b/res/values-sl/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Ponastavi"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Trenutno stanje slike"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index edc5e14..ebea3fc 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-sr/filtershow_strings.xml b/res/values-sr/filtershow_strings.xml
index d5837d9..b6d66cb 100644
--- a/res/values-sr/filtershow_strings.xml
+++ b/res/values-sr/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Поново постави"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Актуелни статус слике"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index fd4bf1b..c0b0452 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-sv/filtershow_strings.xml b/res/values-sv/filtershow_strings.xml
index f43c7d2..3644a5f 100644
--- a/res/values-sv/filtershow_strings.xml
+++ b/res/values-sv/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Återställ"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Aktuellt bildläge"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 2981c15..86aeb62 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-sw/filtershow_strings.xml b/res/values-sw/filtershow_strings.xml
index 9f26a05..6f66386 100644
--- a/res/values-sw/filtershow_strings.xml
+++ b/res/values-sw/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Weka upya"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Hali ya Sasa ya Picha"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index a99402c..d98c0ce 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -394,4 +394,8 @@
     <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>
 </resources>
diff --git a/res/values-th/filtershow_strings.xml b/res/values-th/filtershow_strings.xml
index 1f86b14..2cdaf87 100644
--- a/res/values-th/filtershow_strings.xml
+++ b/res/values-th/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"รีเซ็ต"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"สถานะภาพปัจจุบัน"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index ba9ab83..d22b540 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-tl/filtershow_strings.xml b/res/values-tl/filtershow_strings.xml
index 8ea767a..4861dde 100644
--- a/res/values-tl/filtershow_strings.xml
+++ b/res/values-tl/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"I-reset"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Kasalukuyang Katayuan ng Larawan"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 2acc2dc..0295ad8 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-tr/filtershow_strings.xml b/res/values-tr/filtershow_strings.xml
index cdc8189..63740ec 100644
--- a/res/values-tr/filtershow_strings.xml
+++ b/res/values-tr/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Sıfırla"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Mevcut Resim Durumu"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 71c1671..2755fff 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-uk/filtershow_strings.xml b/res/values-uk/filtershow_strings.xml
index a05ef93..c236088 100644
--- a/res/values-uk/filtershow_strings.xml
+++ b/res/values-uk/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Скинути"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Поточний стан зображення"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 1af1e56..fcae756 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-v14/styles.xml b/res/values-v14/styles.xml
index 18f3440..c05bf30 100644
--- a/res/values-v14/styles.xml
+++ b/res/values-v14/styles.xml
@@ -22,4 +22,8 @@
     <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
index 0f4c9d3..660172b 100644
--- a/res/values-vi/filtershow_strings.xml
+++ b/res/values-vi/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Đặt lại"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Trạng thái hình ảnh hiện tại"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 86c2503..51cb2b1 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-zh-rCN/filtershow_strings.xml b/res/values-zh-rCN/filtershow_strings.xml
index 8876ce8..1c060bc 100644
--- a/res/values-zh-rCN/filtershow_strings.xml
+++ b/res/values-zh-rCN/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"重置"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"当前的图片状态"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index f41fa83..5b7ea91 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-zh-rTW/filtershow_strings.xml b/res/values-zh-rTW/filtershow_strings.xml
index 8f205fb..f2a3679 100644
--- a/res/values-zh-rTW/filtershow_strings.xml
+++ b/res/values-zh-rTW/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"重設"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"目前圖片狀態"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 3178058..ac46bc8 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -389,4 +389,8 @@
     <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>
 </resources>
diff --git a/res/values-zu/filtershow_strings.xml b/res/values-zu/filtershow_strings.xml
index 5798c10..22126bf 100644
--- a/res/values-zu/filtershow_strings.xml
+++ b/res/values-zu/filtershow_strings.xml
@@ -20,6 +20,7 @@
     <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>
@@ -33,7 +34,7 @@
     <string name="reset" msgid="9013181350779592937">"Setha kabusha"</string>
     <!-- no translation found for history_original (150973253194312841) -->
     <skip />
-    <string name="imageState" msgid="3609930035023754855">"Isimo sesithombe samanje"</string>
+    <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>
@@ -59,6 +60,7 @@
     <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>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index b7f8174..6d3b682 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -391,4 +391,8 @@
     <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>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index cb38007..d2720d9 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -88,4 +88,8 @@
     <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">600dip</dimen>
+    <dimen name="navigation_bar_height">48dip</dimen>
+    <dimen name="navigation_bar_width">42dip</dimen>
 </resources>
diff --git a/res/values/dimensions.xml b/res/values/dimensions.xml
index dc9e8c3..ae50680 100644
--- a/res/values/dimensions.xml
+++ b/res/values/dimensions.xml
@@ -19,7 +19,7 @@
     <dimen name="stack_photo_width">160dp</dimen>
     <dimen name="stack_photo_height">120dp</dimen>
 
-    <!-- configuration for album set page -->
+    <!-- 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>
@@ -50,4 +50,8 @@
     <!--  configuration for filtershow UI -->
     <dimen name="thumbnail_size">96dip</dimen>
     <dimen name="thumbnail_margin">3dip</dimen>
+
+    <!-- configuration for album set page -->
+    <dimen name="album_set_item_image_height">100dp</dimen>
+    <dimen name="album_set_item_width">160dp</dimen>
 </resources>
diff --git a/res/values/filtershow_ids.xml b/res/values/filtershow_ids.xml
index 28e7816..9380740 100644
--- a/res/values/filtershow_ids.xml
+++ b/res/values/filtershow_ids.xml
@@ -42,4 +42,8 @@
     <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" />
 </resources>
diff --git a/res/values/filtershow_strings.xml b/res/values/filtershow_strings.xml
index 3e9e355..80d0fc6 100644
--- a/res/values/filtershow_strings.xml
+++ b/res/values/filtershow_strings.xml
@@ -62,7 +62,7 @@
     <!--  Image state panel -->
 
     <!--  Text for the image state panel title [CHAR LIMIT=50] -->
-    <string name="imageState">Current Image State</string>
+    <string name="imageState">Applied Effects</string>
 
     <!--  Additional filters buttons  -->
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index fae3466..263b8b1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1002,4 +1002,12 @@
     <!-- 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>
 </resources>
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
index a207b2b..24c49ba 100644
--- a/src/com/android/camera/CameraActivity.java
+++ b/src/com/android/camera/CameraActivity.java
@@ -19,8 +19,8 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
-import android.content.Context;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.content.pm.ActivityInfo;
@@ -30,16 +30,18 @@
 import android.os.IBinder;
 import android.provider.MediaStore;
 import android.provider.Settings;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.OrientationEventListener;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
 import android.widget.FrameLayout;
 
 import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.RotatableLayout;
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.PhotoPage;
 import com.android.gallery3d.common.ApiHelper;
@@ -56,12 +58,14 @@
     private FrameLayout mFrame;
     private ShutterButton mShutter;
     private CameraSwitcher mSwitcher;
-    private View mShutterSwitcher;
+    private View mCameraControls;
     private View mControlsBackground;
+    private View mPieMenuButton;
     private Drawable[] mDrawables;
     private int mCurrentModuleIndex;
     private MotionEvent mDown;
     private boolean mAutoRotateScreen;
+    private int mHeightOrWidth = -1;
 
     private MyOrientationEventListener mOrientationListener;
     // The degrees of the device rotated clockwise from its natural orientation.
@@ -92,7 +96,7 @@
     public void onCreate(Bundle state) {
         super.onCreate(state);
         setContentView(R.layout.camera_main);
-        mFrame = (FrameLayout) findViewById(R.id.main_content);
+        mFrame = (FrameLayout) findViewById(R.id.camera_app_root);
         mDrawables = new Drawable[DRAW_IDS.length];
         for (int i = 0; i < DRAW_IDS.length; i++) {
             mDrawables[i] = getResources().getDrawable(DRAW_IDS[i]);
@@ -113,10 +117,13 @@
     }
 
     public void init() {
-        mControlsBackground = findViewById(R.id.controls);
-        mShutterSwitcher = findViewById(R.id.camera_shutter_switcher);
+        boolean landscape = Util.getDisplayRotation(this) % 180 == 90;
+        setMargins(landscape);
+        mControlsBackground = findViewById(R.id.blocker);
+        mCameraControls = findViewById(R.id.camera_controls);
         mShutter = (ShutterButton) findViewById(R.id.shutter_button);
         mSwitcher = (CameraSwitcher) findViewById(R.id.camera_switcher);
+        mPieMenuButton = findViewById(R.id.menu);
         int totaldrawid = (LightCycleHelper.hasLightCycleCapture(this)
                                 ? DRAW_IDS.length : DRAW_IDS.length - 1);
         if (!ApiHelper.HAS_OLD_PANORAMA) totaldrawid--;
@@ -215,6 +222,8 @@
                 mCurrentModule = LightCycleHelper.createPanoramaModule();
                 break;
         }
+        showPieMenuButton(mCurrentModule.needsPieMenu());
+
         openModule(mCurrentModule, canReuse);
         mCurrentModule.onOrientationChanged(mLastRawOrientation);
         if (mMediaSaveService != null) {
@@ -224,6 +233,18 @@
         getCameraScreenNail().setOnFrameDrawnOneShot(mOnFrameDrawn);
     }
 
+    public void showPieMenuButton(boolean show) {
+        if (show) {
+            findViewById(R.id.blocker).setVisibility(View.VISIBLE);
+            findViewById(R.id.menu).setVisibility(View.VISIBLE);
+            findViewById(R.id.on_screen_indicators).setVisibility(View.VISIBLE);
+        } else {
+            findViewById(R.id.blocker).setVisibility(View.INVISIBLE);
+            findViewById(R.id.menu).setVisibility(View.INVISIBLE);
+            findViewById(R.id.on_screen_indicators).setVisibility(View.INVISIBLE);
+        }
+    }
+
     private Runnable mOnFrameDrawn = new Runnable() {
 
         @Override
@@ -266,13 +287,13 @@
     }
 
     public void hideUI() {
-        mControlsBackground.setVisibility(View.INVISIBLE);
+        mCameraControls.setVisibility(View.INVISIBLE);
         hideSwitcher();
         mShutter.setVisibility(View.GONE);
     }
 
     public void showUI() {
-        mControlsBackground.setVisibility(View.VISIBLE);
+        mCameraControls.setVisibility(View.VISIBLE);
         showSwitcher();
         mShutter.setVisibility(View.VISIBLE);
         // Force a layout change to show shutter button
@@ -297,25 +318,24 @@
     @Override
     public void onConfigurationChanged(Configuration config) {
         super.onConfigurationChanged(config);
-
-        ViewGroup appRoot = (ViewGroup) findViewById(R.id.content);
-        // remove old switcher, shutter and shutter icon
-        View cameraControlsView = findViewById(R.id.camera_shutter_switcher);
-        appRoot.removeView(cameraControlsView);
-
-        // create new layout with the current orientation
-        LayoutInflater inflater = getLayoutInflater();
-        inflater.inflate(R.layout.camera_shutter_switcher, appRoot);
-        init();
-
-        if (mShowCameraAppView) {
-            showUI();
-        } else {
-            hideUI();
-        }
+        boolean landscape = (config.orientation == Configuration.ORIENTATION_LANDSCAPE);
+        setMargins(landscape);
         mCurrentModule.onConfigurationChanged(config);
     }
 
+    private void setMargins(boolean landscape) {
+        ViewGroup appRoot = (ViewGroup) findViewById(R.id.content);
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) appRoot.getLayoutParams();
+        int navBarWidth = getResources().getDimensionPixelSize(R.dimen.navigation_bar_width);
+        int navBarHeight = getResources().getDimensionPixelSize(R.dimen.navigation_bar_height);
+        if (landscape) {
+            lp.setMargins(navBarHeight, 0, navBarHeight - navBarWidth, 0);
+        } else {
+            lp.setMargins(0, navBarHeight, 0, 0);
+        }
+        appRoot.setLayoutParams(lp);
+    }
+
     @Override
     public void onPause() {
         mPaused = true;
@@ -362,9 +382,23 @@
             hideUI();
         }
         super.onFullScreenChanged(full);
+        if (ApiHelper.HAS_ROTATION_ANIMATION) {
+            setRotationAnimation(full);
+        }
         mCurrentModule.onFullScreenChanged(full);
     }
 
+    private void setRotationAnimation(boolean fullscreen) {
+        int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
+        if (fullscreen) {
+            rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
+        }
+        Window win = getWindow();
+        WindowManager.LayoutParams winParams = win.getAttributes();
+        winParams.rotationAnimation = rotationAnimation;
+        win.setAttributes(winParams);
+    }
+
     @Override
     protected void onStop() {
         super.onStop();
@@ -440,9 +474,10 @@
         }
         if ((mSwitcher != null) && mSwitcher.showsPopup() && !mSwitcher.isInsidePopup(m)) {
             return mSwitcher.onTouch(null, m);
+        } else if ((mSwitcher != null) && mSwitcher.isInsidePopup(m)) {
+            return superDispatchTouchEvent(m);
         } else {
-            return mShutterSwitcher.dispatchTouchEvent(m)
-                    || mCurrentModule.dispatchTouchEvent(m);
+            return mCurrentModule.dispatchTouchEvent(m);
         }
     }
 
diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java
index 37eabd0..aa057b9 100644
--- a/src/com/android/camera/CameraModule.java
+++ b/src/com/android/camera/CameraModule.java
@@ -68,6 +68,8 @@
 
     public boolean needsSwitcher();
 
+    public boolean needsPieMenu();
+
     public void onOrientationChanged(int orientation);
 
     public void onShowSwitcherPopup();
diff --git a/src/com/android/camera/PanoramaModule.java b/src/com/android/camera/PanoramaModule.java
index 4edc686..d12c828 100644
--- a/src/com/android/camera/PanoramaModule.java
+++ b/src/com/android/camera/PanoramaModule.java
@@ -714,13 +714,13 @@
     }
 
     private void createContentView() {
-        mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView);
+        mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView, true);
         Resources appRes = mActivity.getResources();
-        mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root);
+        mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app);
         mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
         mReviewBackground = appRes.getColor(R.color.review_background);
         mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
-        mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout);
+        mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.camera_app_root);
         mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog);
         setViews(appRes);
     }
@@ -1308,6 +1308,11 @@
     }
 
     @Override
+    public boolean needsPieMenu() {
+        return false;
+    }
+
+    @Override
     public void onShowSwitcherPopup() {
     }
 
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index 15f4046..4049aa5 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -52,7 +52,9 @@
 import android.view.OrientationEventListener;
 import android.view.SurfaceHolder;
 import android.view.View;
+import android.view.ViewStub;
 import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
@@ -66,11 +68,8 @@
 import com.android.camera.ui.FaceView;
 import com.android.camera.ui.PieRenderer;
 import com.android.camera.ui.PopupManager;
-import com.android.camera.ui.PreviewSurfaceView;
 import com.android.camera.ui.RenderOverlay;
-import com.android.camera.ui.Rotatable;
 import com.android.camera.ui.RotateTextToast;
-import com.android.camera.ui.TwoStateImageView;
 import com.android.camera.ui.ZoomRenderer;
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
@@ -92,7 +91,6 @@
     FocusOverlayManager.Listener,
     CameraPreference.OnPreferenceChangedListener,
     LocationManager.Listener,
-    PreviewFrameLayout.OnSizeChangedListener,
     ShutterButton.OnShutterButtonListener,
     SurfaceHolder.Callback,
     PieRenderer.PieListener,
@@ -172,18 +170,15 @@
     private ShutterButton mShutterButton;
     private boolean mFaceDetectionStarted = false;
 
-    private PreviewFrameLayout mPreviewFrameLayout;
     private Object mSurfaceTexture;
     private CountDownView mCountDownView;
 
-    // for API level 10
-    private PreviewSurfaceView mPreviewSurfaceView;
     private volatile SurfaceHolder mCameraSurfaceHolder;
 
     private FaceView mFaceView;
     private RenderOverlay mRenderOverlay;
-    private Rotatable mReviewCancelButton;
-    private Rotatable mReviewDoneButton;
+    private View mReviewCancelButton;
+    private View mReviewDoneButton;
     private View mReviewRetakeButton;
 
     // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true.
@@ -212,6 +207,16 @@
         }
     };
 
+    private final View.OnLayoutChangeListener mLayoutChangeListener =
+            new View.OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right,
+                int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+            onScreenSizeChanged(right - left, bottom - top);
+        }
+    };
+    private int mPreviewWidth = 0;
+    private int mPreviewHeight = 0;
     private final StringBuilder mBuilder = new StringBuilder();
     private final Formatter mFormatter = new Formatter(mBuilder);
     private final Object[] mFormatterArgs = new Object[1];
@@ -288,7 +293,6 @@
     private PhotoController mPhotoControl;
 
     private ZoomRenderer mZoomRenderer;
-
     private String mSceneMode;
     private Toast mNotSelectableToast;
 
@@ -466,7 +470,17 @@
         mCameraStartUpThread = new CameraStartUpThread();
         mCameraStartUpThread.start();
 
-        mActivity.getLayoutInflater().inflate(R.layout.photo_module, (ViewGroup) mRootView);
+        mActivity.getLayoutInflater().inflate(R.layout.photo_module,
+                (ViewGroup) mRootView, true);
+        mRootView.addOnLayoutChangeListener(mLayoutChangeListener);
+        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);
+            }
+        }
 
         // Surface texture is from camera screen nail and startPreview needs it.
         // This must be done before startPreview.
@@ -487,7 +501,6 @@
 
         initializeControlByIntent();
         mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
-        initializeMiscControls();
         mLocationManager = new LocationManager(mActivity, this);
         initOnScreenIndicator();
         mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture));
@@ -557,10 +570,10 @@
 
             if (isImageCaptureIntent()) {
                 if (mReviewCancelButton != null) {
-                    mGestures.addTouchReceiver((View) mReviewCancelButton);
+                    mGestures.addTouchReceiver(mReviewCancelButton);
                 }
                 if (mReviewDoneButton != null) {
-                    mGestures.addTouchReceiver((View) mReviewDoneButton);
+                    mGestures.addTouchReceiver(mReviewDoneButton);
                 }
             }
         }
@@ -585,9 +598,17 @@
         initializePhotoControl();
 
         // These depend on camera parameters.
-        setPreviewFrameLayoutAspectRatio();
-        mFocusManager.setPreviewSize(mPreviewFrameLayout.getWidth(),
-                mPreviewFrameLayout.getHeight());
+        int width = mRootView.getWidth();
+        int height = mRootView.getHeight();
+        mFocusManager.setPreviewSize(width, height);
+        // Full-screen screennail
+        if (Util.getDisplayRotation(mActivity) % 180 == 0) {
+            ((CameraScreenNail) mActivity.mCameraScreenNail).setPreviewFrameLayoutSize(width, height);
+        } else {
+            ((CameraScreenNail) mActivity.mCameraScreenNail).setPreviewFrameLayoutSize(height, width);
+        }
+        // Set touch focus listener.
+        mActivity.setSingleTapUpListener(mRootView);
         loadCameraPreferences();
         initializeZoom();
         updateOnScreenIndicators();
@@ -595,6 +616,21 @@
         onFullScreenChanged(mActivity.isInCameraApp());
     }
 
+    public void onScreenSizeChanged(int width, int height) {
+        // Full-screen screennail
+        int w = width;
+        int h = height;
+        if (Util.getDisplayRotation(mActivity) % 180 != 0) {
+            w = height;
+            h = width;
+        }
+        if (mPreviewWidth != w || mPreviewHeight != h) {
+            Log.d(TAG, "Preview size changed.");
+            if (mFocusManager != null) mFocusManager.setPreviewSize(width, height);
+            ((CameraScreenNail) mActivity.mCameraScreenNail).setPreviewFrameLayoutSize(w, h);
+        }
+    }
+
     private void initializePhotoControl() {
         loadCameraPreferences();
         if (mPhotoControl != null) {
@@ -795,7 +831,7 @@
     }
 
     private void initOnScreenIndicator() {
-        mOnScreenIndicators = mRootView.findViewById(R.id.on_screen_indicators);
+        mOnScreenIndicators = mActivity.findViewById(R.id.on_screen_indicators);
         mExposureIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_exposure_indicator);
         mFlashIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_flash_indicator);
         mSceneIndicator = (ImageView) mOnScreenIndicators.findViewById(R.id.menu_scenemode_indicator);
@@ -1217,11 +1253,6 @@
             }
             return;
         }
-        if (full) {
-            mPreviewSurfaceView.expand();
-        } else {
-            mPreviewSurfaceView.shrink();
-        }
     }
 
     @Override
@@ -1586,10 +1617,9 @@
         mCountDownView.cancelCountDown();
         // Close the camera now because other activities may need to use it.
         closeCamera();
-        if (mSurfaceTexture != null) {
-            ((CameraScreenNail) mActivity.mCameraScreenNail).releaseSurfaceTexture();
-            mSurfaceTexture = null;
-        }
+        // Release surface texture.
+        ((CameraScreenNail) mActivity.mCameraScreenNail).releaseSurfaceTexture();
+        mSurfaceTexture = null;
         resetScreenOn();
 
         // Clear UI.
@@ -1615,6 +1645,9 @@
         mHandler.removeMessages(OPEN_CAMERA_FAIL);
         mHandler.removeMessages(CAMERA_DISABLED);
 
+        mRootView.removeOnLayoutChangeListener(mLayoutChangeListener);
+        mPreviewWidth = 0;
+        mPreviewHeight = 0;
         mPendingSwitchCameraId = -1;
         if (mFocusManager != null) mFocusManager.removeMessages();
         MediaSaveService s = mActivity.getMediaSaveService();
@@ -1624,8 +1657,8 @@
     }
 
     private void initializeControlByIntent() {
-        mBlocker = mRootView.findViewById(R.id.blocker);
-        mMenu = mRootView.findViewById(R.id.menu);
+        mBlocker = mActivity.findViewById(R.id.blocker);
+        mMenu = mActivity.findViewById(R.id.menu);
         mMenu.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -1638,23 +1671,22 @@
             }
         });
         if (mIsImageCaptureIntent) {
-
             mActivity.hideSwitcher();
-            // Cannot use RotateImageView for "done" and "cancel" button because
-            // the tablet layout uses RotateLayout, which cannot be cast to
-            // RotateImageView.
-            mReviewDoneButton = (Rotatable) mRootView.findViewById(R.id.btn_done);
-            mReviewCancelButton = (Rotatable) mRootView.findViewById(R.id.btn_cancel);
-            mReviewRetakeButton = mRootView.findViewById(R.id.btn_retake);
-            ((View) mReviewCancelButton).setVisibility(View.VISIBLE);
+            ViewGroup cameraControls = (ViewGroup) mActivity.findViewById(R.id.camera_controls);
+            mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls);
 
-            ((View) mReviewDoneButton).setOnClickListener(new OnClickListener() {
+            mReviewDoneButton = mActivity.findViewById(R.id.btn_done);
+            mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel);
+            mReviewRetakeButton = mActivity.findViewById(R.id.btn_retake);
+            mReviewCancelButton.setVisibility(View.VISIBLE);
+
+            mReviewDoneButton.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     onReviewDoneClicked(v);
                 }
             });
-            ((View) mReviewCancelButton).setOnClickListener(new OnClickListener() {
+            mReviewCancelButton.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     onReviewCancelClicked(v);
@@ -1668,13 +1700,6 @@
                 }
             });
 
-            // Not grayed out upon disabled, to make the follow-up fade-out
-            // effect look smooth. Note that the review done button in tablet
-            // layout is not a TwoStateImageView.
-            if (mReviewDoneButton instanceof TwoStateImageView) {
-                ((TwoStateImageView) mReviewDoneButton).enableFilter(false);
-            }
-
             setupCaptureParams();
         }
     }
@@ -1701,62 +1726,10 @@
         }
     }
 
-    private void initializeMiscControls() {
-        // startPreview needs this.
-        mPreviewFrameLayout = (PreviewFrameLayout) mRootView.findViewById(R.id.frame);
-        // Set touch focus listener.
-        mActivity.setSingleTapUpListener(mPreviewFrameLayout);
-
-        mFaceView = (FaceView) mRootView.findViewById(R.id.face_view);
-        mPreviewFrameLayout.setOnSizeChangedListener(this);
-        mPreviewFrameLayout.setOnLayoutChangeListener(mActivity);
-        if (!ApiHelper.HAS_SURFACE_TEXTURE) {
-            mPreviewSurfaceView =
-                    (PreviewSurfaceView) mRootView.findViewById(R.id.preview_surface_view);
-            mPreviewSurfaceView.setVisibility(View.VISIBLE);
-            mPreviewSurfaceView.getHolder().addCallback(this);
-        }
-    }
-
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         Log.v(TAG, "onConfigurationChanged");
         setDisplayOrientation();
-
-        // Only the views in photo_module_content need to be removed and recreated
-        // i.e. CountDownView won't be recreated
-        ViewGroup viewGroup = (ViewGroup) mRootView.findViewById(R.id.camera_app);
-        viewGroup.removeAllViews();
-        LayoutInflater inflater = mActivity.getLayoutInflater();
-        inflater.inflate(R.layout.photo_module_content, (ViewGroup) viewGroup);
-
-        // from onCreate()
-        initializeControlByIntent();
-
-        initializeFocusManager();
-        initializeMiscControls();
-        loadCameraPreferences();
-
-        // from initializeFirstTime()
-        mShutterButton = mActivity.getShutterButton();
-        mShutterButton.setOnShutterButtonListener(this);
-        initializeZoom();
-        initOnScreenIndicator();
-        updateOnScreenIndicators();
-        if (mFaceView != null) {
-            mFaceView.clear();
-            mFaceView.setVisibility(View.VISIBLE);
-            mFaceView.setDisplayOrientation(mDisplayOrientation);
-            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
-            mFaceView.setMirror(info.facing == CameraInfo.CAMERA_FACING_FRONT);
-            mFaceView.resume();
-            mFocusManager.setFaceView(mFaceView);
-        }
-        initializeRenderOverlay();
-        onFullScreenChanged(mActivity.isInCameraApp());
-        if (mJpegImageData != null) {  // Jpeg data found, picture has been taken.
-            showPostCaptureAlert();
-        }
     }
 
     @Override
@@ -2259,7 +2232,7 @@
         if (mIsImageCaptureIntent) {
             mOnScreenIndicators.setVisibility(View.GONE);
             mMenu.setVisibility(View.GONE);
-            Util.fadeIn((View) mReviewDoneButton);
+            Util.fadeIn(mReviewDoneButton);
             mShutterButton.setVisibility(View.INVISIBLE);
             Util.fadeIn(mReviewRetakeButton);
         }
@@ -2269,7 +2242,7 @@
         if (mIsImageCaptureIntent) {
             mOnScreenIndicators.setVisibility(View.VISIBLE);
             mMenu.setVisibility(View.VISIBLE);
-            Util.fadeOut((View) mReviewDoneButton);
+            Util.fadeOut(mReviewDoneButton);
             mShutterButton.setVisibility(View.VISIBLE);
             Util.fadeOut(mReviewRetakeButton);
         }
@@ -2285,7 +2258,6 @@
         mLocationManager.recordLocation(recordLocation);
 
         setCameraParametersWhenIdle(UPDATE_PARAM_PREFERENCE);
-        setPreviewFrameLayoutAspectRatio();
         updateOnScreenIndicators();
     }
 
@@ -2432,12 +2404,6 @@
                 Util.FOCUS_MODE_CONTINUOUS_PICTURE);
     }
 
-    // PreviewFrameLayout size has changed.
-    @Override
-    public void onSizeChanged(int width, int height) {
-        if (mFocusManager != null) mFocusManager.setPreviewSize(width, height);
-    }
-
     @Override
     public void onCountDownFinished() {
         mSnapshotOnIdle = false;
@@ -2445,17 +2411,16 @@
         mFocusManager.onShutterUp();
     }
 
-    void setPreviewFrameLayoutAspectRatio() {
-        // Set the preview frame aspect ratio according to the picture size.
-        Size size = mParameters.getPictureSize();
-        mPreviewFrameLayout.setAspectRatio((double) size.width / size.height);
-    }
-
     @Override
     public boolean needsSwitcher() {
         return !mIsImageCaptureIntent;
     }
 
+    @Override
+    public boolean needsPieMenu() {
+        return true;
+    }
+
     public void showPopup(AbstractSettingPopup popup) {
         mActivity.hideUI();
         mBlocker.setVisibility(View.INVISIBLE);
diff --git a/src/com/android/camera/PreviewFrameLayout.java b/src/com/android/camera/PreviewFrameLayout.java
index 87e3c8d..03ef91c 100644
--- a/src/com/android/camera/PreviewFrameLayout.java
+++ b/src/com/android/camera/PreviewFrameLayout.java
@@ -54,15 +54,6 @@
     @Override
     protected void onFinishInflate() {
         mBorder = findViewById(R.id.preview_border);
-        if (ApiHelper.HAS_FACE_DETECTION) {
-            ViewStub faceViewStub = (ViewStub) findViewById(R.id.face_view_stub);
-            /* preview_frame_video.xml does not have face view stub, so we need to
-             * check that.
-             */
-            if (faceViewStub != null) {
-                faceViewStub.inflate();
-            }
-        }
     }
 
     public void setAspectRatio(double ratio) {
diff --git a/src/com/android/camera/ShutterButton.java b/src/com/android/camera/ShutterButton.java
index a1bbb1a..228fc51 100755
--- a/src/com/android/camera/ShutterButton.java
+++ b/src/com/android/camera/ShutterButton.java
@@ -17,11 +17,14 @@
 package com.android.camera;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ImageView;
 
+import com.android.camera.ui.RotatableLayout;
+
 /**
  * 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
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
index 245ef59..7bda657 100644
--- a/src/com/android/camera/VideoModule.java
+++ b/src/com/android/camera/VideoModule.java
@@ -73,7 +73,6 @@
 import com.android.camera.ui.RotateImageView;
 import com.android.camera.ui.RotateLayout;
 import com.android.camera.ui.RotateTextToast;
-import com.android.camera.ui.TwoStateImageView;
 import com.android.camera.ui.ZoomRenderer;
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
@@ -142,14 +141,13 @@
     private SurfaceHolder.Callback mSurfaceViewCallback;
     private PreviewSurfaceView mPreviewSurfaceView;
     private CameraScreenNail.OnFrameDrawnListener mFrameDrawnListener;
-    private View mReviewControl;
 
     // An review image having same size as preview. It is displayed when
     // recording is stopped in capture intent.
     private ImageView mReviewImage;
-    private Rotatable mReviewCancelButton;
-    private Rotatable mReviewDoneButton;
-    private RotateImageView mReviewPlayButton;
+    private View mReviewCancelButton;
+    private View mReviewDoneButton;
+    private View mReviewPlayButton;
     private ShutterButton mShutterButton;
     private TextView mRecordingTimeView;
     private RotateLayout mBgLearningMessageRotater;
@@ -414,13 +412,13 @@
 
         if (isVideoCaptureIntent()) {
             if (mReviewCancelButton != null) {
-                mGestures.addTouchReceiver((View) mReviewCancelButton);
+                mGestures.addTouchReceiver(mReviewCancelButton);
             }
             if (mReviewDoneButton != null) {
-                mGestures.addTouchReceiver((View) mReviewDoneButton);
+                mGestures.addTouchReceiver(mReviewDoneButton);
             }
             if (mReviewPlayButton != null) {
-                mGestures.addTouchReceiver((View) mReviewPlayButton);
+                mGestures.addTouchReceiver(mReviewPlayButton);
             }
         }
     }
@@ -449,7 +447,7 @@
 
         mContentResolver = mActivity.getContentResolver();
 
-        mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView);
+        mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView, true);
 
         // Surface texture is from camera screen nail and startPreview needs it.
         // This must be done before startPreview.
@@ -589,8 +587,7 @@
 
     private void setOrientationIndicator(int orientation, boolean animation) {
         Rotatable[] indicators = {
-                mBgLearningMessageRotater,
-                mReviewDoneButton, mReviewPlayButton};
+                mBgLearningMessageRotater};
         for (Rotatable indicator : indicators) {
             if (indicator != null) indicator.setOrientation(orientation, animation);
         }
@@ -598,14 +595,6 @@
             mGestures.setOrientation(orientation);
         }
 
-        // We change the orientation of the review cancel button only for tablet
-        // UI because there's a label along with the X icon. For phone UI, we
-        // don't change the orientation because there's only a symmetrical X
-        // icon.
-        if (mReviewCancelButton instanceof RotateLayout) {
-            mReviewCancelButton.setOrientation(orientation, animation);
-        }
-
         // We change the orientation of the linearlayout only for phone UI because when in portrait
         // the width is not enough.
         if (mLabelsLinearLayout != null) {
@@ -1636,7 +1625,6 @@
             mActivity.hideSwitcher();
             mRecordingTimeView.setText("");
             mRecordingTimeView.setVisibility(View.VISIBLE);
-            if (mReviewControl != null) mReviewControl.setVisibility(View.GONE);
             // 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.
@@ -1650,7 +1638,6 @@
             mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
             mActivity.showSwitcher();
             mRecordingTimeView.setVisibility(View.GONE);
-            if (mReviewControl != null) mReviewControl.setVisibility(View.VISIBLE);
             if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING
                     && mParameters.isZoomSupported()) {
                 // TODO: enable zoom UI here.
@@ -1679,7 +1666,7 @@
 
         Util.fadeOut(mShutterButton);
 
-        Util.fadeIn((View) mReviewDoneButton);
+        Util.fadeIn(mReviewDoneButton);
         Util.fadeIn(mReviewPlayButton);
         mMenu.setVisibility(View.GONE);
         mOnScreenIndicators.setVisibility(View.GONE);
@@ -1695,7 +1682,7 @@
         mOnScreenIndicators.setVisibility(View.VISIBLE);
         enableCameraControls(true);
 
-        Util.fadeOut((View) mReviewDoneButton);
+        Util.fadeOut(mReviewDoneButton);
         Util.fadeOut(mReviewPlayButton);
 
         Util.fadeIn(mShutterButton);
@@ -2115,8 +2102,8 @@
     }
 
     private void initializeControlByIntent() {
-        mBlocker = mRootView.findViewById(R.id.blocker);
-        mMenu = mRootView.findViewById(R.id.menu);
+        mBlocker = mActivity.findViewById(R.id.blocker);
+        mMenu = mActivity.findViewById(R.id.menu);
         mMenu.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -2125,46 +2112,40 @@
                 }
             }
         });
-        mOnScreenIndicators = mRootView.findViewById(R.id.on_screen_indicators);
-        mFlashIndicator = (ImageView) mRootView.findViewById(R.id.menu_flash_indicator);
+        mOnScreenIndicators = mActivity.findViewById(R.id.on_screen_indicators);
+        mFlashIndicator = (ImageView) mActivity.findViewById(R.id.menu_flash_indicator);
         if (mIsVideoCaptureIntent) {
             mActivity.hideSwitcher();
+            ViewGroup cameraControls = (ViewGroup) mActivity.findViewById(R.id.camera_controls);
+            mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls);
             // Cannot use RotateImageView for "done" and "cancel" button because
             // the tablet layout uses RotateLayout, which cannot be cast to
             // RotateImageView.
-            mReviewDoneButton = (Rotatable) mRootView.findViewById(R.id.btn_done);
-            mReviewCancelButton = (Rotatable) mRootView.findViewById(R.id.btn_cancel);
-            mReviewPlayButton = (RotateImageView) mRootView.findViewById(R.id.btn_play);
+            mReviewDoneButton = mActivity.findViewById(R.id.btn_done);
+            mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel);
+            mReviewPlayButton = mActivity.findViewById(R.id.btn_play);
 
-            ((View) mReviewCancelButton).setVisibility(View.VISIBLE);
+            mReviewCancelButton.setVisibility(View.VISIBLE);
 
-            ((View) mReviewDoneButton).setOnClickListener(new OnClickListener() {
+            mReviewDoneButton.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     onReviewDoneClicked(v);
                 }
             });
-            ((View) mReviewCancelButton).setOnClickListener(new OnClickListener() {
+            mReviewCancelButton.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     onReviewCancelClicked(v);
                 }
             });
 
-            ((View) mReviewPlayButton).setOnClickListener(new OnClickListener() {
+            mReviewPlayButton.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     onReviewPlayClicked(v);
                 }
             });
-
-
-            // Not grayed out upon disabled, to make the follow-up fade-out
-            // effect look smooth. Note that the review done button in tablet
-            // layout is not a TwoStateImageView.
-            if (mReviewDoneButton instanceof TwoStateImageView) {
-                ((TwoStateImageView) mReviewDoneButton).enableFilter(false);
-            }
         }
     }
 
@@ -2203,28 +2184,8 @@
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
+        Log.v(TAG, "onConfigurationChanged");
         setDisplayOrientation();
-        // Change layout in response to configuration change
-        LayoutInflater inflater = mActivity.getLayoutInflater();
-        ((ViewGroup) mRootView).removeAllViews();
-        inflater.inflate(R.layout.video_module, (ViewGroup) mRootView);
-
-        // from onCreate()
-        initializeControlByIntent();
-        initializeOverlay();
-        initializeSurfaceView();
-        initializeMiscControls();
-        showTimeLapseUI(mCaptureTimeLapse);
-        initializeVideoSnapshot();
-
-        // from onResume()
-        showVideoSnapshotUI(false);
-        initializeZoom();
-        onFullScreenChanged(mActivity.isInCameraApp());
-        updateOnScreenIndicators();
-        if (mIsVideoCaptureIntent && mVideoFileDescriptor != null) {
-            showCaptureResult();
-        }
     }
 
     @Override
@@ -2785,6 +2746,11 @@
     }
 
     @Override
+    public boolean needsPieMenu() {
+        return true;
+    }
+
+    @Override
     public void onPieOpened(int centerX, int centerY) {
         mActivity.cancelActivityTouchHandling();
         mActivity.setSwipingEnabled(false);
diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java
index 8d2cd71..ce4f850 100644
--- a/src/com/android/camera/ui/CameraSwitcher.java
+++ b/src/com/android/camera/ui/CameraSwitcher.java
@@ -19,19 +19,23 @@
 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.Gravity;
 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;
 
@@ -119,6 +123,13 @@
                 (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--) {
@@ -129,7 +140,7 @@
             item.setOnClickListener(new OnClickListener() {
                 @Override
                 public void onClick(View v) {
-                    onCameraSelected(index);
+                    if (showsPopup()) onCameraSelected(index);
                 }
             });
             switch (mDrawIds[i]) {
@@ -162,10 +173,14 @@
 
     public boolean isInsidePopup(MotionEvent evt) {
         if (!showsPopup()) return false;
-        return evt.getX() >= mPopup.getLeft()
-                && evt.getX() < mPopup.getRight()
-                && evt.getY() >= mPopup.getTop()
-                && evt.getY() < mPopup.getBottom();
+        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() {
@@ -177,6 +192,16 @@
         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) {
@@ -213,15 +238,22 @@
     }
 
     private void updateInitialTranslations() {
-        if (getResources().getConfiguration().orientation
-                == Configuration.ORIENTATION_PORTRAIT) {
+        int orientation = Util.getDisplayRotation((Activity) getContext());
+        if (orientation == 0) {
             mTranslationX = -getWidth() / 2;
             mTranslationY = getHeight();
-        } else {
+        } else if (orientation == 90) {
             mTranslationX = getWidth();
             mTranslationY = getHeight() / 2;
+        } else if (orientation == 180) {
+            mTranslationX = getWidth();
+            mTranslationY = -getHeight() / 2;
+        } else {
+            mTranslationX = -getWidth();
+            mTranslationY = -getHeight() / 2;
         }
     }
+
     private void popupAnimationSetup() {
         if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
             return;
@@ -243,8 +275,10 @@
                 @Override
                 public void onAnimationEnd(Animator animation) {
                     // Verify that we weren't canceled
-                    if (!showsPopup()) {
+                    if (!showsPopup() && mPopup != null) {
                         mPopup.setVisibility(View.INVISIBLE);
+                        ((ViewGroup) mParent).removeView(mPopup);
+                        mPopup = null;
                     }
                 }
             };
diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java
index 95597b2..b60d9f6 100644
--- a/src/com/android/camera/ui/PieRenderer.java
+++ b/src/com/android/camera/ui/PieRenderer.java
@@ -16,8 +16,6 @@
 
 package com.android.camera.ui;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -37,7 +35,6 @@
 import android.view.animation.Transformation;
 
 import com.android.gallery3d.R;
-import com.android.gallery3d.common.ApiHelper;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -63,6 +60,8 @@
     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;
@@ -117,6 +116,7 @@
     private boolean mOpening;
     private LinearAnimation mXFade;
     private LinearAnimation mFadeIn;
+    private FadeOutAnimation mFadeOut;
     private volatile boolean mFocusCancelled;
 
     private Handler mHandler = new Handler() {
@@ -347,21 +347,35 @@
         return (float) (360 - 180 * angle / Math.PI);
     }
 
-    private void startFadeOut() {
-        if (ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
-            mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    deselect();
-                    show(false);
-                    mOverlay.setAlpha(1);
-                    super.onAnimationEnd(animation);
-                }
-            }).setDuration(PIE_SELECT_FADE_DURATION);
-        } else {
-            deselect();
-            show(false);
+    private void startFadeOut(final PieItem item) {
+        if (mFadeIn != null) {
+            mFadeIn.cancel();
         }
+        if (mXFade != null) {
+            mXFade.cancel();
+        }
+        mFadeOut = new FadeOutAnimation();
+        mFadeOut.setDuration(PIE_FADE_OUT_DURATION);
+        mFadeOut.setAnimationListener(new AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                item.performClick();
+                mFadeOut = null;
+                deselect();
+                show(false);
+                mOverlay.setAlpha(1);
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {
+            }
+        });
+        mFadeOut.startNow();
+        mOverlay.startAnimation(mFadeOut);
     }
 
     @Override
@@ -371,6 +385,8 @@
             alpha = mXFade.getValue();
         } else if (mFadeIn != null) {
             alpha = mFadeIn.getValue();
+        } else if (mFadeOut != null) {
+            alpha = mFadeOut.getValue();
         }
         int state = canvas.save();
         if (mFadeIn != null) {
@@ -390,7 +406,11 @@
         }
         if (mOpenItem != null) {
             for (PieItem inner : mOpenItem.getItems()) {
-                drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+                if (mFadeOut != null) {
+                    drawItem(canvas, inner, alpha);
+                } else {
+                    drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+                }
             }
         }
         canvas.restoreToCount(state);
@@ -404,12 +424,20 @@
                     int state = canvas.save();
                     float r = getDegrees(item.getStartAngle());
                     canvas.rotate(r, mCenter.x, mCenter.y);
+                    if (mFadeOut != null) {
+                        p.setAlpha((int)(255 * alpha));
+                    }
                     canvas.drawPath(item.getPath(), p);
+                    if (mFadeOut != null) {
+                        p.setAlpha(255);
+                    }
                     canvas.restoreToCount(state);
                 }
-                alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
-                // draw the item view
-                item.setAlpha(alpha);
+                if (mFadeOut == null) {
+                    alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
+                    // draw the item view
+                    item.setAlpha(alpha);
+                }
                 item.draw(canvas);
             }
         }
@@ -451,8 +479,7 @@
                     show(false);
                 } else if (!mOpening
                         && !item.hasItems()) {
-                    item.performClick();
-                    startFadeOut();
+                    startFadeOut(item);
                     mTapMode = false;
                 }
                 return true;
@@ -527,6 +554,9 @@
             mCurrentItem.setSelected(false);
             mOpenItem = mCurrentItem;
             mOpening = true;
+            if (mFadeIn != null) {
+                mFadeIn.cancel();
+            }
             mXFade = new LinearAnimation(1, 0);
             mXFade.setDuration(PIE_XFADE_DURATION);
             mXFade.setAnimationListener(new AnimationListener() {
@@ -780,6 +810,26 @@
         }
     }
 
+    private class FadeOutAnimation extends Animation {
+
+        private float mAlpha;
+
+        public float getValue() {
+            return mAlpha;
+        }
+
+        @Override
+        protected void applyTransformation(float interpolatedTime, Transformation t) {
+            if (interpolatedTime < 0.2) {
+                mAlpha = 1;
+            } else if (interpolatedTime < 0.3) {
+                mAlpha = 0;
+            } else {
+                mAlpha = 1 - (interpolatedTime - 0.3f) / 0.7f;
+            }
+        }
+    }
+
     private class ScaleAnimation extends Animation {
         private float mFrom = 1f;
         private float mTo = 1f;
diff --git a/src/com/android/camera/ui/RotatableLayout.java b/src/com/android/camera/ui/RotatableLayout.java
new file mode 100644
index 0000000..4edec5d
--- /dev/null
+++ b/src/com/android/camera/ui/RotatableLayout.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.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";
+    private int mPrevRotation;
+    public RotatableLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public RotatableLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public RotatableLayout(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onFinishInflate() { // get initial orientation
+        mPrevRotation = Util.getDisplayRotation((Activity) getContext());
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        // 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
+        int rotation = Util.getDisplayRotation((Activity) getContext());
+        boolean clockwise = isClockWiseRotation(mPrevRotation, rotation);
+        mPrevRotation = rotation;
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            rotate(child, clockwise);
+        }
+    }
+
+    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);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
index 8fcf400..38865e9 100644
--- a/src/com/android/gallery3d/data/DataManager.java
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -16,13 +16,13 @@
 
 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.ApiHelper;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
 import com.android.gallery3d.data.MediaSet.ItemConsumer;
@@ -65,6 +65,11 @@
     // 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.
diff --git a/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java
index dee9d2e..735803c 100644
--- a/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java
+++ b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java
@@ -1,7 +1,5 @@
 package com.android.gallery3d.filtershow;
 
-import android.content.Context;
-import android.util.Log;
 import android.view.View;
 import android.widget.FrameLayout;
 
@@ -9,7 +7,6 @@
 import com.android.gallery3d.filtershow.editors.Editor;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 
-import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
 import java.util.Vector;
 
@@ -79,4 +76,8 @@
     public void setImageLoader(ImageLoader imageLoader) {
         mImageLoader = imageLoader;
     }
+
+    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
index 7e8a3f5..33fdef5 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -44,28 +44,42 @@
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.WindowManager;
-import android.widget.*;
+import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ShareActionProvider;
 import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+import android.widget.Toast;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.data.LocalAlbum;
 import com.android.gallery3d.filtershow.cache.FilteringPipeline;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
 import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
 import com.android.gallery3d.filtershow.editors.EditorDraw;
+import com.android.gallery3d.filtershow.editors.EditorFlip;
+import com.android.gallery3d.filtershow.editors.EditorInfo;
 import com.android.gallery3d.filtershow.editors.EditorManager;
 import com.android.gallery3d.filtershow.editors.EditorRedEye;
-import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+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.filters.*;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterColorBorderRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.filters.ImageFilterBorder;
+import com.android.gallery3d.filtershow.filters.ImageFilterRS;
+import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 import com.android.gallery3d.filtershow.imageshow.ImageCrop;
-import com.android.gallery3d.filtershow.imageshow.ImageDraw;
-import com.android.gallery3d.filtershow.imageshow.ImageFlip;
-import com.android.gallery3d.filtershow.imageshow.ImageRedEye;
-import com.android.gallery3d.filtershow.imageshow.ImageRotate;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
-import com.android.gallery3d.filtershow.imageshow.ImageStraighten;
 import com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet;
 import com.android.gallery3d.filtershow.imageshow.ImageZoom;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
@@ -98,29 +112,10 @@
     private final PanelController mPanelController = new PanelController();
     private ImageLoader mImageLoader = null;
     private ImageShow mImageShow = null;
-    private ImageDraw mImageDraw = null;
-    private ImageStraighten mImageStraighten = null;
-    private ImageCrop mImageCrop = null;
-    private ImageRotate mImageRotate = null;
-    private ImageFlip mImageFlip = null;
     private ImageTinyPlanet mImageTinyPlanet = null;
 
-    private View mListFx = null;
-    private View mListBorders = null;
-    private View mListGeometry = null;
-    private View mListColors = null;
-    private View mListFilterButtons = null;
     private View mSaveButton = null;
 
-    private ImageButton mFxButton = null;
-    private ImageButton mBorderButton = null;
-    private ImageButton mGeometryButton = null;
-    private ImageButton mColorsButton = null;
-
-    private LinearLayout listColors = null;
-    private LinearLayout listFilters = null;
-    private LinearLayout listBorders = null;
-
     private EditorPlaceHolder mEditorPlaceHolder = new EditorPlaceHolder(this);
 
     private static final int SELECT_PICTURE = 1;
@@ -128,12 +123,11 @@
     protected static final boolean ANIMATE_PANELS = true;
     private static int mImageBorderSize = 4; // in percent
 
+    private boolean mShowingTinyPlanet = false;
     private boolean mShowingHistoryPanel = false;
     private boolean mShowingImageStatePanel = false;
 
     private final Vector<ImageShow> mImageViews = new Vector<ImageShow>();
-    private final Vector<View> mListViews = new Vector<View>();
-    private final Vector<ImageButton> mBottomPanelButtons = new Vector<ImageButton>();
 
     private ShareActionProvider mShareActionProvider;
     private File mSharedOutputFile = null;
@@ -141,7 +135,6 @@
     private boolean mSharingImage = false;
 
     private WeakReference<ProgressDialog> mSavingProgressDialog;
-    private static final int SEEK_BAR_MAX = 600;
 
     private LoadBitmapTask mLoadBitmapTask;
     private FilterIconButton mNullFxFilter;
@@ -152,31 +145,25 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setResources();
 
-        Resources res = getResources();
         setupMasterImage();
-        ImageFilterRS.setRenderScriptContext(this);
+        setDefaultValues();
+        fillEditors();
 
-        ImageShow.setDefaultBackgroundColor(res.getColor(R.color.background_screen));
-        // TODO: get those values from XML.
-        ImageZoom.setZoomedSize(getPixelsFromDip(256));
-        FramedTextButton.setTextSize((int) getPixelsFromDip(14));
-        FramedTextButton.setTrianglePadding((int) getPixelsFromDip(4));
-        FramedTextButton.setTriangleSize((int) getPixelsFromDip(10));
-        ImageShow.setTextSize((int) getPixelsFromDip(12));
-        ImageShow.setTextPadding((int) getPixelsFromDip(10));
-        ImageShow.setOriginalTextMargin((int) getPixelsFromDip(4));
-        ImageShow.setOriginalTextSize((int) getPixelsFromDip(18));
-        ImageShow.setOriginalText(res.getString(R.string.original_picture_text));
-        mIconSeedSize = res.getDimensionPixelSize(R.dimen.thumbnail_size);
+        loadXML();
+        if (getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE) {
+            mShowingImageStatePanel = true;
+        }
 
-        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));
+        setDefaultPreset();
 
+        processIntent();
+    }
+
+    private void loadXML() {
         setContentView(R.layout.filtershow_activity);
+
         ActionBar actionBar = getActionBar();
         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
         actionBar.setCustomView(R.layout.filtershow_actionbar);
@@ -189,123 +176,95 @@
             }
         });
 
-        mImageLoader = new ImageLoader(this, getApplicationContext());
-
-        listFilters = (LinearLayout) findViewById(R.id.listFilters);
-        listBorders = (LinearLayout) findViewById(R.id.listBorders);
-        listColors = (LinearLayout) findViewById(R.id.listColorsFx);
-
         mImageShow = (ImageShow) findViewById(R.id.imageShow);
-        mImageStraighten = (ImageStraighten) findViewById(R.id.imageStraighten);
-        mImageCrop = (ImageCrop) findViewById(R.id.imageCrop);
-        mImageRotate = (ImageRotate) findViewById(R.id.imageRotate);
-        mImageFlip = (ImageFlip) findViewById(R.id.imageFlip);
         mImageTinyPlanet = (ImageTinyPlanet) findViewById(R.id.imageTinyPlanet);
-        mImageDraw = (ImageDraw) findViewById(R.id.imageDraw);
-
-        mImageCrop.setAspectTextSize((int) getPixelsFromDip(18));
-        ImageCrop.setTouchTolerance((int) getPixelsFromDip(25));
-        ImageCrop.setMinCropSize((int) getPixelsFromDip(55));
         mImageViews.add(mImageShow);
-        mImageViews.add(mImageStraighten);
-        mImageViews.add(mImageCrop);
-        mImageViews.add(mImageRotate);
-        mImageViews.add(mImageFlip);
         mImageViews.add(mImageTinyPlanet);
 
-        mEditorPlaceHolder.setContainer((FrameLayout) findViewById(R.id.editorContainer));
-        mEditorPlaceHolder.addEditor(new EditorDraw());
-        mEditorPlaceHolder.addEditor(new BasicEditor());
-        mEditorPlaceHolder.addEditor(new ImageOnlyEditor());
-        mEditorPlaceHolder.addEditor(new EditorTinyPlanet());
-        mEditorPlaceHolder.addEditor(new EditorRedEye());
-        EditorManager.addEditors(mEditorPlaceHolder);
-        mEditorPlaceHolder.setOldViews(mImageViews);
-        mEditorPlaceHolder.setImageLoader(mImageLoader);
+        setupEditors();
 
         mEditorPlaceHolder.hide();
 
-        mListFx = findViewById(R.id.fxList);
-        mListBorders = findViewById(R.id.bordersList);
-        mListGeometry = findViewById(R.id.geometryList);
-        mListFilterButtons = findViewById(R.id.filterButtonsList);
-        mListColors = findViewById(R.id.colorsFxList);
-        mListViews.add(mListFx);
-        mListViews.add(mListBorders);
-        mListViews.add(mListGeometry);
-        mListViews.add(mListFilterButtons);
-        mListViews.add(mListColors);
-
-        mFxButton = (ImageButton) findViewById(R.id.fxButton);
-        mBorderButton = (ImageButton) findViewById(R.id.borderButton);
-        mGeometryButton = (ImageButton) findViewById(R.id.geometryButton);
-        mColorsButton = (ImageButton) findViewById(R.id.colorsButton);
-
-        mBottomPanelButtons.add(mFxButton);
-        mBottomPanelButtons.add(mBorderButton);
-        mBottomPanelButtons.add(mGeometryButton);
-        mBottomPanelButtons.add(mColorsButton);
-
         mImageShow.setImageLoader(mImageLoader);
-        mImageStraighten.setImageLoader(mImageLoader);
-        mImageCrop.setImageLoader(mImageLoader);
-        mImageRotate.setImageLoader(mImageLoader);
-        mImageFlip.setImageLoader(mImageLoader);
         mImageTinyPlanet.setImageLoader(mImageLoader);
-        mImageDraw.setImageLoader(mImageLoader);
 
+        mPanelController.clear();
         mPanelController.setActivity(this);
         mPanelController.setEditorPlaceHolder(mEditorPlaceHolder);
 
         mPanelController.addImageView(findViewById(R.id.imageShow));
-        mPanelController.addImageView(findViewById(R.id.imageStraighten));
-        mPanelController.addImageView(findViewById(R.id.imageCrop));
-        mPanelController.addImageView(findViewById(R.id.imageRotate));
-        mPanelController.addImageView(findViewById(R.id.imageFlip));
         mPanelController.addImageView(findViewById(R.id.imageTinyPlanet));
-        mPanelController.addImageView(findViewById(R.id.imageDraw));
 
-        mPanelController.addPanel(mFxButton, mListFx, 0);
-        mPanelController.addPanel(mBorderButton, mListBorders, 1);
+        mPanelController.addPanel(R.id.fxButton, R.id.fxList, 0);
+        mPanelController.addPanel(R.id.borderButton, R.id.bordersList, 1);
+        mPanelController.addPanel(R.id.geometryButton, R.id.geometryList, 2);
+        mPanelController.addPanel(R.id.colorsButton, R.id.colorsFxList, 3);
 
-        mPanelController.addPanel(mGeometryButton, mListGeometry, 2);
-        mPanelController.addComponent(mGeometryButton, findViewById(R.id.straightenButton));
-        mPanelController.addComponent(mGeometryButton, findViewById(R.id.cropButton));
-        mPanelController.addComponent(mGeometryButton, findViewById(R.id.rotateButton));
-        mPanelController.addComponent(mGeometryButton, findViewById(R.id.flipButton));
-
-        mPanelController.addPanel(mColorsButton, mListColors, 3);
-
-        Vector<FilterRepresentation> filtersRepresentations = new Vector<FilterRepresentation>();
-
-        FiltersManager filtersManager = FiltersManager.getManager();
-        filtersManager.addEffects(filtersRepresentations);
-
-        for (FilterRepresentation representation : filtersRepresentations) {
-            setupFilterRepresentationButton(representation, listColors, mColorsButton);
-        }
+        fillFx((LinearLayout) findViewById(R.id.listFilters), R.id.fxButton);
+        LoadBordersTask loadBorders = new LoadBordersTask((LinearLayout) findViewById(R.id.listBorders));
+        loadBorders.execute();
+        fillGeometry();
+        fillFilters();
 
         mPanelController.addView(findViewById(R.id.applyEffect));
+
         findViewById(R.id.resetOperationsButton).setOnClickListener(
                 createOnClickResetOperationsButton());
 
         ListView operationsList = (ListView) findViewById(R.id.operationsList);
         operationsList.setAdapter(mMasterImage.getHistory());
         operationsList.setOnItemClickListener(this);
+
         ListView imageStateList = (ListView) findViewById(R.id.imageStateList);
         imageStateList.setAdapter(mMasterImage.getState());
         mImageLoader.setAdapter(mMasterImage.getHistory());
 
-        fillListImages(listFilters);
-        LoadBordersTask loadBorders = new LoadBordersTask(listBorders);
-        loadBorders.execute();
-
         mPanelController.setRowPanel(findViewById(R.id.secondRowPanel));
         mPanelController.setUtilityPanel(this, findViewById(R.id.filterButtonsList),
                 findViewById(R.id.panelAccessoryViewList),
                 findViewById(R.id.applyEffect));
 
-        mPanelController.setCurrentPanel(mFxButton);
+        mPanelController.setCurrentPanel(R.id.fxButton);
+    }
+
+    private void fillPanel(Vector<FilterRepresentation> representations, int layoutId, int buttonId) {
+        ImageButton button = (ImageButton) findViewById(buttonId);
+        LinearLayout layout = (LinearLayout) findViewById(layoutId);
+
+        for (FilterRepresentation representation : representations) {
+            setupFilterRepresentationButton(representation, layout, button);
+        }
+    }
+
+    private void fillFilters() {
+        Vector<FilterRepresentation> filtersRepresentations = new Vector<FilterRepresentation>();
+        FiltersManager filtersManager = FiltersManager.getManager();
+        filtersManager.addEffects(filtersRepresentations);
+        fillPanel(filtersRepresentations, R.id.listColorsFx, R.id.colorsButton);
+    }
+
+    private void fillGeometry() {
+        Vector<FilterRepresentation> filtersRepresentations = new Vector<FilterRepresentation>();
+        FiltersManager filtersManager = FiltersManager.getManager();
+
+        GeometryMetadata geo = new GeometryMetadata();
+        int[] editorsId = geo.getEditorIds();
+        for (int i = 0; i < editorsId.length; i++) {
+            int editorId = editorsId[i];
+            GeometryMetadata geometry = new GeometryMetadata(geo);
+            geometry.setEditorId(editorId);
+            EditorInfo editorInfo = (EditorInfo) mEditorPlaceHolder.getEditor(editorId);
+            geometry.setTextId(editorInfo.getTextId());
+            geometry.setOverlayId(editorInfo.getOverlayId());
+            geometry.setOverlayOnly(editorInfo.getOverlayOnly());
+            filtersRepresentations.add(geometry);
+        }
+
+        filtersManager.addTools(filtersRepresentations);
+        fillPanel(filtersRepresentations, R.id.listGeometry, R.id.geometryButton);
+    }
+
+    private void processIntent() {
         Intent intent = getIntent();
         if (intent.getBooleanExtra(LAUNCH_FULLSCREEN, false)) {
             getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -342,16 +301,71 @@
                 }
                 mImageShow.getImagePreset().mGeoData.setCropExtras(mCropExtras);
 
-                mImageCrop.setExtras(mCropExtras);
+                // FIXME: moving to editors breaks the crop action
+                EditorCrop crop = (EditorCrop) mEditorPlaceHolder.getEditor(EditorCrop.ID);
+
+                crop.setExtras(mCropExtras);
                 String s = getString(R.string.Fixed);
-                mImageCrop.setAspectString(s);
-                mImageCrop.setCropActionFlag(true);
+                crop.setAspectString(s);
+                crop.setCropActionFlag(true);
                 mPanelController.setFixedAspect(mCropExtras.getAspectX() > 0
                         && mCropExtras.getAspectY() > 0);
             }
         }
     }
 
+    private void setupEditors() {
+        mEditorPlaceHolder.setContainer((FrameLayout) findViewById(R.id.editorContainer));
+        EditorManager.addEditors(mEditorPlaceHolder);
+        mEditorPlaceHolder.setOldViews(mImageViews);
+        mEditorPlaceHolder.setImageLoader(mImageLoader);
+    }
+
+    private void fillEditors() {
+        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 EditorFlip());
+        mEditorPlaceHolder.addEditor(new EditorRotate());
+        mEditorPlaceHolder.addEditor(new EditorStraighten());
+    }
+
+    private void setDefaultValues() {
+        ImageFilter.setActivityForMemoryToasts(this);
+        ImageFilterRS.setRenderScriptContext(this);
+
+        Resources res = getResources();
+        ImageFilterBorder filterBorder = (ImageFilterBorder) FiltersManager.getManager().getFilter(ImageFilterBorder.class);
+        filterBorder.setResources(res);
+
+        ImageShow.setDefaultBackgroundColor(res.getColor(R.color.background_screen));
+        // TODO: get those values from XML.
+        ImageZoom.setZoomedSize(getPixelsFromDip(256));
+        FramedTextButton.setTextSize((int) getPixelsFromDip(14));
+        FramedTextButton.setTrianglePadding((int) getPixelsFromDip(4));
+        FramedTextButton.setTriangleSize((int) getPixelsFromDip(10));
+        ImageShow.setTextSize((int) getPixelsFromDip(12));
+        ImageShow.setTextPadding((int) getPixelsFromDip(10));
+        ImageShow.setOriginalTextMargin((int) getPixelsFromDip(4));
+        ImageShow.setOriginalTextSize((int) getPixelsFromDip(18));
+        ImageShow.setOriginalText(res.getString(R.string.original_picture_text));
+        mIconSeedSize = res.getDimensionPixelSize(R.dimen.thumbnail_size);
+        // TODO: pick correct value
+        // MasterImage.setIconSeedSize(mIconSeedSize);
+
+        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));
+
+        ImageCrop.setAspectTextSize((int) getPixelsFromDip(18));
+        ImageCrop.setTouchTolerance((int) getPixelsFromDip(25));
+        ImageCrop.setMinCropSize((int) getPixelsFromDip(55));
+    }
+
     private void startLoadBitmap(Uri uri) {
         final View filters = findViewById(R.id.filtersPanel);
         final View loading = findViewById(R.id.loading);
@@ -360,8 +374,9 @@
         filters.setVisibility(View.INVISIBLE);
         loading.setVisibility(View.VISIBLE);
 
-        View tinyPlanetView = findViewById(R.id.tinyplanetButton);
+        View tinyPlanetView = findViewById(EditorTinyPlanet.ID);
         if (tinyPlanetView != null) {
+            mShowingTinyPlanet = false;
             tinyPlanetView.setVisibility(View.GONE);
         }
         mLoadBitmapTask = new LoadBitmapTask(tinyPlanetView);
@@ -406,12 +421,14 @@
                 if (i == 0) {
                     filter.setName(getString(R.string.none));
                 }
-                FilterIconButton b = setupFilterRepresentationButton(filter, mList, mBorderButton);
+                ImageButton borderButton = (ImageButton) findViewById(R.id.borderButton);
+                FilterIconButton b = setupFilterRepresentationButton(filter, mList, borderButton);
                 if (i == 0) {
                     mNullBorderFilter = b;
                     mNullBorderFilter.setSelected(true);
                 }
             }
+            fillButtonIcons();
         }
     }
 
@@ -440,6 +457,7 @@
                 return;
             }
             if (values[0]) {
+                mShowingTinyPlanet = true;
                 mTinyPlanetButton.setVisibility(View.VISIBLE);
             }
         }
@@ -473,41 +491,14 @@
             float previewScale = (float) largeBitmap.getWidth() / (float) mImageLoader.getOriginalBounds().width();
             pipeline.setPreviewScaleFactor(previewScale);
 
-            Bitmap bmap = mImageLoader.getOriginalBitmapSmall();
-            if (bmap != null && bmap.getWidth() > 0 && bmap.getHeight() > 0) {
-                float w = bmap.getWidth();
-                float h = bmap.getHeight();
-                float f = mIconSeedSize / Math.min(w, h);
-                w = w * f;
-                h = h * f;
-                bmap = Bitmap.createScaledBitmap(bmap, (int) w, (int) h, true);
-
-                int num_colors_buttons = listColors.getChildCount();
-                for (int i = 0; i < num_colors_buttons; i++) {
-                    FilterIconButton b = (FilterIconButton) listColors.getChildAt(i);
-
-                    b.setIcon(bmap);
-                }
-                int num_filters_buttons = listFilters.getChildCount();
-                for (int i = 0; i < num_filters_buttons; i++) {
-                    FilterIconButton b = (FilterIconButton) listFilters.getChildAt(i);
-
-                    b.setIcon(bmap);
-                }
-                int num_borders_buttons = listBorders.getChildCount();
-                for (int i = 0; i < num_borders_buttons; i++) {
-                    FilterIconButton b = (FilterIconButton) listBorders.getChildAt(i);
-
-                    b.setIcon(bmap);
-                }
-
-            }
+            fillButtonIcons();
+            MasterImage.getImage().setOriginalGeometry(largeBitmap);
             mLoadBitmapTask = null;
 
             if (mAction == CROP_ACTION) {
-                mPanelController.showComponent(findViewById(R.id.cropButton));
+                mPanelController.showComponent(findViewById(EditorCrop.ID));
             } else if (mAction == TINY_PLANET_ACTION) {
-                mPanelController.showComponent(findViewById(R.id.tinyplanetButton));
+                mPanelController.showComponent(findViewById(EditorTinyPlanet.ID));
             }
 
             super.onPostExecute(result);
@@ -515,11 +506,51 @@
 
     }
 
+    private void fillButtonIcons() {
+        Bitmap bmap = mImageLoader.getOriginalBitmapSmall();
+        if (bmap != null && bmap.getWidth() > 0 && bmap.getHeight() > 0) {
+            float w = bmap.getWidth();
+            float h = bmap.getHeight();
+            float f = mIconSeedSize / Math.min(w, h);
+            w = w * f;
+            h = h * f;
+            bmap = Bitmap.createScaledBitmap(bmap, (int) w, (int) h, true);
+
+            LinearLayout listColors = (LinearLayout) findViewById(R.id.listColorsFx);
+            int num_colors_buttons = listColors.getChildCount();
+            for (int i = 0; i < num_colors_buttons; i++) {
+                FilterIconButton b = (FilterIconButton) listColors.getChildAt(i);
+                b.setIcon(bmap);
+            }
+
+            LinearLayout listFilters = (LinearLayout) findViewById(R.id.listFilters);
+            int num_filters_buttons = listFilters.getChildCount();
+            for (int i = 0; i < num_filters_buttons; i++) {
+                FilterIconButton b = (FilterIconButton) listFilters.getChildAt(i);
+                b.setIcon(bmap);
+            }
+
+            LinearLayout listBorders = (LinearLayout) findViewById(R.id.listBorders);
+            int num_borders_buttons = listBorders.getChildCount();
+            for (int i = 0; i < num_borders_buttons; i++) {
+                FilterIconButton b = (FilterIconButton) listBorders.getChildAt(i);
+                b.setIcon(bmap);
+            }
+
+        }
+    }
+
     @Override
     protected void onDestroy() {
         if (mLoadBitmapTask != null) {
             mLoadBitmapTask.cancel(false);
         }
+        // TODO:  Using singletons is a bad design choice for many of these
+        // due static reference leaks and in general.  Please refactor.
+        MasterImage.reset();
+        FilteringPipeline.reset();
+        ImageFilter.resetStatics();
+        FiltersManager.reset();
         super.onDestroy();
     }
 
@@ -669,6 +700,7 @@
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.undoButton: {
+                mPanelController.resetParameters();
                 HistoryAdapter adapter = mMasterImage.getHistory();
                 int position = adapter.undo();
                 mMasterImage.onHistoryItemClick(position);
@@ -720,16 +752,13 @@
         String text = representation.getName();
         icon.setup(text, mPanelController, panel);
         icon.setFilterRepresentation(representation);
-        if (representation instanceof FilterTinyPlanetRepresentation) {
-            // needed to hide tinyplanet on startup
-            icon.setId(R.id.tinyplanetButton);
-        }
+        icon.setId(representation.getEditorId());
         mPanelController.addComponent(button, icon);
         panel.addView(icon);
         return icon;
     }
 
-    private void fillListImages(LinearLayout listFilters) {
+    private void fillFx(LinearLayout listFilters, int buttonId) {
         // TODO: use listview
         // TODO: load the filters straight from the filesystem
 
@@ -760,9 +789,6 @@
                 R.string.ffx_x_process
         };
 
-        ImagePreset preset = new ImagePreset(getString(R.string.history_original)); // empty
-        preset.setImageLoader(mImageLoader);
-
         BitmapFactory.Options o = new BitmapFactory.Options();
         o.inScaled = false;
 
@@ -773,21 +799,28 @@
             fxArray[p++] = fx;
         }
 
+        ImageButton button = (ImageButton) findViewById(buttonId);
+
         FilterFxRepresentation nullFx = new FilterFxRepresentation(getString(R.string.none), 0, R.string.none);
-        mNullFxFilter = setupFilterRepresentationButton(nullFx, listFilters, mFxButton);
+        mNullFxFilter = setupFilterRepresentationButton(nullFx, listFilters, button);
         mNullFxFilter.setSelected(true);
 
         Vector<FilterRepresentation> filtersRepresentations = new Vector<FilterRepresentation>();
         FiltersManager.getManager().addLooks(filtersRepresentations);
         for (FilterRepresentation representation : filtersRepresentations) {
-            setupFilterRepresentationButton(representation, listFilters, mFxButton);
+            setupFilterRepresentationButton(representation, listFilters, button);
         }
 
         for (int i = 0; i < p; i++) {
-            setupFilterRepresentationButton(fxArray[i], listFilters, mFxButton);
+            setupFilterRepresentationButton(fxArray[i], listFilters, button);
         }
+    }
 
+    public void setDefaultPreset() {
         // Default preset (original)
+        ImagePreset preset = new ImagePreset(getString(R.string.history_original)); // empty
+        preset.setImageLoader(mImageLoader);
+
         mMasterImage.setPreset(preset, true);
     }
 
@@ -795,12 +828,6 @@
     // Some utility functions
     // TODO: finish the cleanup.
 
-    public void showOriginalViews(boolean value) {
-        for (ImageShow views : mImageViews) {
-            views.showOriginal(value);
-        }
-    }
-
     public void invalidateViews() {
         for (ImageShow views : mImageViews) {
             views.invalidate();
@@ -808,12 +835,6 @@
         }
     }
 
-    public void hideListViews() {
-        for (View view : mListViews) {
-            view.setVisibility(View.GONE);
-        }
-    }
-
     public void hideImageViews() {
         for (View view : mImageViews) {
             view.setVisibility(View.GONE);
@@ -821,34 +842,6 @@
         mEditorPlaceHolder.hide();
     }
 
-    public void unselectBottomPanelButtons() {
-        for (ImageButton button : mBottomPanelButtons) {
-            button.setSelected(false);
-        }
-    }
-
-    public void unselectPanelButtons(Vector<ImageButton> buttons) {
-        for (ImageButton button : buttons) {
-            button.setSelected(false);
-        }
-    }
-
-    public void disableFilterButtons() {
-        for (ImageButton b : mBottomPanelButtons) {
-            b.setEnabled(false);
-            b.setClickable(false);
-            b.setAlpha(0.4f);
-        }
-    }
-
-    public void enableFilterButtons() {
-        for (ImageButton b : mBottomPanelButtons) {
-            b.setEnabled(true);
-            b.setClickable(true);
-            b.setAlpha(1.0f);
-        }
-    }
-
     // //////////////////////////////////////////////////////////////////////////////
     // imageState panel...
 
@@ -857,44 +850,19 @@
     }
 
     private void toggleImageStatePanel() {
-        final View view = findViewById(R.id.mainPanel);
         final View viewList = findViewById(R.id.imageStatePanel);
 
         if (mShowingHistoryPanel) {
-            findViewById(R.id.historyPanel).setVisibility(View.INVISIBLE);
+            findViewById(R.id.historyPanel).setVisibility(View.GONE);
             mShowingHistoryPanel = false;
         }
 
-        int translate = translateMainPanel(viewList);
         if (!mShowingImageStatePanel) {
             mShowingImageStatePanel = true;
-            if (PanelController.useAnimations()) {
-                view.animate().setDuration(200).x(translate)
-                    .withLayer().withEndAction(new Runnable() {
-                        @Override
-                        public void run() {
-                            viewList.setAlpha(0);
-                            viewList.setVisibility(View.VISIBLE);
-                            viewList.animate().setDuration(100)
-                                    .alpha(1.0f).start();
-                        }
-                    }).start();
-            } else {
-                view.setX(translate);
-                viewList.setAlpha(0);
-                viewList.setVisibility(View.VISIBLE);
-                viewList.animate().setDuration(100)
-                        .alpha(1.0f).start();
-            }
+            viewList.setVisibility(View.VISIBLE);
         } else {
             mShowingImageStatePanel = false;
-            viewList.setVisibility(View.INVISIBLE);
-            if (PanelController.useAnimations()) {
-                view.animate().setDuration(200).x(0).withLayer()
-                    .start();
-            } else {
-                view.setX(0);
-            }
+            viewList.setVisibility(View.GONE);
         }
         invalidateOptionsMenu();
     }
@@ -903,13 +871,28 @@
     public void onConfigurationChanged(Configuration newConfig)
     {
         super.onConfigurationChanged(newConfig);
-        setResources();
+        setDefaultValues();
+        loadXML();
+        if (getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE) {
+            mShowingImageStatePanel = true;
+        }
         if (mShowingHistoryPanel) {
             toggleHistoryPanel();
         }
+        if (mShowingTinyPlanet == false) {
+            View tinyPlanetView = findViewById(EditorTinyPlanet.ID);
+            if (tinyPlanetView != null) {
+                tinyPlanetView.setVisibility(View.GONE);
+            }
+        }
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.GONE);
     }
 
     public void setupMasterImage() {
+        mImageLoader = new ImageLoader(this, getApplicationContext());
+
         HistoryAdapter mHistoryAdapter = new HistoryAdapter(
                 this, R.layout.filtershow_history_operation_row,
                 R.id.rowTextView);
@@ -931,16 +914,19 @@
         final View viewList = findViewById(R.id.historyPanel);
 
         if (mShowingImageStatePanel) {
-            findViewById(R.id.imageStatePanel).setVisibility(View.INVISIBLE);
-            mShowingImageStatePanel = false;
+            findViewById(R.id.imageStatePanel).setVisibility(View.GONE);
         }
 
         int translate = translateMainPanel(viewList);
         if (!mShowingHistoryPanel) {
             mShowingHistoryPanel = true;
-            if (PanelController.useAnimations()) {
-                view.animate().setDuration(200).x(translate)
-                    .withLayer().withEndAction(new Runnable() {
+            if (getResources().getConfiguration().orientation
+                    == Configuration.ORIENTATION_PORTRAIT) {
+                // If portrait, always remove the state panel
+                mShowingImageStatePanel = false;
+                if (PanelController.useAnimations()) {
+                    view.animate().setDuration(200).x(translate)
+                            .withLayer().withEndAction(new Runnable() {
                         @Override
                         public void run() {
                             viewList.setAlpha(0);
@@ -949,21 +935,35 @@
                                     .alpha(1.0f).start();
                         }
                     }).start();
+                } else {
+                    view.setX(translate);
+                    viewList.setAlpha(0);
+                    viewList.setVisibility(View.VISIBLE);
+                    viewList.animate().setDuration(100)
+                            .alpha(1.0f).start();
+                }
             } else {
-                view.setX(translate);
-                viewList.setAlpha(0);
+                findViewById(R.id.filtersPanel).setVisibility(View.GONE);
                 viewList.setVisibility(View.VISIBLE);
-                viewList.animate().setDuration(100)
-                        .alpha(1.0f).start();
             }
         } else {
             mShowingHistoryPanel = false;
-            viewList.setVisibility(View.INVISIBLE);
-            if (PanelController.useAnimations()) {
-                view.animate().setDuration(200).x(0).withLayer()
-                    .start();
+            if (getResources().getConfiguration().orientation
+                    == Configuration.ORIENTATION_PORTRAIT) {
+                viewList.setVisibility(View.INVISIBLE);
+                if (PanelController.useAnimations()) {
+                    view.animate().setDuration(200).x(0).withLayer()
+                            .start();
+                } else {
+                    view.setX(0);
+                }
             } else {
-                view.setX(0);
+                viewList.setVisibility(View.GONE);
+                findViewById(R.id.filtersPanel).setVisibility(View.VISIBLE);
+                // In landscape, bring back the state panel if it was there
+                if (mShowingImageStatePanel) {
+                    findViewById(R.id.imageStatePanel).setVisibility(View.VISIBLE);
+                }
             }
         }
         invalidateOptionsMenu();
@@ -1001,6 +1001,10 @@
         }
     }
 
+    public PanelController getPanelController() {
+        return mPanelController;
+    }
+
     public void cannotLoadImage() {
         CharSequence text = getString(R.string.cannot_load_image);
         Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
@@ -1142,11 +1146,6 @@
         finish();
     }
 
-    private void setResources() {
-        ImageFilterBorder filterBorder = (ImageFilterBorder) FiltersManager.getManager().getFilter(ImageFilterBorder.class);
-        filterBorder.setResources(getResources());
-    }
-
     static {
         System.loadLibrary("jni_filtershow_filters");
     }
diff --git a/src/com/android/gallery3d/filtershow/HistoryAdapter.java b/src/com/android/gallery3d/filtershow/HistoryAdapter.java
index 057ab38..a10e66b 100644
--- a/src/com/android/gallery3d/filtershow/HistoryAdapter.java
+++ b/src/com/android/gallery3d/filtershow/HistoryAdapter.java
@@ -17,7 +17,8 @@
 package com.android.gallery3d.filtershow;
 
 import android.content.Context;
-import android.util.Log;
+import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.view.LayoutInflater;
 import android.view.MenuItem;
 import android.view.View;
@@ -196,30 +197,15 @@
             if (itemView != null) {
                 itemView.setText(item.historyName());
             }
-            ImageView markView = (ImageView) view.findViewById(R.id.selectedMark);
-            if (position == mCurrentPresetPosition) {
-                markView.setVisibility(View.VISIBLE);
-            } else {
-                markView.setVisibility(View.INVISIBLE);
+            ImageView preview = (ImageView) view.findViewById(R.id.preview);
+            Bitmap bmp = item.getPreviewImage();
+            if (bmp != null) {
+                preview.setImageBitmap(bmp);
             }
-            ImageView typeView = (ImageView) view.findViewById(R.id.typeMark);
-            // TODO: use type of last filter, not a string, to discriminate.
-            if (position == getCount() - 1) {
-                typeView.setImageResource(R.drawable.ic_photoeditor_effects);
-            } else if (item.historyName().equalsIgnoreCase(mBorders)) {
-                typeView.setImageResource(R.drawable.ic_photoeditor_border);
-            } else if (item.historyName().equalsIgnoreCase(mStraighten)) {
-                typeView.setImageResource(R.drawable.ic_photoeditor_fix);
-            } else if (item.historyName().equalsIgnoreCase(mCrop)) {
-                typeView.setImageResource(R.drawable.ic_photoeditor_fix);
-            } else if (item.historyName().equalsIgnoreCase(mRotate)) {
-                typeView.setImageResource(R.drawable.ic_photoeditor_fix);
-            } else if (item.historyName().equalsIgnoreCase(mMirror)) {
-                typeView.setImageResource(R.drawable.ic_photoeditor_fix);
-            } else if (item.isFx()) {
-                typeView.setImageResource(R.drawable.ic_photoeditor_effects);
+            if (position == mCurrentPresetPosition) {
+                view.setBackgroundColor(Color.WHITE);
             } else {
-                typeView.setImageResource(R.drawable.ic_photoeditor_color);
+                view.setBackgroundResource(R.drawable.filtershow_button_background);
             }
         }
 
diff --git a/src/com/android/gallery3d/filtershow/ImageStateAdapter.java b/src/com/android/gallery3d/filtershow/ImageStateAdapter.java
index 58e0035..62633e2 100644
--- a/src/com/android/gallery3d/filtershow/ImageStateAdapter.java
+++ b/src/com/android/gallery3d/filtershow/ImageStateAdapter.java
@@ -21,11 +21,13 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
 
 public class ImageStateAdapter extends ArrayAdapter<FilterRepresentation> {
     private static final String LOGTAG = "ImageStateAdapter";
@@ -36,13 +38,20 @@
 
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
-        View view = convertView;
+        MovableLinearLayout view = (MovableLinearLayout) convertView;
         if (view == null) {
             LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
                     Context.LAYOUT_INFLATER_SERVICE);
-            view = inflater.inflate(R.layout.filtershow_imagestate_row, null);
+            view = (MovableLinearLayout) inflater.inflate(R.layout.filtershow_imagestate_row, null);
         }
         FilterRepresentation filter = getItem(position);
+        view.setFilterRepresentation(filter);
+        ImageView markView = (ImageView) view.findViewById(R.id.selectedMark);
+        if (filter == MasterImage.getImage().getCurrentFilterRepresentation()) {
+                markView.setVisibility(View.VISIBLE);
+        } else {
+            markView.setVisibility(View.INVISIBLE);
+        }
         if (filter != null) {
             TextView itemLabel = (TextView) view.findViewById(R.id.imagestate_label);
             itemLabel.setText(filter.getName());
diff --git a/src/com/android/gallery3d/filtershow/MovableLinearLayout.java b/src/com/android/gallery3d/filtershow/MovableLinearLayout.java
new file mode 100644
index 0000000..9eddb41
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/MovableLinearLayout.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;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class MovableLinearLayout extends LinearLayout {
+
+    private Point mTouchDown = new Point();
+    private FilterRepresentation mFilterRepresentation;
+    private int mTouchSlope = 3;
+    private static final String LOGTAG = "MovableLinearLayout";
+
+    public MovableLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    private void resetView() {
+        setTranslationX(0);
+        mTouchDown.x = 0;
+        mTouchDown.y = 0;
+        setAlpha(1.0f);
+        setBackgroundResource(R.drawable.filtershow_button_background);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int ex = (int) event.getX();
+        int ey = (int) event.getY();
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            mTouchDown.x = ex;
+            mTouchDown.y = ey;
+            FilterShowActivity activity = (FilterShowActivity) getContext();
+            activity.getPanelController().showComponentWithRepresentation(mFilterRepresentation);
+        }
+        if (event.getAction() == MotionEvent.ACTION_MOVE) {
+            int delta = ex - mTouchDown.x;
+            if (delta > 0 && (delta - getTranslationX()) > mTouchSlope) {
+                setTranslationX(delta);
+                float alpha = (getWidth() - getTranslationX()) / getWidth();
+                int backgroundColor = Color.argb((int) (1.0f - alpha * 255), 255, 0, 0);
+                setBackgroundColor(backgroundColor);
+                setAlpha(alpha);
+            }
+        }
+        if (event.getAction() == MotionEvent.ACTION_UP
+                || event.getAction() == MotionEvent.ACTION_CANCEL) {
+            if (getTranslationX() > getWidth() / 4) {
+                delete(mFilterRepresentation);
+            } else {
+                resetView();
+            }
+        }
+        return true;
+    }
+
+    private void delete(FilterRepresentation filterRepresentation) {
+        FilterShowActivity activity = (FilterShowActivity) getContext();
+        activity.getPanelController().removeFilterRepresentation(filterRepresentation);
+    }
+
+    public void setFilterRepresentation(FilterRepresentation filterRepresentation) {
+        mFilterRepresentation = filterRepresentation;
+        resetView();
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/PanelController.java b/src/com/android/gallery3d/filtershow/PanelController.java
index 5bda246..3c4470a 100644
--- a/src/com/android/gallery3d/filtershow/PanelController.java
+++ b/src/com/android/gallery3d/filtershow/PanelController.java
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.filtershow;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.text.Html;
 import android.view.View;
@@ -25,18 +24,21 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import android.util.Log;
+
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
 import com.android.gallery3d.filtershow.filters.ImageFilterTinyPlanet;
-import com.android.gallery3d.filtershow.imageshow.ImageCrop;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.filtershow.ui.FilterIconButton;
 
 import java.util.HashMap;
+import java.util.Set;
 import java.util.Vector;
 
 public class PanelController implements OnClickListener {
@@ -46,7 +48,6 @@
     private static int HORIZONTAL_MOVE = 1;
     private static final int ANIM_DURATION = 200;
     private static final String LOGTAG = "PanelController";
-    private boolean mDisableFilterButtons = false;
     private boolean mFixedAspect = false;
 
     public static boolean useAnimations() {
@@ -156,7 +157,6 @@
         private String mEffectName = null;
         private int mParameterValue = 0;
         private boolean mShowParameterValue = false;
-        boolean firstTimeCropDisplayed = true;
 
         public UtilityPanel(Context context, View view, View accessoryViewList,
                 View textView) {
@@ -267,6 +267,13 @@
     private FilterShowActivity mActivity = null;
     private EditorPlaceHolder mEditorPlaceHolder = null;
 
+    public void clear() {
+        mPanels.clear();
+        mViews.clear();
+        mFilters.clear();
+        mImageViews.clear();
+    }
+
     public void setActivity(FilterShowActivity activity) {
         mActivity = activity;
     }
@@ -276,7 +283,18 @@
         mViews.put(view, new ViewType(view, COMPONENT));
     }
 
-    public void addPanel(View view, View container, int position) {
+    public View getViewFromId(int viewId) {
+        for (View view : mPanels.keySet()) {
+            if (view.getId() == viewId) {
+                return view;
+            }
+        }
+        return null;
+    }
+
+    public void addPanel(int viewId, int containerId, int position) {
+        View view = mActivity.findViewById(viewId);
+        View container = mActivity.findViewById(containerId);
         mPanels.put(view, new Panel(view, container, position));
         view.setOnClickListener(this);
         mViews.put(view, new ViewType(view, PANEL));
@@ -312,10 +330,6 @@
             }
 
         }
-        if (mDisableFilterButtons) {
-            mActivity.enableFilterButtons();
-            mDisableFilterButtons = false;
-        }
     }
 
     public boolean onBackPressed() {
@@ -330,12 +344,6 @@
         if (mCurrentEditor != null) {
             mCurrentEditor.reflectCurrentFilter();
         }
-
-        if (mDisableFilterButtons) {
-            mActivity.enableFilterButtons();
-            mActivity.resetHistory();
-            mDisableFilterButtons = false;
-        }
         return false;
     }
 
@@ -347,8 +355,8 @@
         mUtilityPanel.setShowParameter(s);
     }
 
-    public void setCurrentPanel(View panel) {
-        showPanel(panel);
+    public void setCurrentPanel(int panelId) {
+        showPanel(getViewFromId(panelId));
     }
 
     public void setRowPanel(View rowPanel) {
@@ -447,6 +455,34 @@
         return MasterImage.getImage().getPreset();
     }
 
+    public void setEffectName(String ename) {
+        mUtilityPanel.setEffectName(ename);
+    }
+
+    public void removeFilterRepresentation(FilterRepresentation filterRepresentation) {
+        if (filterRepresentation == null) {
+            Log.v(LOGTAG, "RemoveFilterRepresentation: " + filterRepresentation);
+            return;
+        }
+        ImagePreset oldPreset = MasterImage.getImage().getPreset();
+        ImagePreset copy = new ImagePreset(oldPreset);
+        copy.removeFilter(filterRepresentation);
+        MasterImage.getImage().setPreset(copy, true);
+        if (MasterImage.getImage().getCurrentFilterRepresentation() == filterRepresentation) {
+            FilterRepresentation lastRepresentation = copy.getLastRepresentation();
+            MasterImage.getImage().setCurrentFilterRepresentation(lastRepresentation);
+        }
+        // Now let's reset the panel
+        if (mUtilityPanel == null || !mUtilityPanel.selected()) {
+            return;
+        }
+        showPanel(mCurrentPanel);
+        mCurrentImage.select();
+        if (mCurrentEditor != null) {
+            mCurrentEditor.reflectCurrentFilter();
+        }
+    }
+
     public void useFilterRepresentation(FilterRepresentation filterRepresentation) {
         if (filterRepresentation == null) {
             return;
@@ -470,6 +506,23 @@
         MasterImage.getImage().setCurrentFilterRepresentation(filterRepresentation);
     }
 
+    public void showComponentWithRepresentation(FilterRepresentation filterRepresentation) {
+        if (filterRepresentation == null) {
+            return;
+        }
+        Set<View> views = mViews.keySet();
+        for (View view : views) {
+            if (view instanceof FilterIconButton) {
+                FilterIconButton button = (FilterIconButton) view;
+                if (button.getFilterRepresentation().getFilterClass() == filterRepresentation.getFilterClass()) {
+                    MasterImage.getImage().setCurrentFilterRepresentation(filterRepresentation);
+                    showComponent(view);
+                    return;
+                }
+            }
+        }
+    }
+
     public void showComponent(View view) {
 
         boolean doPanelTransition = true;
@@ -532,59 +585,21 @@
             return;
         }
 
-        switch (view.getId()) {
-            case R.id.tinyplanetButton: {
-                mCurrentImage = showImageView(R.id.imageTinyPlanet);
-                String ename = mCurrentImage.getContext().getString(R.string.tinyplanet);
-                mUtilityPanel.setEffectName(ename);
-                if (!mDisableFilterButtons) {
-                    mActivity.disableFilterButtons();
-                    mDisableFilterButtons = true;
-                }
-                break;
-            }
-            case R.id.straightenButton: {
-                mCurrentImage = showImageView(R.id.imageStraighten);
-                String ename = mCurrentImage.getContext().getString(R.string.straighten);
-                mUtilityPanel.setEffectName(ename);
-                break;
-            }
-            case R.id.cropButton: {
-                mCurrentImage = showImageView(R.id.imageCrop);
-                String ename = mCurrentImage.getContext().getString(R.string.crop);
-                mUtilityPanel.setEffectName(ename);
-                mUtilityPanel.setShowParameter(false);
-                if (mCurrentImage instanceof ImageCrop && mUtilityPanel.firstTimeCropDisplayed) {
-                    ((ImageCrop) mCurrentImage).clear();
-                    mUtilityPanel.firstTimeCropDisplayed = false;
-                    ((ImageCrop) mCurrentImage).setFixedAspect(mFixedAspect);
-                }
-                break;
-            }
-            case R.id.rotateButton: {
-                mCurrentImage = showImageView(R.id.imageRotate);
-                String ename = mCurrentImage.getContext().getString(R.string.rotate);
-                mUtilityPanel.setEffectName(ename);
-                break;
-            }
-            case R.id.flipButton: {
-                mCurrentImage = showImageView(R.id.imageFlip);
-                String ename = mCurrentImage.getContext().getString(R.string.mirror);
-                mUtilityPanel.setEffectName(ename);
-                mUtilityPanel.setShowParameter(false);
-                break;
-            }
-            case R.id.applyEffect: {
+        int id = view.getId();
+        if (id == EditorTinyPlanet.ID) {
+            mCurrentImage = showImageView(R.id.imageTinyPlanet);
+            String ename = mCurrentImage.getContext().getString(R.string.tinyplanet);
+            mUtilityPanel.setEffectName(ename);
+
+        } else {
+            if (id == R.id.applyEffect) {
                 if (MasterImage.getImage().getCurrentFilter() instanceof ImageFilterTinyPlanet) {
                     mActivity.saveImage();
                 } else {
-                    if (mCurrentImage instanceof ImageCrop) {
-                        ((ImageCrop) mCurrentImage).saveAndSetPreset();
-                    }
                     showPanel(mCurrentPanel);
                 }
                 MasterImage.getImage().invalidateFiltersOnly();
-                break;
+
             }
         }
         mCurrentImage.select();
diff --git a/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java b/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java
index 7d5b529..17c19eb 100644
--- a/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java
+++ b/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java
@@ -22,7 +22,6 @@
 import android.support.v8.renderscript.*;
 import android.util.Log;
 
-import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
 import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.filters.ImageFilterRS;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
@@ -31,7 +30,7 @@
 
 public class FilteringPipeline implements Handler.Callback {
 
-    private final static FilteringPipeline gPipeline = new FilteringPipeline();
+    private static FilteringPipeline sPipeline;
     private static final String LOGTAG = "FilteringPipeline";
     private ImagePreset mPreviousGeometryPreset = null;
     private ImagePreset mPreviousFiltersPreset = null;
@@ -117,7 +116,10 @@
     }
 
     public static FilteringPipeline getPipeline() {
-        return gPipeline;
+        if (sPipeline == null) {
+            sPipeline = new FilteringPipeline();
+        }
+        return sPipeline;
     }
 
     public synchronized void setOriginal(Bitmap bitmap) {
@@ -247,11 +249,11 @@
         setPresetParameters(preset);
 
         if (request.getType() == RenderingRequest.PARTIAL_RENDERING) {
-            bitmap = MasterImage.getImage().getImageLoader().getScaleOneImageForPreset(null, preset, request.getBounds(), request.getDestination(), false);
+            bitmap = MasterImage.getImage().getImageLoader().getScaleOneImageForPreset(null, preset,
+                    request.getBounds(), request.getDestination(), false);
             if (bitmap == null) {
                 return;
             }
-            bitmap = preset.applyGeometry(bitmap);
         }
 
         if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
@@ -337,4 +339,8 @@
     public float getPreviewScaleFactor() {
         return mPreviewScaleFactor;
     }
+
+    public static void reset() {
+        sPipeline = null;
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
index df4f3fd..a8191cf 100644
--- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -18,16 +18,15 @@
 
 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.Bitmap.CompressFormat;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
 import android.graphics.Matrix;
 import android.graphics.Rect;
-import android.graphics.Bitmap.CompressFormat;
 import android.media.ExifInterface;
 import android.net.Uri;
 import android.provider.MediaStore;
@@ -35,16 +34,13 @@
 
 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.ExifInvalidFormatException;
 import com.android.gallery3d.exif.ExifParser;
 import com.android.gallery3d.exif.ExifTag;
-import com.android.gallery3d.filtershow.CropExtras;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.HistoryAdapter;
-import com.android.gallery3d.filtershow.imageshow.ImageCrop;
 import com.android.gallery3d.filtershow.imageshow.ImageShow;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 import com.android.gallery3d.filtershow.tools.BitmapTask;
@@ -52,7 +48,6 @@
 import com.android.gallery3d.util.InterruptableOutputStream;
 import com.android.gallery3d.util.XmpUtilHelper;
 
-import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -62,6 +57,8 @@
 import java.util.Vector;
 import java.util.concurrent.locks.ReentrantLock;
 
+
+// TODO: this class has waaaay to much bitmap copying.  Cleanup.
 public class ImageLoader {
 
     private static final String LOGTAG = "ImageLoader";
@@ -313,6 +310,7 @@
             // decode with inSampleSize
             BitmapFactory.Options o2 = new BitmapFactory.Options();
             o2.inSampleSize = scale;
+            o2.inMutable = true;
 
             Utils.closeSilently(is);
             is = mContext.getContentResolver().openInputStream(uri);
@@ -374,9 +372,9 @@
         mLoadingLock.lock();
         Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
         if (force || bmp == null) {
-            BitmapFactory.Options options = null;
+            BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inMutable = true;
             if (destination != null) {
-                options = new BitmapFactory.Options();
                 if (bounds.width() > destination.width()) {
                     int sampleSize = 1;
                     int w = bounds.width();
@@ -393,15 +391,14 @@
                 return bmp;
             }
             if (bmp != null) {
-                // TODO: this workaround for RS might not be needed ultimately
-                Bitmap bmp2 = bmp.copy(Bitmap.Config.ARGB_8888, true);
                 float scaleFactor = imagePreset.getScaleFactor();
-                imagePreset.setScaleFactor(1.0f);
-                bmp2 = imagePreset.apply(bmp2);
+                float scale = (float) bmp.getWidth() / (float) getOriginalBounds().width();
+                imagePreset.setScaleFactor(scale);
+                bmp = imagePreset.apply(bmp);
                 imagePreset.setScaleFactor(scaleFactor);
-                mZoomCache.setImage(imagePreset, bounds, bmp2);
+                mZoomCache.setImage(imagePreset, bounds, bmp);
                 mLoadingLock.unlock();
-                return bmp2;
+                return bmp;
             }
         }
         mLoadingLock.unlock();
diff --git a/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java b/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java
index 3ec74e2..e81f47f 100644
--- a/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java
+++ b/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java
@@ -27,6 +27,7 @@
     private boolean mIsDirect = false;
     private Bitmap mBitmap = null;
     private ImagePreset mImagePreset = null;
+    private ImagePreset mOriginalImagePreset = null;
     private RenderingRequestCaller mCaller = null;
     private Rect mBounds = null;
     private Rect mDestination = null;
@@ -61,6 +62,7 @@
         request.setBitmap(bitmap);
         ImagePreset passedPreset = new ImagePreset(preset);
         passedPreset.setImageLoader(MasterImage.getImage().getImageLoader());
+        request.setOriginalImagePreset(preset);
 
         if (type == PARTIAL_RENDERING) {
             request.setBounds(bounds);
@@ -137,4 +139,12 @@
     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/cache/TripleBufferBitmap.java b/src/com/android/gallery3d/filtershow/cache/TripleBufferBitmap.java
index cc14bf6..5d169c6 100644
--- a/src/com/android/gallery3d/filtershow/cache/TripleBufferBitmap.java
+++ b/src/com/android/gallery3d/filtershow/cache/TripleBufferBitmap.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.filtershow.cache;
 
 import android.graphics.Bitmap;
-import com.android.gallery3d.app.Log;
 
 public class TripleBufferBitmap {
 
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java
index 0acedb5..dd4df7d 100644
--- a/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.drawable.GradientDrawable;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
diff --git a/src/com/android/gallery3d/filtershow/editors/BasicEditor.java b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java
index 48aa592..fb09101 100644
--- a/src/com/android/gallery3d/filtershow/editors/BasicEditor.java
+++ b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java
@@ -16,16 +16,14 @@
 
 package com.android.gallery3d.filtershow.editors;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.filtershow.filters.*;
-
 import android.content.Context;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.SeekBar;
 import android.widget.SeekBar.OnSeekBarChangeListener;
-import com.android.gallery3d.filtershow.imageshow.MasterImage;
-import com.android.gallery3d.filtershow.presets.ImagePreset;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation;
 
 /**
  * The basic editor that all the one parameter filters
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..947fccb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorCrop.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.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.imageshow.ImageCrop;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorCrop extends Editor implements EditorInfo {
+    public static final int ID = R.id.editorCrop;
+    private static final String LOGTAG = "EditorCrop";
+
+    ImageCrop mImageCrop;
+    private String mAspectString = "";
+    private boolean mCropActionFlag = false;
+    private CropExtras mCropExtras = null;
+
+    public EditorCrop() {
+        super(ID);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageCrop == null) {
+            // TODO: need this for now because there's extra state in ImageCrop.
+            // all the state instead should be in the representation.
+            // Same thing for the other geometry editors.
+            mImageCrop = new ImageCrop(context);
+        }
+        mView = mImageShow = mImageCrop;
+        mImageCrop.setImageLoader(MasterImage.getImage().getImageLoader());
+        mImageCrop.setEditor(this);
+        mImageCrop.syncLocalToMasterGeometry();
+        mImageCrop.setCropActionFlag(mCropActionFlag);
+        if (mCropActionFlag) {
+            mImageCrop.setExtras(mCropExtras);
+            mImageCrop.setAspectString(mAspectString);
+            mImageCrop.clear();
+        } else {
+            mImageCrop.setExtras(null);
+        }
+    }
+
+    @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;
+    }
+
+    public void setExtras(CropExtras cropExtras) {
+        mCropExtras = cropExtras;
+    }
+
+    public void setAspectString(String s) {
+        mAspectString = s;
+    }
+
+    public void setCropActionFlag(boolean b) {
+        mCropActionFlag = b;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorFlip.java b/src/com/android/gallery3d/filtershow/editors/EditorFlip.java
new file mode 100644
index 0000000..94ab2ee
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorFlip.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.ImageFlip;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorFlip extends Editor implements EditorInfo {
+    public static final int ID = R.id.editorFlip;
+    ImageFlip mImageFlip;
+
+    public EditorFlip() {
+        super(ID);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageFlip == null) {
+            mImageFlip = new ImageFlip(context);
+        }
+        mView = mImageShow = mImageFlip;
+        mImageFlip.setImageLoader(MasterImage.getImage().getImageLoader());
+        mImageFlip.setEditor(this);
+        mImageFlip.syncLocalToMasterGeometry();
+    }
+
+    @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;
+    }
+}
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/EditorRedEye.java b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java
index c37102b..8e9e705 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.filtershow.editors;
 
 import android.content.Context;
-import android.util.Log;
 import android.widget.FrameLayout;
 
 import com.android.gallery3d.R;
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..0032399
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.ImageRotate;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorRotate extends Editor implements EditorInfo {
+    public static final int ID = R.id.editorRotate;
+    ImageRotate mImageRotate;
+
+    public EditorRotate() {
+        super(ID);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageRotate == null) {
+            mImageRotate = new ImageRotate(context);
+        }
+        mView = mImageShow = mImageRotate;
+        mImageRotate.setImageLoader(MasterImage.getImage().getImageLoader());
+        mImageRotate.setEditor(this);
+        mImageRotate.syncLocalToMasterGeometry();
+    }
+
+    @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;
+    }
+}
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..4641970
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.ImageStraighten;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorStraighten extends Editor implements EditorInfo {
+    public static final int ID = R.id.editorStraighten;
+    ImageStraighten mImageStraighten;
+
+    public EditorStraighten() {
+        super(ID);
+    }
+
+    @Override
+    public void createEditor(Context context, FrameLayout frameLayout) {
+        super.createEditor(context, frameLayout);
+        if (mImageStraighten == null) {
+            mImageStraighten = new ImageStraighten(context);
+        }
+        mView = mImageShow = mImageStraighten;
+        mImageStraighten.setImageLoader(MasterImage.getImage().getImageLoader());
+        mImageStraighten.setEditor(this);
+        mImageStraighten.syncLocalToMasterGeometry();
+    }
+
+    @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;
+    }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java
index d219509..c0fcdff 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.filtershow.editors;
 
 import android.content.Context;
-import android.util.Log;
 import android.widget.FrameLayout;
 
 import com.android.gallery3d.R;
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
index a60c168..a7d99e4 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
@@ -17,12 +17,10 @@
 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.FilterTinyPlanetRepresentation;
 import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation;
 import com.android.gallery3d.filtershow.imageshow.ImageVignette;
 
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
index 377bd2b..1fe2ac6 100644
--- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -13,49 +13,39 @@
  * 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.cache.ImageLoader;
-
 import java.util.HashMap;
+import java.util.Map;
 import java.util.Vector;
 
-public class BaseFiltersManager {
+public abstract class BaseFiltersManager {
+    protected HashMap<Class, ImageFilter> mFilters = null;
 
-    private static final String LOGTAG = "BaseFiltersManager";
-    private static HashMap<Class, ImageFilter> mFilters = new HashMap<Class, ImageFilter>();
+    protected void addFilters(Map<Class, ImageFilter> filters) {
+        filters.put(ImageFilterTinyPlanet.class, new ImageFilterTinyPlanet());
+        filters.put(ImageFilterRedEye.class, new ImageFilterRedEye());
+        filters.put(ImageFilterWBalance.class, new ImageFilterWBalance());
+        filters.put(ImageFilterExposure.class, new ImageFilterExposure());
+        filters.put(ImageFilterVignette.class, new ImageFilterVignette());
+        filters.put(ImageFilterContrast.class, new ImageFilterContrast());
+        filters.put(ImageFilterShadows.class, new ImageFilterShadows());
+        filters.put(ImageFilterHighlights.class, new ImageFilterHighlights());
+        filters.put(ImageFilterVibrance.class, new ImageFilterVibrance());
+        filters.put(ImageFilterSharpen.class, new ImageFilterSharpen());
+        filters.put(ImageFilterCurves.class, new ImageFilterCurves());
+        filters.put(ImageFilterDraw.class, new ImageFilterDraw());
+        filters.put(ImageFilterHue.class, new ImageFilterHue());
+        filters.put(ImageFilterSaturated.class, new ImageFilterSaturated());
+        filters.put(ImageFilterBwFilter.class, new ImageFilterBwFilter());
+        filters.put(ImageFilterNegative.class, new ImageFilterNegative());
+        filters.put(ImageFilterEdge.class, new ImageFilterEdge());
+        filters.put(ImageFilterKMeans.class, new ImageFilterKMeans());
+        filters.put(ImageFilterFx.class, new ImageFilterFx());
+        filters.put(ImageFilterBorder.class, new ImageFilterBorder());
+        filters.put(ImageFilterParametricBorder.class, new ImageFilterParametricBorder());
+        filters.put(ImageFilterGeometry.class, new ImageFilterGeometry());
 
-    protected BaseFiltersManager() {
-        Vector<ImageFilter> filters = new Vector<ImageFilter>();
-        addFilters(filters);
-        for (ImageFilter filter : filters) {
-            mFilters.put(filter.getClass(), filter);
-        }
-    }
-
-    protected void addFilters(Vector<ImageFilter> filters) {
-        filters.add(new ImageFilterTinyPlanet());
-        filters.add(new ImageFilterRedEye());
-        filters.add(new ImageFilterWBalance());
-        filters.add(new ImageFilterExposure());
-        filters.add(new ImageFilterVignette());
-        filters.add(new ImageFilterContrast());
-        filters.add(new ImageFilterShadows());
-        filters.add(new ImageFilterHighlights());
-        filters.add(new ImageFilterVibrance());
-        filters.add(new ImageFilterSharpen());
-        filters.add(new ImageFilterCurves());
-        filters.add(new ImageFilterDraw());
-        filters.add(new ImageFilterHue());
-        filters.add(new ImageFilterSaturated());
-        filters.add(new ImageFilterBwFilter());
-        filters.add(new ImageFilterNegative());
-        filters.add(new ImageFilterEdge());
-        filters.add(new ImageFilterKMeans());
-        filters.add(new ImageFilterFx());
-        filters.add(new ImageFilterBorder());
-        filters.add(new ImageFilterParametricBorder());
     }
 
     public ImageFilter getFilter(Class c) {
@@ -79,12 +69,11 @@
     }
 
     public void addLooks(Vector<FilterRepresentation> representations) {
-        // subclass can add representations
+        // Override
     }
 
     public void addEffects(Vector<FilterRepresentation> representations) {
         representations.add(getRepresentation(ImageFilterTinyPlanet.class));
-        representations.add(getRepresentation(ImageFilterRedEye.class));
         representations.add(getRepresentation(ImageFilterWBalance.class));
         representations.add(getRepresentation(ImageFilterExposure.class));
         representations.add(getRepresentation(ImageFilterVignette.class));
@@ -94,7 +83,6 @@
         representations.add(getRepresentation(ImageFilterVibrance.class));
         representations.add(getRepresentation(ImageFilterSharpen.class));
         representations.add(getRepresentation(ImageFilterCurves.class));
-        representations.add(getRepresentation(ImageFilterDraw.class));
         representations.add(getRepresentation(ImageFilterHue.class));
         representations.add(getRepresentation(ImageFilterSaturated.class));
         representations.add(getRepresentation(ImageFilterBwFilter.class));
@@ -103,6 +91,11 @@
         representations.add(getRepresentation(ImageFilterKMeans.class));
     }
 
+    public void addTools(Vector<FilterRepresentation> representations) {
+        representations.add(getRepresentation(ImageFilterRedEye.class));
+        representations.add(getRepresentation(ImageFilterDraw.class));
+    }
+
     public void resetBitmapsRS() {
         for (Class c : mFilters.keySet()) {
             ImageFilter filter = mFilters.get(c);
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
index 8b8504b..dc59b0c 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
@@ -54,6 +54,8 @@
         setTextId(R.string.imageDraw);
         setButtonId(R.id.drawOnImageButton);
         setEditorId(EditorDraw.ID);
+        setOverlayId(R.drawable.filtershow_drawing);
+        setOverlayOnly(true);
     }
 
     @Override
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
index 70d016f..3f823ea 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.filtershow.filters;
 
 import android.graphics.RectF;
-import android.util.Log;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.editors.EditorRedEye;
@@ -31,6 +30,7 @@
         super("RedEye",R.string.redeye,EditorRedEye.ID);
         setFilterClass(ImageFilterRedEye.class);
         setOverlayId(R.drawable.photoeditor_effect_redeye);
+        setOverlayOnly(true);
     }
 
     public void addRect(RectF rect, RectF bounds) {
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
index 83f2a1b..e0dc905 100644
--- a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
@@ -18,7 +18,6 @@
 
 import com.android.gallery3d.app.Log;
 import com.android.gallery3d.filtershow.editors.BasicEditor;
-import com.android.gallery3d.filtershow.presets.ImagePreset;
 
 public class FilterRepresentation implements Cloneable {
     private static final String LOGTAG = "FilterRepresentation";
@@ -30,6 +29,7 @@
     private int mEditorId = BasicEditor.ID;
     private int mButtonId = 0;
     private int mOverlayId = 0;
+    private boolean mOverlayOnly = false;
     private boolean mShowEditingControls = true;
     private boolean mShowParameterValue = true;
     private boolean mShowUtilityPanel = true;
@@ -58,6 +58,7 @@
         representation.setEditorId(getEditorId());
         representation.setButtonId(getButtonId());
         representation.setOverlayId(getOverlayId());
+        representation.setOverlayOnly(getOverlayOnly());
         representation.setShowEditingControls(showEditingControls());
         representation.setShowParameterValue(showParameterValue());
         representation.setShowUtilityPanel(showUtilityPanel());
@@ -77,6 +78,7 @@
                 && representation.mEditorId == mEditorId
                 && representation.mButtonId == mButtonId
                 && representation.mOverlayId == mOverlayId
+                && representation.mOverlayOnly == mOverlayOnly
                 && representation.mShowEditingControls == mShowEditingControls
                 && representation.mShowParameterValue == mShowParameterValue
                 && representation.mShowUtilityPanel == mShowUtilityPanel) {
@@ -181,10 +183,23 @@
         mOverlayId = overlayId;
     }
 
-    public int getEditorId() {
+    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;
     }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
index 614c6a0..b4eac0f 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
@@ -18,9 +18,9 @@
 
 import android.graphics.Bitmap;
 import android.graphics.Matrix;
+import android.widget.Toast;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
@@ -31,6 +31,30 @@
     protected String mName = "Original";
     private final String LOGTAG = "ImageFilter";
 
+    // TODO: Temporary, for dogfood note memory issues with toasts for better
+    // feedback. Remove this when filters actually work in low memory
+    // situations.
+    private static FilterShowActivity sActivity = null;
+
+    public static void setActivityForMemoryToasts(FilterShowActivity activity) {
+        sActivity = activity;
+    }
+
+    public static void resetStatics() {
+        sActivity = null;
+    }
+
+    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;
     }
@@ -45,8 +69,8 @@
     }
 
     /**
-     * Called on small bitmaps to create button icons for each filter.
-     * Override this to provide filter-specific button icons.
+     * Called on small bitmaps to create button icons for each filter. Override
+     * this to provide filter-specific button icons.
      */
     public Bitmap iconApply(Bitmap bitmap, float scaleFactor, int quality) {
         return apply(bitmap, scaleFactor, quality);
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
index 70e7f22..e156afc 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
@@ -24,9 +24,6 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
-
 import java.util.HashMap;
 
 public class ImageFilterBorder extends ImageFilter {
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
index aa4cf22..daa1cd3 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
@@ -17,9 +17,7 @@
 package com.android.gallery3d.filtershow.filters;
 
 import android.graphics.Bitmap;
-import android.util.Log;
 
-import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.ui.Spline;
 
 public class ImageFilterCurves extends ImageFilter {
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
index 6d7614e..1fd9071 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
@@ -27,8 +27,6 @@
 import android.graphics.PathMeasure;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
-import android.util.Log;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation.StrokeData;
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
index 820ec3e..c2a7b7b 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
@@ -18,9 +18,6 @@
 
 import android.graphics.Bitmap;
 
-import com.android.gallery3d.filtershow.editors.BasicEditor;
-import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
-
 public class ImageFilterFx extends ImageFilter {
     private static final String TAG = "ImageFilterFx";
     private FilterFxRepresentation mParameters = null;
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
index 329ca81..cbb443f 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
@@ -50,10 +50,6 @@
         return filter;
     }
 
-    public void setGeometryMetadata(GeometryMetadata m) {
-        mGeometry = m;
-    }
-
     native protected void nativeApplyFilterFlip(Bitmap src, int srcWidth, int srcHeight,
             Bitmap dst, int dstWidth, int dstHeight, int flip);
 
@@ -68,7 +64,7 @@
 
     @Override
     public void useRepresentation(FilterRepresentation representation) {
-
+        mGeometry = (GeometryMetadata) representation;
     }
 
     @Override
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
index 12f032d..0022a9e 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
@@ -16,10 +16,9 @@
 
 package com.android.gallery3d.filtershow.filters;
 
-import com.android.gallery3d.R;
-
 import android.graphics.Bitmap;
-import android.util.Log;
+
+import com.android.gallery3d.R;
 
 public class ImageFilterHighlights extends SimpleImageFilter {
     private static final String LOGTAG = "ImageFilterVignette";
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
index 6f785ef..29e6d16 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
@@ -20,7 +20,6 @@
 import android.text.format.Time;
 
 import com.android.gallery3d.R;
-import com.android.gallery3d.ingest.ui.MtpThumbnailTileView;
 
 public class ImageFilterKMeans extends SimpleImageFilter {
     private int mSeed = 0;
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
index 316a286..3a7878e 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
@@ -18,13 +18,10 @@
 
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.RectF;
 
-import com.android.gallery3d.R;
-
 public class ImageFilterParametricBorder extends ImageFilter {
     private FilterColorBorderRepresentation mParameters = null;
 
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
index 74712be..4373c95 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
@@ -20,6 +20,7 @@
 import android.graphics.Bitmap;
 import android.support.v8.renderscript.*;
 import android.util.Log;
+import com.android.gallery3d.R;
 
 public abstract class ImageFilterRS extends ImageFilter {
     private final String LOGTAG = "ImageFilterRS";
@@ -89,6 +90,11 @@
             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;
     }
@@ -113,4 +119,23 @@
         sOldBitmap = null;
     }
 
+    public Allocation convertRGBAtoA(Bitmap bitmap) {
+        Type.Builder tb_a8 = new Type.Builder(mRS, Element.U8(mRS));
+        ScriptC_grey greyConvert = new ScriptC_grey(mRS, mResources, R.raw.grey);
+
+        Allocation bitmapTemp = Allocation.createFromBitmap(mRS, bitmap);
+
+        if (bitmapTemp.getType().getElement().isCompatible(Element.U8(mRS))) {
+            return bitmapTemp;
+        }
+
+        tb_a8.setX(bitmapTemp.getType().getX());
+        tb_a8.setY(bitmapTemp.getType().getY());
+        Allocation bitmapAlloc = Allocation.createTyped(mRS, tb_a8.create());
+        greyConvert.forEach_RGBAtoA(bitmapTemp, bitmapAlloc);
+
+        return bitmapAlloc;
+
+    }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
index 702cc66..276da13 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
@@ -24,9 +24,7 @@
 
 import com.adobe.xmp.XMPException;
 import com.adobe.xmp.XMPMeta;
-import com.android.gallery3d.R;
 import com.android.gallery3d.app.Log;
-import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
 /**
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
index 9ff737e..0a7ee3c 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
@@ -19,8 +19,6 @@
 import android.graphics.Bitmap;
 import android.graphics.Matrix;
 
-import com.android.gallery3d.app.Log;
-
 public class ImageFilterVignette extends SimpleImageFilter {
     private static final String LOGTAG = "ImageFilterVignette";
 
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/imageshow/EclipseControl.java b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java
index b4ca8e1..a329677 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java
@@ -46,15 +46,15 @@
     private float mDownRadiusY;
     private Matrix mScrToImg;
 
-    private final static int HAN_CENTER = 0;
-    private final static int HAN_NORTH = 7;
-    private final static int HAN_NE = 8;
-    private final static int HAN_EAST = 1;
-    private final static int HAN_SE = 2;
-    private final static int HAN_SOUTH = 3;
-    private final static int HAN_SW = 4;
-    private final static int HAN_WEST = 5;
-    private final static int HAN_NW = 6;
+    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 = context.getResources().getColor(R.color.slider_line_color);
@@ -158,7 +158,24 @@
         }
     }
 
-    void paintPoint(Canvas canvas, float x, float y) {
+    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;
         }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
index a3645d6..c7d08d8 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
@@ -23,10 +23,14 @@
 
 import com.android.gallery3d.filtershow.CropExtras;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.editors.EditorFlip;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.ImageFilterGeometry;
 
-public class GeometryMetadata {
-    private static final ImageFilterGeometry mImageFilter = new ImageFilterGeometry();
+public class GeometryMetadata extends FilterRepresentation {
     private static final String LOGTAG = "GeometryMetadata";
     private float mScaleFactor = 1.0f;
     private float mRotation = 0;
@@ -59,9 +63,25 @@
     }
 
     public GeometryMetadata() {
+        super("GeometryMetadata");
+        setFilterClass(ImageFilterGeometry.class);
+        setEditorId(EditorCrop.ID);
+        setName("Crop");
+        setTextId(0);
+    }
+
+    @Override
+    public int[] getEditorIds() {
+        return new int[] {
+                EditorCrop.ID,
+                EditorStraighten.ID,
+                EditorRotate.ID,
+                EditorFlip.ID
+        };
     }
 
     public GeometryMetadata(GeometryMetadata g) {
+        super("GeometryMetadata");
         set(g);
     }
 
@@ -86,15 +106,6 @@
         return false;
     }
 
-    public Bitmap apply(Bitmap original, float scaleFactor, int quality) {
-        if (!hasModifications()) {
-            return original;
-        }
-        mImageFilter.setGeometryMetadata(this);
-        Bitmap m = mImageFilter.apply(original, scaleFactor, quality);
-        return m;
-    }
-
     public void set(GeometryMetadata g) {
         mScaleFactor = g.mScaleFactor;
         mRotation = g.mRotation;
@@ -170,7 +181,7 @@
     }
 
     @Override
-    public boolean equals(Object o) {
+    public boolean equals(FilterRepresentation o) {
         if (this == o)
             return true;
         if (o == null || getClass() != o.getClass())
@@ -436,4 +447,17 @@
         m.preRotate(-straighten, photo.centerX(), photo.centerY());
         return m;
     }
+
+    @Override
+    public void useParametersFrom(FilterRepresentation a) {
+        GeometryMetadata data = (GeometryMetadata) a;
+        set(data);
+    }
+
+    @Override
+    public FilterRepresentation clone() throws CloneNotSupportedException {
+        GeometryMetadata representation = (GeometryMetadata) super.clone();
+        representation.useParametersFrom(this);
+        return representation;
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
index 284bfde..2ea6f6a 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
@@ -26,17 +26,15 @@
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
 import android.widget.LinearLayout;
 import android.widget.PopupMenu;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
 import com.android.gallery3d.filtershow.ui.FramedTextButton;
 
 public class ImageCrop extends ImageGeometry {
@@ -81,11 +79,13 @@
     private static final String LOGTAG = "ImageCrop";
 
     private String mAspect = "";
-    private int mAspectTextSize = 24;
+    private static int mAspectTextSize = 24;
 
     private boolean mFixedAspect = false;
 
-    public void setAspectTextSize(int textSize) {
+    private EditorCrop mEditorCrop;
+
+    public static void setAspectTextSize(int textSize) {
         mAspectTextSize = textSize;
     }
 
@@ -151,9 +151,7 @@
         mAspectHeight = h;
         setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
                 getLocalStraighten()));
-        if (mVisibilityGained) {
-            cropSetup();
-        }
+        cropSetup();
         saveAndSetPreset();
         invalidate();
     }
@@ -168,9 +166,7 @@
         mAspectHeight = h / scale;
         setLocalCropBounds(getUntranslatedStraightenCropBounds(photobounds,
                 getLocalStraighten()));
-        if (mVisibilityGained) {
-            cropSetup();
-        }
+        cropSetup();
         saveAndSetPreset();
         invalidate();
     }
@@ -181,9 +177,7 @@
         mAspectHeight = 1;
         setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
                 getLocalStraighten()));
-        if (mVisibilityGained) {
-            cropSetup();
-        }
+        cropSetup();
         saveAndSetPreset();
         invalidate();
     }
@@ -779,4 +773,8 @@
         }
     }
 
+    public void setEditor(EditorCrop editorCrop) {
+        mEditorCrop = editorCrop;
+    }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
index 479652c..6c3417a 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
@@ -7,7 +7,6 @@
 import android.graphics.Matrix;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 
 import com.android.gallery3d.filtershow.editors.EditorDraw;
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java b/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
index 6bfba1b..70637a3 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageFlip.java
@@ -24,6 +24,7 @@
 import android.util.AttributeSet;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorFlip;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata.FLIP;
 
 public class ImageFlip extends ImageGeometry {
@@ -32,6 +33,7 @@
     private static final float MIN_FLICK_DIST_FOR_FLIP = 0.1f;
     private static final String LOGTAG = "ImageFlip";
     private FLIP mNextFlip = FLIP.NONE;
+    private EditorFlip mEditorFlip;
 
     public ImageFlip(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -140,4 +142,8 @@
         drawTransformedCropped(canvas, image, gPaint);
     }
 
+    public void setEditor(EditorFlip editorFlip) {
+        mEditorFlip = editorFlip;
+    }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java b/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
index e18f0d0..68a74dc 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageGeometry.java
@@ -29,7 +29,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 
-import com.android.gallery3d.app.Log;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata.FLIP;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
@@ -138,7 +137,7 @@
     }
 
     // Overwrites local with master
-    protected void syncLocalToMasterGeometry() {
+    public void syncLocalToMasterGeometry() {
         mLocalGeometry = getGeometry();
         calculateLocalScalingFactorAndOffset();
     }
@@ -423,6 +422,7 @@
             return;
         }
         mHasDrawn = true;
+
         drawShape(canvas, image);
     }
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java
index 625cdbe..c58dd5f 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java
@@ -22,16 +22,12 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Paint.Style;
-import android.graphics.RectF;
 import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
 
 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;
-import com.android.gallery3d.filtershow.filters.RedEyeCandidate;
 
 public abstract class ImagePoint extends ImageShow {
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
index 30cc9e2..c4b9aa2 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
@@ -23,6 +23,7 @@
 import android.util.AttributeSet;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
 
 public class ImageRotate extends ImageGeometry {
 
@@ -30,6 +31,7 @@
     private float mAngle = 0;
 
     private final boolean mSnapToNinety = true;
+    private EditorRotate mEditorRotate;
     private static final String LOGTAG = "ImageRotate";
 
     public ImageRotate(Context context, AttributeSet attrs) {
@@ -84,4 +86,8 @@
         gPaint.setARGB(255, 255, 255, 255);
         drawTransformedCropped(canvas, image, gPaint);
     }
+
+    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
index 39e0cc8..1edfd79 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
@@ -17,20 +17,28 @@
 package com.android.gallery3d.filtershow.imageshow;
 
 import android.content.Context;
-import android.graphics.*;
+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.net.Uri;
 import android.os.Handler;
 import android.util.AttributeSet;
-import android.util.Log;
-import android.view.*;
+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.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.PanelController;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
-import com.android.gallery3d.filtershow.cache.RenderingRequestCaller;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
 import com.android.gallery3d.filtershow.presets.ImagePreset;
 
@@ -289,11 +297,6 @@
         }
     }
 
-    public void defaultDrawImage(Canvas canvas) {
-        drawImage(canvas, getFilteredImage());
-        drawPartialImage(canvas, getGeometryOnlyImage());
-    }
-
     @Override
     public void onDraw(Canvas canvas) {
         MasterImage.getImage().setImageShowSize(getWidth(), getHeight());
@@ -306,7 +309,7 @@
         canvas.scale(scaleFactor, scaleFactor, cx, cy);
         canvas.translate(translation.x, translation.y);
         drawBackground(canvas);
-        defaultDrawImage(canvas);
+        drawImage(canvas, getFilteredImage());
         canvas.restore();
 
         if (showTitle() && getImagePreset() != null) {
@@ -326,6 +329,13 @@
             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();
+
         drawToast(canvas);
     }
 
@@ -374,7 +384,7 @@
         canvas.save();
         if (image != null) {
             if (mShowOriginalDirection == 0) {
-                if ((mTouch.y - mTouchDown.y) > (mTouch.x - mTouchDown.x)) {
+                if (Math.abs(mTouch.y - mTouchDown.y) > Math.abs(mTouch.x - mTouchDown.x)) {
                     mShowOriginalDirection = UNVEIL_VERTICAL;
                 } else {
                     mShowOriginalDirection = UNVEIL_HORIZONTAL;
@@ -397,16 +407,18 @@
             drawImage(canvas, image);
             Paint paint = new Paint();
             paint.setColor(Color.BLACK);
+            paint.setStrokeWidth(3);
 
             if (mShowOriginalDirection == UNVEIL_VERTICAL) {
-                canvas.drawLine(mImageBounds.left, mTouch.y - 1,
-                        mImageBounds.right, mTouch.y - 1, paint);
+                canvas.drawLine(mImageBounds.left, mTouch.y,
+                        mImageBounds.right, mTouch.y, paint);
             } else {
-                canvas.drawLine(mTouch.x - 1, mImageBounds.top,
-                        mTouch.x - 1, mImageBounds.bottom, paint);
+                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);
@@ -560,6 +572,7 @@
                     translation.y = (int) (originalTranslation.y + translateY);
                     MasterImage.getImage().setTranslation(translation);
                 }
+                mTouchShowOriginal = false;
             } else if (!mOriginalDisabled && !mActivity.isShowingHistoryPanel()
                     && (System.currentTimeMillis() - mTouchShowOriginalDate
                             > mTouchShowOriginalDelayMin)
@@ -617,14 +630,16 @@
         if (mActivity == null) {
             return false;
         }
+        if (endEvent.getPointerCount() == 2) {
+            return false;
+        }
         if ((!mActivity.isShowingHistoryPanel() && startEvent.getX() > endEvent.getX())
                 || (mActivity.isShowingHistoryPanel() && endEvent.getX() > startEvent.getX())) {
             if (!mTouchShowOriginal
                     || (mTouchShowOriginal &&
                             (System.currentTimeMillis() - mTouchShowOriginalDate
                             < mTouchShowOriginalDelayMax))) {
-                // TODO fix gesture.
-                // mActivity.toggleHistoryPanel();
+                mActivity.toggleHistoryPanel();
             }
         }
         return true;
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
index dfd9505..866b1b0 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
@@ -19,18 +19,18 @@
 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.RectF;
 import android.util.AttributeSet;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
 
 public class ImageStraighten extends ImageGeometry {
 
     private float mBaseAngle = 0;
     private float mAngle = 0;
+    private EditorStraighten mEditorStraighten;
 
     private static final String LOGTAG = "ImageStraighten";
     private static final Paint gPaint = new Paint();
@@ -134,4 +134,8 @@
         }
     }
 
+    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
index 3795d1f..a49636f 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java
@@ -18,15 +18,11 @@
 
 import android.content.Context;
 import android.graphics.Canvas;
-import android.graphics.Paint;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 
 import com.android.gallery3d.filtershow.editors.BasicEditor;
-import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
-import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation;
 import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
-import com.android.gallery3d.filtershow.filters.ImageFilterTinyPlanet;
 
 public class ImageTinyPlanet extends ImageShow {
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
index a51d102..c55e5ae 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2013 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -88,14 +88,20 @@
                 setRepresentation(mVignetteRep);
                 break;
         }
-        resetImageCaches(this);
+        computeEllipses();
         invalidate();
-        mEditorVignette.commitLocalRepresentation();
         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);
@@ -125,6 +131,7 @@
             mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()),
                     toScr.mapRadius(mVignetteRep.getRadiusY()));
         }
+        mEditorVignette.commitLocalRepresentation();
     }
 
     public void setEditor(EditorVignette editorVignette) {
@@ -132,11 +139,28 @@
     }
 
     @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);
-        setRepresentation(mVignetteRep);
-        mElipse.draw(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/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
index 9eafe22..209c3bd 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
@@ -17,7 +17,11 @@
 package com.android.gallery3d.filtershow.imageshow;
 
 import android.graphics.*;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
 
+import android.util.Log;
 import com.android.gallery3d.filtershow.FilterShowActivity;
 import com.android.gallery3d.filtershow.HistoryAdapter;
 import com.android.gallery3d.filtershow.ImageStateAdapter;
@@ -31,8 +35,12 @@
 public class MasterImage implements RenderingRequestCaller {
 
     private static final String LOGTAG = "MasterImage";
+    private boolean DEBUG  = false;
 
     private static MasterImage sMasterImage = null;
+    private static int sIconSeedSize = 128;
+    private static float sHistoryPreviewSize = 128.0f;
+    private Bitmap mThumbnailBitmap;
 
     private ImageFilter mCurrentFilter = null;
     private ImagePreset mPreset = null;
@@ -63,6 +71,20 @@
 
     private Point mImageShowSize = new Point();
 
+    final private static int NEW_GEOMETRY = 1;
+
+    private final Handler mHandler = new Handler() {
+            @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case NEW_GEOMETRY: {
+                hasNewGeometry();
+                break;
+            }
+            }
+        }
+    };
+
     private MasterImage() {
     }
 
@@ -73,6 +95,10 @@
         return sMasterImage;
     }
 
+    public static void setIconSeedSize(int iconSeedSize) {
+        sIconSeedSize = iconSeedSize;
+    }
+
     public void addObserver(ImageShow observer) {
         if (mObservers.contains(observer)) {
             return;
@@ -107,6 +133,7 @@
         mPreset.fillImageStateAdapter(mState);
         if (addToHistory) {
             mHistory.addHistoryItem(mPreset);
+            renderHistoryPreview();
         }
         updatePresets(true);
         GeometryMetadata geo = mPreset.mGeoData;
@@ -116,6 +143,23 @@
         mPreviousGeometry = new GeometryMetadata(geo);
     }
 
+    private void renderHistoryPreview() {
+        ImagePreset historyPreset = mPreset;
+        if (historyPreset != null) {
+            Bitmap preview = mLoader.getOriginalBitmapSmall();
+            if (preview != null) {
+                float s = Math.min(preview.getWidth(), preview.getHeight());
+                float f = sHistoryPreviewSize / s;
+                int w = (int) (preview.getWidth() * f);
+                int h = (int) (preview.getHeight() * f);
+                Bitmap historyPreview = Bitmap.createScaledBitmap(preview, w, h, true);
+                historyPreset.setPreviewImage(historyPreview);
+                RenderingRequest.post(historyPreview,
+                        historyPreset, RenderingRequest.ICON_RENDERING, this);
+            }
+        }
+    }
+
     private void setGeometry() {
         Bitmap image = mLoader.getOriginalBitmapLarge();
         if (image == null) {
@@ -182,6 +226,16 @@
         return mFilteredPreview;
     }
 
+    public void setOriginalGeometry(Bitmap originalBitmapLarge) {
+        GeometryMetadata geo = getPreset().mGeoData;
+        float w = originalBitmapLarge.getWidth();
+        float h = originalBitmapLarge.getHeight();
+        RectF r = new RectF(0, 0, w, h);
+        geo.setPhotoBounds(r);
+        geo.setCropBounds(r);
+        getPreset().setGeometry(geo);
+    }
+
     public Bitmap getFilteredImage() {
         return mFilteredPreview.getConsumer();
     }
@@ -208,7 +262,8 @@
         if (force || mGeometryOnlyPreset == null) {
             ImagePreset newPreset = new ImagePreset(mPreset);
             newPreset.setDoApplyFilters(false);
-            if (mGeometryOnlyPreset == null
+            newPreset.setDoApplyGeometry(true);
+            if (force || mGeometryOnlyPreset == null
                     || !newPreset.same(mGeometryOnlyPreset)) {
                 mGeometryOnlyPreset = newPreset;
                 RenderingRequest.post(mLoader.getOriginalBitmapLarge(),
@@ -217,8 +272,9 @@
         }
         if (force || mFiltersOnlyPreset == null) {
             ImagePreset newPreset = new ImagePreset(mPreset);
+            newPreset.setDoApplyFilters(true);
             newPreset.setDoApplyGeometry(false);
-            if (mFiltersOnlyPreset == null
+            if (force || mFiltersOnlyPreset == null
                     || !newPreset.same(mFiltersOnlyPreset)) {
                 mFiltersOnlyPreset = newPreset;
                 RenderingRequest.post(mLoader.getOriginalBitmapLarge(),
@@ -255,6 +311,7 @@
         invalidatePartialPreview();
         needsUpdateFullResPreview();
         FilteringPipeline.getPipeline().updatePreviewBuffer();
+        renderHistoryPreview();
     }
 
     public void setImageShowSize(int w, int h) {
@@ -319,6 +376,12 @@
             mPartialBitmap = request.getBitmap();
             notifyObservers();
         }
+        if (request.getType() == RenderingRequest.ICON_RENDERING) {
+            // History preview images
+            ImagePreset preset = request.getOriginalImagePreset();
+            preset.setPreviewImage(request.getBitmap());
+            mHistory.notifyDataSetChanged();
+        }
     }
 
     public static void reset() {
@@ -330,11 +393,67 @@
     }
 
     public void notifyGeometryChange() {
+        if (mHandler.hasMessages(NEW_GEOMETRY)) {
+            return;
+        }
+        mHandler.sendEmptyMessage(NEW_GEOMETRY);
+    }
+
+    public void hasNewGeometry() {
+        updatePresets(true);
+        computeThumbnailBitmap();
         for (GeometryListener listener : mGeometryListeners) {
             listener.geometryChanged();
         }
     }
 
+    private Bitmap createSquareImage(Bitmap dst, Bitmap image, Rect destination) {
+        if (image != null) {
+            Canvas canvas = new Canvas(dst);
+            int iw = image.getWidth();
+            int ih = image.getHeight();
+            int x = 0;
+            int y = 0;
+            int size = 0;
+            Rect source = null;
+            if (iw > ih) {
+                size = ih;
+                x = (int) ((iw - size) / 2.0f);
+                y = 0;
+            } else {
+                size = iw;
+                x = 0;
+                y = (int) ((ih - size) / 2.0f);
+            }
+            source = new Rect(x, y, x + size, y + size);
+            canvas.drawBitmap(image, source, destination, new Paint());
+        }
+        return dst;
+    }
+
+    public void computeThumbnailBitmap() {
+        Bitmap bmap = mLoader.getOriginalBitmapSmall();
+        if (bmap == null) {
+            return;
+        }
+        ImagePreset geoPreset = new ImagePreset(MasterImage.getImage().getGeometryPreset());
+        bmap = geoPreset.applyGeometry(bmap);
+        float w = bmap.getWidth();
+        float h = bmap.getHeight();
+        float s = Math.min(w, h);
+        float f = sIconSeedSize / s;
+        w = w * f;
+        h = h * f;
+        s = Math.min(w, h);
+        Bitmap bmap2 = Bitmap.createScaledBitmap(bmap, (int) s, (int) s, true);
+        bmap = createSquareImage(bmap2, bmap, new Rect(0, 0, (int) s, (int) s));
+        if (DEBUG) {
+            Log.v(LOGTAG, "Create thumbnail of size " + bmap.getWidth() + " x " + bmap.getHeight()
+                    + " seed size: " + sIconSeedSize);
+        }
+        mThumbnailBitmap = bmap;
+    }
+
     public float getScaleFactor() {
         return mScaleFactor;
     }
@@ -368,4 +487,8 @@
         mTranslation.y = 0;
         needsUpdateFullResPreview();
     }
+
+    public Bitmap getThumbnailBitmap() {
+        return mThumbnailBitmap;
+    }
 }
diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
index ae5a034..ca74a87 100644
--- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
+++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java
@@ -22,7 +22,6 @@
 
 import com.android.gallery3d.filtershow.ImageStateAdapter;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
-import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
 import com.android.gallery3d.filtershow.filters.FilterRepresentation;
 import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.filters.ImageFilter;
@@ -55,6 +54,7 @@
     public final GeometryMetadata mGeoData = new GeometryMetadata();
     private boolean mPartialRendering = false;
     private Rect mPartialRenderingBounds;
+    private Bitmap mPreviewImage;
 
     public ImagePreset() {
         setup();
@@ -130,10 +130,17 @@
     }
 
     public void updateFilterRepresentation(FilterRepresentation representation) {
+        if (representation == null) {
+            return;
+        }
         synchronized (mFilters) {
-            int position = getPositionForRepresentation(representation);
-            FilterRepresentation old = mFilters.elementAt(position);
-            old.updateTempParametersFrom(representation);
+            if (representation instanceof GeometryMetadata) {
+                setGeometry((GeometryMetadata) representation);
+            } else {
+                int position = getPositionForRepresentation(representation);
+                FilterRepresentation old = mFilters.elementAt(position);
+                old.updateTempParametersFrom(representation);
+            }
         }
         MasterImage.getImage().invalidatePreview();
     }
@@ -192,6 +199,7 @@
 
     public synchronized void setGeometry(GeometryMetadata m) {
         mGeoData.set(m);
+        MasterImage.getImage().notifyGeometryChange();
     }
 
     private void setBorder(FilterRepresentation filter) {
@@ -327,8 +335,28 @@
         Log.v(LOGTAG, "/// showFilters -- " + mFilters.size() + " filters");
     }
 
+    public FilterRepresentation getLastRepresentation() {
+        if (mFilters.size() > 0) {
+            return mFilters.lastElement();
+        }
+        return null;
+    }
+
+    public void removeFilter(FilterRepresentation filterRepresentation) {
+        for (int i = 0; i < mFilters.size(); i++) {
+            if (mFilters.elementAt(i).getFilterClass() == filterRepresentation.getFilterClass()) {
+                mFilters.remove(i);
+                setHistoryName("Remove");
+                return;
+            }
+        }
+    }
+
     public void addFilter(FilterRepresentation representation) {
-        Log.v(LOGTAG, "*** Add Filter *** " + representation);
+        if (representation instanceof GeometryMetadata) {
+            setGeometry((GeometryMetadata) representation);
+            return;
+        }
         if (representation.getPriority() == FilterRepresentation.TYPE_BORDER) {
             setHistoryName(representation.getName());
             setBorder(representation);
@@ -360,6 +388,9 @@
     }
 
     public FilterRepresentation getRepresentation(FilterRepresentation filterRepresentation) {
+        if (filterRepresentation instanceof GeometryMetadata) {
+            return mGeoData;
+        }
         for (int i = 0; i < mFilters.size(); i++) {
             FilterRepresentation representation = mFilters.elementAt(i);
             if (representation.getFilterClass() == filterRepresentation.getFilterClass()) {
@@ -385,7 +416,14 @@
     public Bitmap applyGeometry(Bitmap bitmap) {
         // Apply any transform -- 90 rotate, flip, straighten, crop
         // Returns a new bitmap.
-        return mGeoData.apply(bitmap, mScaleFactor, mQuality);
+        if (mDoApplyGeometry) {
+            ImageFilter filter = FiltersManager.getManager().getFilterForRepresentation(mGeoData);
+            mGeoData.synchronizeRepresentation();
+            filter.useRepresentation(mGeoData);
+            filter.setImagePreset(this);
+            bitmap = filter.apply(bitmap, mScaleFactor, mQuality);
+        }
+        return bitmap;
     }
 
     public Bitmap applyBorder(Bitmap bitmap) {
@@ -428,6 +466,12 @@
         if (mGeoData.hasModifications()) {
             return false;
         }
+        if (mBorder != null && !mBorder.supportsPartialRendering()) {
+            return false;
+        }
+        if (ImageLoader.getZoomOrientation() != ImageLoader.ORI_NORMAL) {
+            return false;
+        }
         for (int i = 0; i < mFilters.size(); i++) {
             FilterRepresentation representation = null;
             synchronized (mFilters) {
@@ -478,4 +522,13 @@
     public Rect getPartialRenderingBounds() {
         return mPartialRenderingBounds;
     }
+
+    public Bitmap getPreviewImage() {
+        return mPreviewImage;
+    }
+
+    public void setPreviewImage(Bitmap previewImage) {
+        mPreviewImage = previewImage;
+    }
+
 }
diff --git a/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java b/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java
index de2e1e5..9d50d5a 100644
--- a/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java
+++ b/src/com/android/gallery3d/filtershow/ui/FilterIconButton.java
@@ -37,6 +37,7 @@
         RenderingRequestCaller, GeometryListener {
     private static final String LOGTAG = "FilterIconButton";
     private Bitmap mOverlayBitmap = null;
+    private boolean mOverlayOnly = false;
     private PanelController mController = null;
     private FilterRepresentation mFilterRepresentation = null;
     private LinearLayout mParentContainer = null;
@@ -68,14 +69,16 @@
 
     @Override
     protected Bitmap drawImage(Bitmap dst, Bitmap image, Rect destination) {
+        if (mOverlayOnly) {
+            // TODO: merge back IconButton and FilterIconButton
+            return super.drawImage(dst, image, destination);
+        }
         if (mIconBitmap == null && mPreset == null) {
-            ImageLoader loader = MasterImage.getImage().getLoader();
-            if (loader != null) {
-                ImagePreset geoPreset = new ImagePreset(MasterImage.getImage().getGeometryPreset());
-                image = geoPreset.applyGeometry(image);
-                dst = super.drawImage(dst, image, destination);
+            dst = MasterImage.getImage().getThumbnailBitmap();
+            if (dst != null) {
                 ImagePreset mPreset = new ImagePreset();
                 mPreset.addFilter(mFilterRepresentation);
+                mPreset.setDoApplyGeometry(false);
                 mDestination = destination;
                 RenderingRequest.post(dst.copy(Bitmap.Config.ARGB_8888, true), mPreset, RenderingRequest.ICON_RENDERING, this);
             }
@@ -112,6 +115,11 @@
             mOverlayBitmap = BitmapFactory.decodeResource(getResources(),
                     mFilterRepresentation.getOverlayId());
         }
+        mOverlayOnly = mFilterRepresentation.getOverlayOnly();
+        if (mOverlayOnly) {
+            setIcon(mOverlayBitmap);
+        }
+        stale_icon = true;
         invalidate();
     }
 
@@ -124,13 +132,14 @@
         if (mOverlayBitmap != null) {
             mIconBitmap = super.drawImage(mIconBitmap, mOverlayBitmap, mDestination);
         }
-        invalidate();
         stale_icon = true;
+        invalidate();
     }
 
     @Override
     public void geometryChanged() {
         stale_icon = true;
+
         mIconBitmap = null;
         mPreset = null;
         invalidate();
diff --git a/src/com/android/gallery3d/filtershow/ui/IconButton.java b/src/com/android/gallery3d/filtershow/ui/IconButton.java
index 28d01df..ed10be3 100644
--- a/src/com/android/gallery3d/filtershow/ui/IconButton.java
+++ b/src/com/android/gallery3d/filtershow/ui/IconButton.java
@@ -17,19 +17,14 @@
 package com.android.gallery3d.filtershow.ui;
 
 import android.content.Context;
-import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.widget.Button;
 
-import com.android.gallery3d.R;
-
 /**
  * Class of buttons with both an image icon and text.
  */
diff --git a/src/com/android/gallery3d/filtershow/ui/ImageCurves.java b/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
index e54c83e..3e52f5e 100644
--- a/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
+++ b/src/com/android/gallery3d/filtershow/ui/ImageCurves.java
@@ -35,7 +35,6 @@
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.filtershow.editors.EditorCurves;
-import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
 import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation;
 import com.android.gallery3d.filtershow.filters.FiltersManager;
 import com.android.gallery3d.filtershow.filters.ImageFilterCurves;
diff --git a/src/com/android/gallery3d/glrenderer/BasicTexture.java b/src/com/android/gallery3d/glrenderer/BasicTexture.java
index 82eb5a7..2e77b90 100644
--- a/src/com/android/gallery3d/glrenderer/BasicTexture.java
+++ b/src/com/android/gallery3d/glrenderer/BasicTexture.java
@@ -78,8 +78,8 @@
     public void setSize(int width, int height) {
         mWidth = width;
         mHeight = height;
-        mTextureWidth = Utils.nextPowerOf2(width);
-        mTextureHeight = Utils.nextPowerOf2(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());
diff --git a/src/com/android/photos/AlbumSetFragment.java b/src/com/android/photos/AlbumSetFragment.java
new file mode 100644
index 0000000..3c51bba
--- /dev/null
+++ b/src/com/android/photos/AlbumSetFragment.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.CursorAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.photos.data.AlbumSetLoader;
+import com.android.photos.drawables.DrawableFactory;
+import com.android.photos.shims.MediaSetLoader;
+
+import java.util.Date;
+
+
+public class AlbumSetFragment extends Fragment implements OnItemClickListener,
+    LoaderCallbacks<Cursor> {
+
+    private GridView mAlbumSetView;
+    private View mEmptyView;
+    private AlbumSetCursorAdapter mAdapter;
+
+    private static final int LOADER_ALBUMSET = 1;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View root = inflater.inflate(R.layout.album_set, container, false);
+        mAlbumSetView = (GridView) root.findViewById(android.R.id.list);
+        mEmptyView = root.findViewById(android.R.id.empty);
+        mEmptyView.setVisibility(View.GONE);
+        mAdapter = new AlbumSetCursorAdapter(getActivity());
+        mAlbumSetView.setAdapter(mAdapter);
+        mAlbumSetView.setOnItemClickListener(this);
+        getLoaderManager().initLoader(LOADER_ALBUMSET, null, this);
+        updateEmptyStatus();
+        return root;
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        // TODO: Switch to AlbumSetLoader
+        MediaSetLoader loader = new MediaSetLoader(getActivity());
+        mAdapter.setDrawableFactory(loader);
+        return loader;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader,
+            Cursor data) {
+        mAdapter.swapCursor(data);
+        updateEmptyStatus();
+    }
+
+    private void updateEmptyStatus() {
+        boolean empty = (mAdapter == null || mAdapter.getCount() == 0);
+        mAlbumSetView.setVisibility(empty ? View.GONE : View.VISIBLE);
+        mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> av, View v, int pos, long id) {
+        Cursor c = (Cursor) av.getItemAtPosition(pos);
+        int albumId = c.getInt(AlbumSetLoader.INDEX_ID);
+        // TODO launch an activity showing the photos in the album
+        Toast.makeText(v.getContext(), "Clicked " + albumId, Toast.LENGTH_SHORT).show();
+    }
+
+    private static class AlbumSetCursorAdapter extends CursorAdapter {
+
+        private DrawableFactory<Cursor> mDrawableFactory;
+
+        public void setDrawableFactory(DrawableFactory<Cursor> factory) {
+            mDrawableFactory = factory;
+        }
+        private Date mDate = new Date(); // Used for converting timestamps for display
+
+        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 dateTextView = (TextView) v.findViewById(
+                    R.id.album_set_item_date);
+            long timestamp = cursor.getLong(AlbumSetLoader.INDEX_TIMESTAMP);
+            if (timestamp > 0) {
+                mDate.setTime(timestamp);
+                dateTextView.setText(DateFormat.getMediumDateFormat(context).format(mDate));
+            } else {
+                dateTextView.setText(null);
+            }
+
+            ProgressBar uploadProgressBar = (ProgressBar) v.findViewById(
+                    R.id.album_set_item_upload_progress);
+            if (cursor.getInt(AlbumSetLoader.INDEX_COUNT_PENDING_UPLOAD) > 0) {
+                uploadProgressBar.setVisibility(View.VISIBLE);
+                uploadProgressBar.setProgress(50);
+            } else {
+                uploadProgressBar.setVisibility(View.INVISIBLE);
+            }
+
+            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/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java
new file mode 100644
index 0000000..1c71151
--- /dev/null
+++ b/src/com/android/photos/BitmapRegionTileSource.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.photos;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.util.Log;
+import com.android.photos.views.TiledImageRenderer;
+
+import java.io.IOException;
+
+public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
+
+    BitmapRegionDecoder mDecoder;
+
+
+    public BitmapRegionTileSource(String path) {
+        try {
+            mDecoder = BitmapRegionDecoder.newInstance(path, true);
+        } catch (IOException e) {
+            Log.w("BitmapRegionTileSource", "ctor failed", e);
+        }
+    }
+
+    @Override
+    public int getTileSize() {
+        return 256;
+    }
+
+    @Override
+    public int getImageWidth() {
+        return mDecoder.getWidth();
+    }
+
+    @Override
+    public int getImageHeight() {
+        return mDecoder.getHeight();
+    }
+
+    @Override
+    public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
+        int tileSize = getTileSize();
+        int t = tileSize << level;
+
+        Rect wantRegion = new Rect(x, y, x + t, y + t);
+
+        if (bitmap == null) {
+            bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
+        }
+
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        options.inPreferQualityOverSpeed = true;
+        options.inSampleSize =  (1 << level);
+        options.inBitmap = bitmap;
+
+        try {
+            // In CropImage, we may call the decodeRegion() concurrently.
+            bitmap = mDecoder.decodeRegion(wantRegion, options);
+        } finally {
+            if (options.inBitmap != bitmap && options.inBitmap != null) {
+                options.inBitmap = null;
+            }
+        }
+
+        if (bitmap == null) {
+            Log.w("BitmapRegionTileSource", "fail in decoding region");
+        }
+        return bitmap;
+    }
+}
diff --git a/src/com/android/photos/FullscreenViewer.java b/src/com/android/photos/FullscreenViewer.java
new file mode 100644
index 0000000..50ea1ba
--- /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(path));
+        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..46b5140
--- /dev/null
+++ b/src/com/android/photos/GalleryActivity.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.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.view.Menu;
+import android.view.MenuItem;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.R;
+
+public class GalleryActivity extends Activity {
+
+    private final String FTAG_PHOTOSET = "PhotoSet";
+    private final String FTAG_ALBUMSET = "AlbumSet";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setupActionBar();
+    }
+
+    private void setupActionBar() {
+        ActionBar ab = getActionBar();
+        ab.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+        ab.setDisplayShowHomeEnabled(false);
+        ab.setDisplayShowTitleEnabled(false);
+        Tab tab = ab.newTab();
+        tab.setText(R.string.tab_photos);
+        tab.setTabListener(new TabListener<PhotoSetFragment>(this,
+                FTAG_PHOTOSET, PhotoSetFragment.class));
+        ab.addTab(tab, true);
+        tab = ab.newTab();
+        tab.setText(R.string.tab_albums);
+        tab.setTabListener(new TabListener<AlbumSetFragment>(this,
+                FTAG_ALBUMSET, AlbumSetFragment.class));
+        ab.addTab(tab);
+    }
+
+    @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);
+        }
+    }
+
+    private static class TabListener<T extends Fragment> implements ActionBar.TabListener {
+        private Fragment mFragment;
+        private final Activity mActivity;
+        private final String mTag;
+        private final Class<T> mClass;
+
+        /** Constructor used each time a new tab is created.
+          * @param activity  The host Activity, used to instantiate the fragment
+          * @param tag  The identifier tag for the fragment
+          * @param clz  The fragment's Class, used to instantiate the fragment
+          */
+        public TabListener(Activity activity, String tag, Class<T> clz) {
+            mActivity = activity;
+            mTag = tag;
+            mClass = clz;
+        }
+
+        /* The following are each of the ActionBar.TabListener callbacks */
+
+        @Override
+        public void onTabSelected(Tab tab, FragmentTransaction ft) {
+            // Check if the fragment is already initialized
+            if (mFragment == null) {
+                // If not, instantiate and add it to the activity
+                mFragment = Fragment.instantiate(mActivity, mClass.getName());
+                ft.add(android.R.id.content, mFragment, mTag);
+            } else {
+                // If it exists, simply attach it in order to show it
+                ft.attach(mFragment);
+            }
+        }
+
+        @Override
+        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+            if (mFragment != null) {
+                // Detach the fragment, because another one is being attached
+                ft.detach(mFragment);
+            }
+        }
+
+        @Override
+        public void onTabReselected(Tab tab, FragmentTransaction ft) {
+            // User selected the already selected tab. Usually do nothing.
+        }
+    }
+}
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..1de8de5
--- /dev/null
+++ b/src/com/android/photos/PhotoSetFragment.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.CursorAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.drawables.DrawableFactory;
+import com.android.photos.shims.MediaItemsLoader;
+import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter;
+
+
+public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor> {
+
+    private static final int LOADER_PHOTOSET = 1;
+
+    private GridView mPhotoSetView;
+    private View mEmptyView;
+    private ThumbnailAdapter mAdapter;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View root = inflater.inflate(R.layout.photo_set, container, false);
+        mPhotoSetView = (GridView) root.findViewById(android.R.id.list);
+        // TODO: Remove once UI stabilizes
+        mPhotoSetView.setColumnWidth(MediaItemsLoader.getThumbnailSize());
+        mEmptyView = root.findViewById(android.R.id.empty);
+        mEmptyView.setVisibility(View.GONE);
+        mAdapter = new ThumbnailAdapter(getActivity());
+        mPhotoSetView.setAdapter(mAdapter);
+        getLoaderManager().initLoader(LOADER_PHOTOSET, null, this);
+        updateEmptyStatus();
+        return root;
+    }
+
+    private void updateEmptyStatus() {
+        boolean empty = (mAdapter == null || mAdapter.getCount() == 0);
+        mPhotoSetView.setVisibility(empty ? View.GONE : View.VISIBLE);
+        mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        // TODO: Switch to PhotoSetLoader
+        MediaItemsLoader loader = new MediaItemsLoader(getActivity());
+        mAdapter.setDrawableFactory(loader);
+        return loader;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader,
+            Cursor data) {
+        mAdapter.swapCursor(data);
+        updateEmptyStatus();
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    private static class ThumbnailAdapter extends CursorAdapter implements GalleryThumbnailAdapter {
+        private LayoutInflater mInflater;
+        private DrawableFactory<Cursor> mDrawableFactory;
+
+        public ThumbnailAdapter(Context context) {
+            super(context, null, false);
+            mInflater = LayoutInflater.from(context);
+        }
+
+        public void setDrawableFactory(DrawableFactory<Cursor> factory) {
+            mDrawableFactory = factory;
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            ImageView iv = (ImageView) view;
+            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);
+            LayoutParams params = view.getLayoutParams();
+            int columnWidth = ((GridView) parent).getColumnWidth();
+            params.height = columnWidth;
+            view.setLayoutParams(params);
+            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);
+        }
+    }
+}
diff --git a/src/com/android/photos/canvas/CanvasActivity.java b/src/com/android/photos/canvas/CanvasActivity.java
index 17afdb8..ad3cb63 100644
--- a/src/com/android/photos/canvas/CanvasActivity.java
+++ b/src/com/android/photos/canvas/CanvasActivity.java
@@ -20,14 +20,14 @@
 import android.content.Intent;
 import android.os.Bundle;
 
-import com.google.android.canvas.provider.CanvasContract;
+import com.google.android.pano.provider.PanoContract;
 
 
 public class CanvasActivity extends Activity {
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        Intent intent = CanvasContract.getBrowseIntent(
+        Intent intent = PanoContract.getBrowseIntent(
                 CanvasProvider.BROWSER_ROOT_URI, 0);
         startActivity(intent);
         finish();
diff --git a/src/com/android/photos/canvas/CanvasProvider.java b/src/com/android/photos/canvas/CanvasProvider.java
index 1ed6cd4..1bc5566 100644
--- a/src/com/android/photos/canvas/CanvasProvider.java
+++ b/src/com/android/photos/canvas/CanvasProvider.java
@@ -42,8 +42,8 @@
 import com.android.gallery3d.util.ThreadPool.CancelListener;
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
-import com.google.android.canvas.data.Cluster;
-import com.google.android.canvas.provider.CanvasContract;
+import com.google.android.pano.data.Cluster;
+import com.google.android.pano.provider.PanoContract;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -142,7 +142,7 @@
             Cluster.Builder bob = new Cluster.Builder();
             bob.id(i);
             bob.displayName(set.getName());
-            Intent intent = CanvasContract.getBrowseIntent(BROWSER_ROOT_URI, i);
+            Intent intent = PanoContract.getBrowseIntent(BROWSER_ROOT_URI, i);
             bob.intent(intent);
             bob.imageCropAllowed(true);
             bob.cacheTimeMs(CACHE_TIME_MS);
diff --git a/src/com/android/photos/canvas/CanvasProviderBase.java b/src/com/android/photos/canvas/CanvasProviderBase.java
index 4438c53..a38aae5 100644
--- a/src/com/android/photos/canvas/CanvasProviderBase.java
+++ b/src/com/android/photos/canvas/CanvasProviderBase.java
@@ -26,8 +26,8 @@
 import android.os.Binder;
 import android.provider.BaseColumns;
 
-import com.google.android.canvas.data.Cluster;
-import com.google.android.canvas.provider.CanvasContract;
+import com.google.android.pano.data.Cluster;
+import com.google.android.pano.provider.PanoContract;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -45,10 +45,10 @@
     protected static final String PATH_IMAGE = "image";
     protected static final String PATH_LAUNCHER = "launcher";
     protected static final String PATH_LAUNCHER_ITEM = PATH_LAUNCHER + "/"
-            + CanvasContract.PATH_LAUNCHER_ITEM;
+            + PanoContract.PATH_LAUNCHER_ITEM;
     protected static final String PATH_BROWSE = "browse";
     protected static final String PATH_BROWSE_HEADERS = PATH_BROWSE + "/"
-            + CanvasContract.PATH_BROWSE_HEADERS;
+            + PanoContract.PATH_BROWSE_HEADERS;
 
     public static final Uri BROWSER_ROOT_URI = Uri.parse("content://"
             + AUTHORITY + "/" + PATH_BROWSE);
@@ -91,19 +91,19 @@
     static {
         LAUNCHER_COLUMN_CASES.put(BaseColumns._ID, LAUNCHER_CASE_ID);
         LAUNCHER_COLUMN_CASES.put(BaseColumns._COUNT, LAUNCHER_CASE_COUNT);
-        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.NAME,
+        LAUNCHER_COLUMN_CASES.put(PanoContract.Launcher.NAME,
                 LAUNCHER_CASE_NAME);
-        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.IMPORTANCE,
+        LAUNCHER_COLUMN_CASES.put(PanoContract.Launcher.IMPORTANCE,
                 LAUNCHER_CASE_IMPORTANCE);
-        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.DISPLAY_NAME,
+        LAUNCHER_COLUMN_CASES.put(PanoContract.Launcher.DISPLAY_NAME,
                 LAUNCHER_CASE_DISPLAY_NAME);
-        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.VISIBLE_COUNT,
+        LAUNCHER_COLUMN_CASES.put(PanoContract.Launcher.VISIBLE_COUNT,
                 LAUNCHER_CASE_VISIBLE_COUNT);
-        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.IMAGE_CROP_ALLOWED,
+        LAUNCHER_COLUMN_CASES.put(PanoContract.Launcher.IMAGE_CROP_ALLOWED,
                 LAUNCHER_CASE_CROP_ALLOWED);
-        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.CACHE_TIME_MS,
+        LAUNCHER_COLUMN_CASES.put(PanoContract.Launcher.CACHE_TIME_MS,
                 LAUNCHER_CASE_CACHE_TIME);
-        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.INTENT_URI,
+        LAUNCHER_COLUMN_CASES.put(PanoContract.Launcher.INTENT_URI,
                 LAUNCHER_CASE_INTENT_URI);
 
         LAUNCHER_PROJECTION_ALL = LAUNCHER_COLUMN_CASES.keySet().toArray(
@@ -120,9 +120,9 @@
     static {
         CLUSTER_COLUMN_CASES.put(BaseColumns._ID, CLUSTER_CASE_ID);
         CLUSTER_COLUMN_CASES.put(BaseColumns._COUNT, CLUSTER_CASE_COUNT);
-        CLUSTER_COLUMN_CASES.put(CanvasContract.LauncherItem.PARENT_ID,
+        CLUSTER_COLUMN_CASES.put(PanoContract.LauncherItem.PARENT_ID,
                 CLUSTER_CASE_PARENT_ID);
-        CLUSTER_COLUMN_CASES.put(CanvasContract.LauncherItem.IMAGE_URI,
+        CLUSTER_COLUMN_CASES.put(PanoContract.LauncherItem.IMAGE_URI,
                 CLUSTER_CASE_IMAGE_URI);
 
         CLUSTER_PROJECTION_ALL = CLUSTER_COLUMN_CASES.keySet().toArray(
@@ -149,33 +149,33 @@
         BROWSE_HEADER_COLUMN_CASES.put(BaseColumns._ID, BROWSE_HEADER_CASE_ID);
         BROWSE_HEADER_COLUMN_CASES.put(BaseColumns._COUNT,
                 BROWSE_HEADER_CASE_COUNT);
-        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.NAME,
+        BROWSE_HEADER_COLUMN_CASES.put(PanoContract.BrowseHeaders.NAME,
                 BROWSE_HEADER_CASE_NAME);
         BROWSE_HEADER_COLUMN_CASES.put(
-                CanvasContract.BrowseHeaders.DISPLAY_NAME,
+                PanoContract.BrowseHeaders.DISPLAY_NAME,
                 BROWSE_HEADER_CASE_DISPLAY_NAME);
-        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.ICON_URI,
+        BROWSE_HEADER_COLUMN_CASES.put(PanoContract.BrowseHeaders.ICON_URI,
                 BROWSE_HEADER_CASE_ICON_URI);
-        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.BADGE_URI,
+        BROWSE_HEADER_COLUMN_CASES.put(PanoContract.BrowseHeaders.BADGE_URI,
                 BROWSE_HEADER_CASE_BADGE_URI);
-        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.COLOR_HINT,
+        BROWSE_HEADER_COLUMN_CASES.put(PanoContract.BrowseHeaders.COLOR_HINT,
                 BROWSE_HEADER_CASE_COLOR_HINT);
         BROWSE_HEADER_COLUMN_CASES.put(
-                CanvasContract.BrowseHeaders.TEXT_COLOR_HINT,
+                PanoContract.BrowseHeaders.TEXT_COLOR_HINT,
                 BROWSE_HEADER_CASE_TEXT_COLOR_HINT);
         BROWSE_HEADER_COLUMN_CASES.put(
-                CanvasContract.BrowseHeaders.BG_IMAGE_URI,
+                PanoContract.BrowseHeaders.BG_IMAGE_URI,
                 BROWSE_HEADER_CASE_BG_IMAGE_URI);
         BROWSE_HEADER_COLUMN_CASES.put(
-                CanvasContract.BrowseHeaders.EXPAND_GROUP,
+                PanoContract.BrowseHeaders.EXPAND_GROUP,
                 BROWSE_HEADER_CASE_EXPAND_GROUP);
-        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.WRAP_ITEMS,
+        BROWSE_HEADER_COLUMN_CASES.put(PanoContract.BrowseHeaders.WRAP_ITEMS,
                 BROWSE_HEADER_CASE_WRAP);
         BROWSE_HEADER_COLUMN_CASES.put(
-                CanvasContract.BrowseHeaders.DEFAULT_ITEM_WIDTH,
+                PanoContract.BrowseHeaders.DEFAULT_ITEM_WIDTH,
                 BROWSE_HEADER_CASE_DEFAULT_ITEM_WIDTH);
         BROWSE_HEADER_COLUMN_CASES.put(
-                CanvasContract.BrowseHeaders.DEFAULT_ITEM_HEIGHT,
+                PanoContract.BrowseHeaders.DEFAULT_ITEM_HEIGHT,
                 BROWSE_HEADER_CASE_DEFAULT_ITEM_HEIGHT);
 
         BROWSE_HEADER_PROJECTION_ALL = BROWSE_HEADER_COLUMN_CASES.keySet()
@@ -197,19 +197,19 @@
     static {
         BROWSE_COLUMN_CASES.put(BaseColumns._ID, BROWSE_CASE_ID);
         BROWSE_COLUMN_CASES.put(BaseColumns._COUNT, BROWSE_CASE_COUNT);
-        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.PARENT_ID,
+        BROWSE_COLUMN_CASES.put(PanoContract.BrowseItems.PARENT_ID,
                 BROWSE_CASE_PARENT_ID);
-        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.DISPLAY_NAME,
+        BROWSE_COLUMN_CASES.put(PanoContract.BrowseItems.DISPLAY_NAME,
                 BROWSE_CASE_DISPLAY_NAME);
-        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.DISPLAY_DESCRIPTION,
+        BROWSE_COLUMN_CASES.put(PanoContract.BrowseItems.DISPLAY_DESCRIPTION,
                 BROWSE_CASE_DISPLAY_DESCRIPTION);
-        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.IMAGE_URI,
+        BROWSE_COLUMN_CASES.put(PanoContract.BrowseItems.IMAGE_URI,
                 BROWSE_CASE_IMAGE_URI);
-        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.WIDTH,
+        BROWSE_COLUMN_CASES.put(PanoContract.BrowseItems.WIDTH,
                 BROWSE_CASE_WIDTH);
-        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.HEIGHT,
+        BROWSE_COLUMN_CASES.put(PanoContract.BrowseItems.HEIGHT,
                 BROWSE_CASE_HEIGHT);
-        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.INTENT_URI,
+        BROWSE_COLUMN_CASES.put(PanoContract.BrowseItems.INTENT_URI,
                 BROWSE_CASE_INTENT_URI);
 
         BROWSE_PROJECTION_ALL = BROWSE_COLUMN_CASES.keySet().toArray(
diff --git a/src/com/android/photos/data/AlbumSetLoader.java b/src/com/android/photos/data/AlbumSetLoader.java
new file mode 100644
index 0000000..b2b5204
--- /dev/null
+++ b/src/com/android/photos/data/AlbumSetLoader.java
@@ -0,0 +1,51 @@
+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 String[] PROJECTION = {
+        "_id",
+        "title",
+        "timestamp",
+        "thumb_uri",
+        "thumb_width",
+        "thumb_height",
+        "count_pending_upload",
+        "_count"
+    };
+    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
+        };
+        return row;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/photos/data/GalleryBitmapPool.java b/src/com/android/photos/data/GalleryBitmapPool.java
index cddc160..4c3279f 100644
--- a/src/com/android/photos/data/GalleryBitmapPool.java
+++ b/src/com/android/photos/data/GalleryBitmapPool.java
@@ -46,11 +46,9 @@
         mCapacityBytes = capacityBytes;
     }
 
-    private static GalleryBitmapPool sInstance;
+    private static GalleryBitmapPool sInstance = new GalleryBitmapPool(CAPACITY_BYTES);
+
     public static GalleryBitmapPool getInstance() {
-        if (sInstance == null) {
-            sInstance = new GalleryBitmapPool(CAPACITY_BYTES);
-        }
         return sInstance;
     }
 
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..a87f00b
--- /dev/null
+++ b/src/com/android/photos/data/PhotoDatabase.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 = 1;
+
+    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.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 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) {
+        super(context, dbName, null, DB_VERSION);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+    }
+
+    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);
+            }
+        }
+    }
+}
diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java
new file mode 100644
index 0000000..cecfe5e
--- /dev/null
+++ b/src/com/android/photos/data/PhotoProvider.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 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 {
+        /** Internal database table used for basic photo information. */
+        public static final String TABLE = "photo";
+        /** 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 = "album";
+        /** 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";
+        /**
+         * 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;
+    }
+
+    /**
+     * Contains columns and Uri for maintaining the image cache.
+     */
+    public static interface ImageCache extends BaseColumns {
+        /** Internal database table used for the image cache */
+        public static final String TABLE = "image_cache";
+
+        /**
+         * The image_type query parameter required for accessing a specific
+         * image
+         */
+        public static final String IMAGE_TYPE_QUERY_PARAMETER = "image_type";
+
+        // ImageCache.IMAGE_TYPE values
+        public static final int IMAGE_TYPE_ALBUM_COVER = 1;
+        public static final int IMAGE_TYPE_THUMBNAIL = 2;
+        public static final int IMAGE_TYPE_PREVIEW = 3;
+        public static final int IMAGE_TYPE_ORIGINAL = 4;
+
+        /**
+         * Content URI for retrieving image paths. The
+         * IMAGE_TYPE_QUERY_PARAMETER must be used in queries.
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+
+        /**
+         * Content URI for retrieving the album cover art. The album ID must be
+         * appended to the URI.
+         */
+        public static final Uri ALBUM_COVER_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI,
+                Albums.TABLE);
+
+        /**
+         * An _ID from Albums or Photos, depending on whether IMAGE_TYPE is
+         * IMAGE_TYPE_ALBUM or not. Long value.
+         */
+        public static final String REMOTE_ID = "remote_id";
+        /** One of IMAGE_TYPE_* values. */
+        public static final String IMAGE_TYPE = "image_type";
+        /** The String path to the image. */
+        public static final String PATH = "path";
+    };
+
+    // 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 = ")";
+
+    /**
+     * For selecting the mime-type for an image.
+     */
+    private static final String[] PROJECTION_MIME_TYPE = {
+        Photos.MIME_TYPE,
+    };
+
+    private 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_IMAGE = 7;
+    protected static final int MATCH_ALBUM_COVER = 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);
+        // match against image_cache/<ImageCache.PHOTO_ID>
+        sUriMatcher.addURI(AUTHORITY, ImageCache.TABLE + "/#", MATCH_IMAGE);
+        // match against image_cache/album/<Albums._ID>
+        sUriMatcher.addURI(AUTHORITY, ImageCache.TABLE + "/" + Albums.TABLE + "/#",
+                MATCH_ALBUM_COVER);
+    }
+
+    @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);
+        int deleted = 0;
+        SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+        deleted = deleteCascade(db, match, selection, selectionArgs, uri);
+        return deleted;
+    }
+
+    @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) {
+        int match = matchUri(uri);
+        selection = addIdToSelection(match, selection);
+        selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
+        String table = getTableFromMatch(match, uri);
+        SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
+        return db.query(false, table, projection, selection, selectionArgs, null, null, sortOrder,
+                null, cancellationSignal);
+    }
+
+    @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;
+            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);
+        }
+        if (match == MATCH_IMAGE || match == MATCH_ALBUM_COVER) {
+            throw new IllegalArgumentException("Operation not allowed on image cache database");
+        }
+        return match;
+    }
+
+    @Override
+    protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) {
+        if (mNotifier != null) {
+            mNotifier.notifyChange(uri, syncToNetwork);
+        } else {
+            resolver.notifyChange(uri, null, 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 int deleteCascade(SQLiteDatabase db, int match, String selection,
+            String[] selectionArgs, Uri uri) {
+        switch (match) {
+            case MATCH_PHOTO:
+            case MATCH_PHOTO_ID: {
+                deleteCascadeMetadata(db, selection, selectionArgs);
+                break;
+            }
+            case MATCH_ALBUM:
+            case MATCH_ALBUM_ID: {
+                deleteCascadePhotos(db, selection, selectionArgs);
+                break;
+            }
+        }
+        String table = getTableFromMatch(match, uri);
+        int deleted = db.delete(table, selection, selectionArgs);
+        if (deleted > 0) {
+            postNotifyUri(uri);
+        }
+        return deleted;
+    }
+
+    private void deleteCascadePhotos(SQLiteDatabase db, String albumSelect,
+            String[] selectArgs) {
+        String photoWhere = nestWhere(Photos.ALBUM_ID, Albums.TABLE, albumSelect);
+        deleteCascadeMetadata(db, photoWhere, selectArgs);
+        int deleted = db.delete(Photos.TABLE, photoWhere, selectArgs);
+        if (deleted > 0) {
+            postNotifyUri(Photos.CONTENT_URI);
+        }
+    }
+
+    private void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect,
+            String[] selectArgs) {
+        String metadataWhere = nestWhere(Metadata.PHOTO_ID, Photos.TABLE, photosSelect);
+        int deleted = db.delete(Metadata.TABLE, metadataWhere, selectArgs);
+        if (deleted > 0) {
+            postNotifyUri(Metadata.CONTENT_URI);
+        }
+    }
+
+    private static void validateMatchTable(int match) {
+        switch (match) {
+            case MATCH_PHOTO:
+            case MATCH_ALBUM:
+            case MATCH_METADATA:
+                break;
+            default:
+                throw new IllegalArgumentException("Operation not allowed on an existing row.");
+        }
+    }
+}
diff --git a/src/com/android/photos/data/PhotoSetLoader.java b/src/com/android/photos/data/PhotoSetLoader.java
new file mode 100644
index 0000000..21da906
--- /dev/null
+++ b/src/com/android/photos/data/PhotoSetLoader.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.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.drawables.DrawableFactory;
+
+public class PhotoSetLoader extends CursorLoader implements DrawableFactory<Cursor> {
+
+    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,
+    };
+    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;
+
+    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;
+    }
+}
diff --git a/src/com/android/photos/data/SQLiteContentProvider.java b/src/com/android/photos/data/SQLiteContentProvider.java
new file mode 100644
index 0000000..ecd868b
--- /dev/null
+++ b/src/com/android/photos/data/SQLiteContentProvider.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 void 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);
+        }
+    }
+
+    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/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/drawables/DrawableFactory.java b/src/com/android/photos/drawables/DrawableFactory.java
new file mode 100644
index 0000000..ad046c8
--- /dev/null
+++ b/src/com/android/photos/drawables/DrawableFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.drawable.Drawable;
+
+
+public interface DrawableFactory<T> {
+    Drawable drawableForItem(T item, Drawable recycle);
+}
diff --git a/src/com/android/photos/drawables/MtpThumbnailDrawable.java b/src/com/android/photos/drawables/MtpThumbnailDrawable.java
new file mode 100644
index 0000000..e35e069
--- /dev/null
+++ b/src/com/android/photos/drawables/MtpThumbnailDrawable.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+
+import java.io.InputStream;
+
+public class MtpThumbnailDrawable extends AutoThumbnailDrawable<MtpObjectInfo> {
+    public void setImage(MtpObjectInfo data) {
+        if (data == null) {
+            setImage(null, 0, 0);
+        } else {
+            setImage(data, data.getImagePixWidth(), data.getImagePixHeight());
+        }
+    }
+
+    @Override
+    protected byte[] getPreferredImageBytes(MtpObjectInfo data) {
+        if (data == null) {
+            return null;
+        }
+        MtpDevice device = MtpDeviceIndex.getInstance().getDevice();
+        if (device != null) {
+            return device.getThumbnail(data.getObjectHandle());
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    protected InputStream getFallbackImageStream(MtpObjectInfo data) {
+        // No fallback
+        return null;
+    }
+
+    @Override
+    protected boolean dataChangedLocked(MtpObjectInfo data) {
+        // We only fetch the MtpObjectInfo once when creating
+        // the index so checking the reference is enough
+        return 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..299becb
--- /dev/null
+++ b/src/com/android/photos/shims/BitmapJobDrawable.java
@@ -0,0 +1,158 @@
+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;
+import com.android.photos.drawables.AutoThumbnailDrawable;
+
+
+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();
+
+    public BitmapJobDrawable() {
+    }
+
+    public void setMediaItem(MediaItem item) {
+        if (mLoader != null) {
+            mLoader.cancelLoad();
+        }
+        mItem = item;
+        if (mBitmap != null) {
+            GalleryBitmapPool.getInstance().put(mBitmap);
+            mBitmap = null;
+        }
+        // TODO: Figure out why ThumbnailLoader doesn't like to be re-used
+        mLoader = new ThumbnailLoader(this);
+        mLoader.startLoad();
+        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.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/MediaItemsLoader.java b/src/com/android/photos/shims/MediaItemsLoader.java
new file mode 100644
index 0000000..886b3c3
--- /dev/null
+++ b/src/com/android/photos/shims/MediaItemsLoader.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.provider.MediaStore.Files.FileColumns;
+
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSet.SyncListener;
+import com.android.gallery3d.util.Future;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.drawables.DrawableFactory;
+
+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 DrawableFactory<Cursor> {
+
+    private static final SyncListener sNullListener = new SyncListener() {
+        @Override
+        public void onSyncDone(MediaSet mediaSet, int resultCode) {
+        }
+    };
+
+    private MediaSet mMediaSet;
+    private Future<Integer> mSyncTask = null;
+    private ContentListener mObserver = new ContentListener() {
+        @Override
+        public void onContentDirty() {
+            onContentChanged();
+        }
+    };
+    private ArrayList<MediaItem> mMediaItems = new ArrayList<MediaItem>();
+
+    public MediaItemsLoader(Context context) {
+        super(context);
+        DataManager dm = DataManager.from(context);
+        String path = dm.getTopSetPath(DataManager.INCLUDE_ALL);
+        mMediaSet = dm.getMediaSet(path);
+    }
+
+    public MediaItemsLoader(Context context, String parentPath) {
+        super(context);
+        mMediaSet = DataManager.from(getContext()).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() {
+        mMediaSet.loadIfDirty();
+        final MatrixCursor cursor = new MatrixCursor(PhotoSetLoader.PROJECTION);
+        final Object[] row = new Object[PhotoSetLoader.PROJECTION.length];
+        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;
+                cursor.addRow(row);
+                mMediaItems.add(item);
+            }
+        });
+        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);
+    }
+
+}
diff --git a/src/com/android/photos/shims/MediaSetLoader.java b/src/com/android/photos/shims/MediaSetLoader.java
new file mode 100644
index 0000000..7a6fcb8
--- /dev/null
+++ b/src/com/android/photos/shims/MediaSetLoader.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.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.provider.MediaStore.Files.FileColumns;
+
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSet.SyncListener;
+import com.android.gallery3d.util.Future;
+import com.android.photos.data.AlbumSetLoader;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.drawables.DrawableFactory;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+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 DrawableFactory<Cursor>{
+
+    private static final SyncListener sNullListener = new SyncListener() {
+        @Override
+        public void onSyncDone(MediaSet mediaSet, int resultCode) {
+        }
+    };
+
+    private MediaSet mMediaSet;
+    private Future<Integer> mSyncTask = null;
+    private ContentListener mObserver = new ContentListener() {
+        @Override
+        public void onContentDirty() {
+            onContentChanged();
+        }
+    };
+
+    private ArrayList<MediaItem> mCoverItems = new ArrayList<MediaItem>();
+
+    public MediaSetLoader(Context context) {
+        super(context);
+        DataManager dm = DataManager.from(context);
+        String path = dm.getTopSetPath(DataManager.INCLUDE_ALL);
+        mMediaSet = dm.getMediaSet(path);
+    }
+
+    public MediaSetLoader(Context context, String path) {
+        super(context);
+        mMediaSet = DataManager.from(getContext()).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() {
+        mMediaSet.loadIfDirty();
+        final MatrixCursor cursor = new MatrixCursor(AlbumSetLoader.PROJECTION);
+        final Object[] row = new Object[AlbumSetLoader.PROJECTION.length];
+        int count = mMediaSet.getSubMediaSetCount();
+        for (int i = 0; i < count; i++) {
+            MediaSet m = mMediaSet.getSubMediaSet(i);
+            m.loadIfDirty();
+            row[AlbumSetLoader.INDEX_ID] = i;
+            row[AlbumSetLoader.INDEX_TITLE] = m.getName();
+            row[AlbumSetLoader.INDEX_COUNT] = m.getMediaItemCount();
+            MediaItem coverItem = m.getCoverMediaItem();
+            row[AlbumSetLoader.INDEX_TIMESTAMP] = coverItem.getDateInMs();
+            mCoverItems.add(coverItem);
+            cursor.addRow(row);
+        }
+        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);
+    }
+}
diff --git a/src/com/android/photos/views/BlockingGLTextureView.java b/src/com/android/photos/views/BlockingGLTextureView.java
new file mode 100644
index 0000000..c38f8f7
--- /dev/null
+++ b/src/com/android/photos/views/BlockingGLTextureView.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.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;
+
+
+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) {}
+        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[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+            return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+        }
+
+        /**
+         * 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) {}
+        }
+
+        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) {}
+            }
+        }
+
+        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) {}
+                    }
+                    handleMessageLocked(mExecMsgId);
+                    mExecMsgId = INVALID;
+                    mLock.notify();
+                }
+            }
+        }
+    }
+}
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/TiledImageRenderer.java b/src/com/android/photos/views/TiledImageRenderer.java
new file mode 100644
index 0000000..a1f7107
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageRenderer.java
@@ -0,0 +1,751 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.view.View;
+import android.view.WindowManager;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.photos.data.GalleryBitmapPool;
+
+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 GalleryBitmapPool sTilePool = GalleryBitmapPool.getInstance();
+
+    // TILE_SIZE must be 2^N
+    private int mTileSize;
+
+    private TileSource mModel;
+    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;
+
+    public static interface TileSource {
+        public int getTileSize();
+        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 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;
+            invalidate();
+        }
+    }
+
+    private static int calulateLevelCount(TileSource source) {
+        int levels = 1;
+        int maxDim = Math.max(source.getImageWidth(), source.getImageHeight());
+        int t = source.getTileSize();
+        while (t < maxDim) {
+            t <<= 1;
+            levels++;
+        }
+        return levels;
+    }
+
+    public void notifyModelInvalidated() {
+        invalidateTiles();
+        if (mModel == null) {
+            mImageWidth = 0;
+            mImageHeight = 0;
+            mLevelCount = 0;
+        } else {
+            mImageWidth = mModel.getImageWidth();
+            mImageHeight = mModel.getImageHeight();
+            mLevelCount = calulateLevelCount(mModel);
+            mTileSize = mModel.getTileSize();
+        }
+        mLayoutTiles = true;
+        invalidate();
+    }
+
+    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;
+        invalidate();
+    }
+
+    // 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 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;
+
+        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);
+
+        if (sTilePool != null) sTilePool.clear();
+    }
+
+    public void 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);
+                    }
+                }
+            }
+        } finally {
+            if (flags != 0) canvas.restore();
+        }
+
+        if (mRenderComplete) {
+            if (!mBackgroundTileUploaded) {
+                uploadBackgroundTiles(canvas);
+            }
+        } else {
+            invalidate();
+        }
+    }
+
+    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 queueForUpload(Tile tile) {
+        synchronized (mQueueLock) {
+            mUploadQueue.push(tile);
+        }
+        invalidate();
+        // TODO
+//        if (mTileUploader.mActive.compareAndSet(false, true)) {
+//            getGLRoot().addOnGLIdleListener(mTileUploader);
+//        }
+    }
+
+   private void queueForDecode(Tile tile) {
+       synchronized (mQueueLock) {
+           if (tile.mTileState == STATE_ACTIVATED) {
+               tile.mTileState = STATE_IN_QUEUE;
+               if (mDecodeQueue.push(tile)) {
+                   mQueueLock.notifyAll();
+               }
+           }
+       }
+    }
+
+    private boolean decodeTile(Tile tile) {
+        synchronized (mQueueLock) {
+            if (tile.mTileState != STATE_IN_QUEUE) return false;
+            tile.mTileState = STATE_DECODING;
+        }
+        boolean decodeComplete = tile.decode();
+        synchronized (mQueueLock) {
+            if (tile.mTileState == STATE_RECYCLING) {
+                tile.mTileState = STATE_RECYCLED;
+                if (tile.mDecodedTile != null) {
+                    if (sTilePool != null) sTilePool.put(tile.mDecodedTile);
+                    tile.mDecodedTile = null;
+                }
+                mRecycledQueue.push(tile);
+                return false;
+            }
+            tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
+            return decodeComplete;
+        }
+    }
+
+    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) {
+                if (sTilePool != null) sTilePool.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 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()) {
+                Utils.assertTrue(tile.mTileState == STATE_DECODED);
+                tile.updateContent(canvas);
+                --quota;
+            }
+        }
+        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);
+                }
+            }
+            drawTile(tile, canvas, 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) {
+            if (sTilePool != null) sTilePool.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 {
+                Bitmap reuse = sTilePool.get(mTileSize, mTileSize);
+                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) {
+            boolean wasEmpty = mHead == null;
+            tile.mNext = mHead;
+            mHead = tile;
+            return wasEmpty;
+        }
+
+        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();
+                    if (decodeTile(tile)) {
+                        queueForUpload(tile);
+                    }
+                }
+            } catch (InterruptedException ex) {
+            }
+        }
+
+    }
+}
diff --git a/src/com/android/photos/views/TiledImageView.java b/src/com/android/photos/views/TiledImageView.java
new file mode 100644
index 0000000..6fe030d
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageView.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.opengl.GLSurfaceView.Renderer;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
+import android.widget.FrameLayout;
+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;
+
+
+public class TiledImageView extends FrameLayout implements OnScaleGestureListener {
+
+    private BlockingGLTextureView mTextureView;
+    private float mLastX, mLastY;
+
+    private static class ImageRendererWrapper {
+        // Guarded by locks
+        float scale;
+        int centerX, centerY;
+        int rotation;
+        TileSource source;
+
+        // GL thread only
+        TiledImageRenderer image;
+    }
+
+    // TODO: left/right paging
+    private ImageRendererWrapper mRenderers[] = new ImageRendererWrapper[1];
+    private ImageRendererWrapper mFocusedRenderer;
+
+    // -------------------------
+    // Guarded by mLock
+    // -------------------------
+    private Object mLock = new Object();
+    private ScaleGestureDetector mScaleGestureDetector;
+
+    public TiledImageView(Context context) {
+        this(context, null);
+    }
+
+    public TiledImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mTextureView = new BlockingGLTextureView(context);
+        addView(mTextureView, new LayoutParams(
+                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+        mTextureView.setRenderer(new TileRenderer());
+        setTileSource(new ColoredTiles());
+        mScaleGestureDetector = new ScaleGestureDetector(context, this);
+    }
+
+    public void destroy() {
+        mTextureView.destroy();
+    }
+
+    public void setTileSource(TileSource source) {
+        synchronized (mLock) {
+            for (int i = 0; i < mRenderers.length; i++) {
+                ImageRendererWrapper renderer = mRenderers[i];
+                if (renderer == null) {
+                    renderer = mRenderers[i] = new ImageRendererWrapper();
+                }
+                renderer.source = source;
+                renderer.centerX = renderer.source.getImageWidth() / 2;
+                renderer.centerY = renderer.source.getImageHeight() / 2;
+                renderer.rotation = 0;
+                renderer.scale = 0;
+                renderer.image = new TiledImageRenderer(this);
+                updateScaleIfNecessaryLocked(renderer);
+            }
+        }
+        mFocusedRenderer = mRenderers[0];
+        invalidate();
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        return true;
+    }
+
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+        // Don't need the lock because this will only fire inside of onTouchEvent
+        mFocusedRenderer.scale *= detector.getScaleFactor();
+        invalidate();
+        return true;
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int action = event.getActionMasked();
+        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
+
+        // Determine focal point
+        float sumX = 0, sumY = 0;
+        final int count = event.getPointerCount();
+        for (int i = 0; i < count; i++) {
+            if (skipIndex == i) continue;
+            sumX += event.getX(i);
+            sumY += event.getY(i);
+        }
+        final int div = pointerUp ? count - 1 : count;
+        float x = sumX / div;
+        float y = sumY / div;
+
+        synchronized (mLock) {
+            mScaleGestureDetector.onTouchEvent(event);
+            switch (action) {
+            case MotionEvent.ACTION_MOVE:
+                mFocusedRenderer.centerX += (mLastX - x) / mFocusedRenderer.scale;
+                mFocusedRenderer.centerY += (mLastY - y) / mFocusedRenderer.scale;
+                invalidate();
+                break;
+            }
+        }
+
+        mLastX = x;
+        mLastY = y;
+        return true;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right,
+            int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        synchronized (mLock) {
+            for (ImageRendererWrapper renderer : mRenderers) {
+                updateScaleIfNecessaryLocked(renderer);
+            }
+        }
+    }
+
+    private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
+        if (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) {
+        mTextureView.render();
+        super.dispatchDraw(canvas);
+    }
+
+    @Override
+    public void invalidate() {
+        super.invalidate();
+        mTextureView.invalidate();
+    }
+
+    private class TileRenderer implements Renderer {
+
+        private GLES20Canvas mCanvas;
+
+        @Override
+        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+            mCanvas = new GLES20Canvas();
+            for (ImageRendererWrapper renderer : mRenderers) {
+                renderer.image.setModel(renderer.source, renderer.rotation);
+            }
+        }
+
+        @Override
+        public void onSurfaceChanged(GL10 gl, int width, int height) {
+            mCanvas.setSize(width, height);
+            for (ImageRendererWrapper renderer : mRenderers) {
+                renderer.image.setViewSize(width, height);
+            }
+        }
+
+        @Override
+        public void onDrawFrame(GL10 gl) {
+            mCanvas.clearBuffer();
+            synchronized (mLock) {
+                for (ImageRendererWrapper renderer : mRenderers) {
+                    renderer.image.setModel(renderer.source, renderer.rotation);
+                    renderer.image.setPosition(renderer.centerX, renderer.centerY, renderer.scale);
+                }
+            }
+            for (ImageRendererWrapper renderer : mRenderers) {
+                renderer.image.draw(mCanvas);
+            }
+        }
+
+    }
+
+    private static class ColoredTiles implements TileSource {
+        private static 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 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;
+        }
+    }
+}
diff --git a/src/com/google/android/canvas/data/Cluster.java b/src/com/google/android/canvas/data/Cluster.java
deleted file mode 100644
index ab6aaed..0000000
--- a/src/com/google/android/canvas/data/Cluster.java
+++ /dev/null
@@ -1,195 +0,0 @@
-// Copyright 2012 Google Inc. All Rights Reserved.
-
-package com.google.android.canvas.data;
-
-import android.content.Intent;
-import android.net.Uri;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Represents a home screen cluster.
- */
-public class Cluster {
-
-    private long mId;
-    private String mName;
-    private CharSequence mDisplayName;
-    private int mImportance;
-    private int mVisibleCount;
-    private boolean mImageCropAllowed;
-    private long mCacheTimeMs;
-    private Intent mIntent;
-
-    private List<ClusterItem> mClusterItems;
-
-    /**
-     * An item displayed inside a cluster.
-     */
-    public static class ClusterItem {
-        private Uri mImageUri;
-
-        ClusterItem(Uri imageUri) {
-            mImageUri = imageUri;
-        }
-
-        public Uri getImageUri() {
-            return mImageUri;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder builder = new StringBuilder();
-            builder.append("imageUri: ").append(mImageUri);
-            return builder.toString();
-        }
-    }
-
-    public Cluster() {
-        mClusterItems = new ArrayList<ClusterItem>();
-        mImageCropAllowed = true;
-    }
-
-    public long getId() {
-        return mId;
-    }
-
-    public String getName() {
-        return mName;
-    }
-
-    public CharSequence getDisplayName() {
-        return mDisplayName;
-    }
-
-    public int getImportance() {
-        return mImportance;
-    }
-
-    public int getVisibleCount() {
-        return mVisibleCount;
-    }
-
-    public boolean isImageCropAllowed() {
-        return mImageCropAllowed;
-    }
-
-    public long getCacheTimeMs() {
-        return mCacheTimeMs;
-    }
-
-    public Intent getIntent() {
-        return mIntent;
-    }
-
-    public int getItemCount() {
-        return mClusterItems.size();
-    }
-
-    public ClusterItem getItem(int position) {
-        if (position >= 0 && position < mClusterItems.size()) {
-            return mClusterItems.get(position);
-        }
-        return null;
-    }
-
-    void addClusterItem(ClusterItem item) {
-        mClusterItems.add(item);
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder builder = new StringBuilder();
-        builder.append("id: ").append(mId)
-                .append(", name: ").append(mName)
-                .append(", displayName: ").append(mDisplayName)
-                .append(", importance: ").append(mImportance)
-                .append(", visibleCount: ").append(mVisibleCount)
-                .append(", imageCropAllowed: ").append(mImageCropAllowed)
-                .append(", cacheTimeMs: ").append(mCacheTimeMs)
-                .append(", intent: ").append(mIntent.toUri(0));
-        return builder.toString();
-    }
-
-    /**
-     * Builds cluster objects.
-     */
-    public static class Builder {
-        private long mId;
-        private String mName;
-        private CharSequence mDisplayName;
-        private int mImportance;
-        private int mVisibleCount;
-        private boolean mImageCropAllowed;
-        private long mCacheTimeMs;
-        private Intent mIntent;
-
-        private List<ClusterItem> mClusterItems;
-
-        public Cluster build() {
-            Cluster cluster = new Cluster();
-            cluster.mId = mId;
-            cluster.mName = mName;
-            cluster.mDisplayName = mDisplayName;
-            cluster.mImportance = mImportance;
-            cluster.mVisibleCount = mVisibleCount;
-            cluster.mImageCropAllowed = mImageCropAllowed;
-            cluster.mIntent = mIntent;
-            cluster.mCacheTimeMs = mCacheTimeMs;
-            cluster.mClusterItems.addAll(mClusterItems);
-            return cluster;
-        }
-
-        public Builder() {
-            mClusterItems = new ArrayList<ClusterItem>();
-            mImageCropAllowed = true;
-        }
-
-        public Builder id(long id) {
-            mId = id;
-            return this;
-        }
-
-        public Builder name(String name) {
-            mName = name;
-            return this;
-        }
-
-        public Builder displayName(CharSequence displayName) {
-            mDisplayName = displayName;
-            return this;
-        }
-
-        public Builder importance(int importance) {
-            mImportance = importance;
-            return this;
-        }
-
-        public Builder visibleCount(int visibleCount) {
-            mVisibleCount = visibleCount;
-            return this;
-        }
-
-        public Builder imageCropAllowed(boolean allowed) {
-            mImageCropAllowed = allowed;
-            return this;
-        }
-
-        public Builder cacheTimeMs(long cacheTimeMs) {
-            mCacheTimeMs = cacheTimeMs;
-            return this;
-        }
-
-        public Builder intent(Intent intent) {
-            mIntent = intent;
-            return this;
-        }
-
-        public Builder addItem(Uri imageUri) {
-            ClusterItem item = new ClusterItem(imageUri);
-            mClusterItems.add(item);
-            return this;
-        }
-    }
-}
diff --git a/src/com/google/android/pano/data/Cluster.java b/src/com/google/android/pano/data/Cluster.java
new file mode 100644
index 0000000..d4a2c1c
--- /dev/null
+++ b/src/com/google/android/pano/data/Cluster.java
@@ -0,0 +1,309 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.android.pano.data;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a home screen cluster.
+ */
+public class Cluster {
+
+    private long mId;
+    private String mName;
+    private CharSequence mDisplayName;
+    private int mImportance;
+    private int mVisibleCount;
+    private boolean mImageCropAllowed;
+    private long mCacheTimeMs;
+    private Intent mIntent;
+    private Uri mBrowseItemsUri;
+
+    private List<ClusterItem> mClusterItems;
+
+    /**
+     * An item displayed inside a cluster.
+     */
+    public static class ClusterItem {
+        private Uri mImageUri;
+
+        public ClusterItem(Uri imageUri) {
+            mImageUri = imageUri;
+        }
+
+        public Uri getImageUri() {
+            return mImageUri;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("imageUri: ").append(mImageUri);
+            return builder.toString();
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((mImageUri == null) ? 0 : mImageUri.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            ClusterItem other = (ClusterItem) obj;
+            if (mImageUri == null) {
+                if (other.mImageUri != null)
+                    return false;
+            } else if (!mImageUri.equals(other.mImageUri))
+                return false;
+            return true;
+        }
+    }
+
+    public Cluster() {
+        mClusterItems = new ArrayList<ClusterItem>();
+        mImageCropAllowed = true;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public CharSequence getDisplayName() {
+        return mDisplayName;
+    }
+
+    public int getImportance() {
+        return mImportance;
+    }
+
+    public int getVisibleCount() {
+        return mVisibleCount;
+    }
+
+    public boolean isImageCropAllowed() {
+        return mImageCropAllowed;
+    }
+
+    public long getCacheTimeMs() {
+        return mCacheTimeMs;
+    }
+
+    public Intent getIntent() {
+        return mIntent;
+    }
+
+    public Uri getBrowseItemsUri() {
+        return mBrowseItemsUri;
+    }
+
+    public int getItemCount() {
+        return mClusterItems.size();
+    }
+
+    public ClusterItem getItem(int position) {
+        if (position >= 0 && position < mClusterItems.size()) {
+            return mClusterItems.get(position);
+        }
+        return null;
+    }
+
+    public void addClusterItem(ClusterItem item) {
+        mClusterItems.add(item);
+    }
+
+    public void clearItems() {
+        mClusterItems.clear();
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (int) (mCacheTimeMs ^ (mCacheTimeMs >>> 32));
+        if (mClusterItems == null) {
+            result = prime * result;
+        } else {
+            for (ClusterItem ci : mClusterItems) {
+                result = prime * result + ci.hashCode();
+            }
+        }
+        result = prime * result + ((mDisplayName == null) ? 0 : mDisplayName.toString().hashCode());
+        result = prime * result + (int) (mId ^ (mId >>> 32));
+        result = prime * result + (mImageCropAllowed ? 1231 : 1237);
+        result = prime * result + mImportance;
+        result = prime * result + ((mIntent == null) ? 0 : mIntent.hashCode());
+        result = prime * result + ((mName == null) ? 0 : mName.hashCode());
+        result = prime * result + mVisibleCount;
+        result = prime * result + ((mBrowseItemsUri == null) ? 0 : mBrowseItemsUri.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Cluster other = (Cluster) obj;
+        if (mCacheTimeMs != other.mCacheTimeMs)
+            return false;
+        if (mClusterItems == null) {
+            if (other.mClusterItems != null)
+                return false;
+        } else if (!mClusterItems.equals(other.mClusterItems))
+            return false;
+        if (mDisplayName == null) {
+            if (other.mDisplayName != null)
+                return false;
+        } else if (!mDisplayName.equals(other.mDisplayName))
+            return false;
+        if (mId != other.mId)
+            return false;
+        if (mImageCropAllowed != other.mImageCropAllowed)
+            return false;
+        if (mImportance != other.mImportance)
+            return false;
+        if (mIntent == null) {
+            if (other.mIntent != null)
+                return false;
+        } else if (!mIntent.equals(other.mIntent))
+            return false;
+        if (mName == null) {
+            if (other.mName != null)
+                return false;
+        } else if (!mName.equals(other.mName))
+            return false;
+        if (mVisibleCount != other.mVisibleCount)
+            return false;
+        if (mBrowseItemsUri == null) {
+            if (other.mBrowseItemsUri != null)
+                return false;
+        } else if (!mBrowseItemsUri.equals(other.mBrowseItemsUri)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("id: ").append(mId)
+                .append(", name: ").append(mName)
+                .append(", displayName: ").append(mDisplayName)
+                .append(", importance: ").append(mImportance)
+                .append(", visibleCount: ").append(mVisibleCount)
+                .append(", imageCropAllowed: ").append(mImageCropAllowed)
+                .append(", cacheTimeMs: ").append(mCacheTimeMs)
+                .append(", clusterItems: ").append(mClusterItems)
+                .append(", intent: ").append(mIntent != null ? mIntent.toUri(0) : "")
+                .append(", browseItems: ").append(mBrowseItemsUri != null ? mBrowseItemsUri : "");
+        return builder.toString();
+    }
+
+    /**
+     * Builds cluster objects.
+     */
+    public static class Builder {
+        private long mId;
+        private String mName;
+        private CharSequence mDisplayName;
+        private int mImportance;
+        private int mVisibleCount;
+        private boolean mImageCropAllowed;
+        private long mCacheTimeMs;
+        private Intent mIntent;
+        private Uri mBrowseItemsUri;
+
+        private List<ClusterItem> mClusterItems;
+
+        public Cluster build() {
+            Cluster cluster = new Cluster();
+            cluster.mId = mId;
+            cluster.mName = mName;
+            cluster.mDisplayName = mDisplayName;
+            cluster.mImportance = mImportance;
+            cluster.mVisibleCount = mVisibleCount;
+            cluster.mImageCropAllowed = mImageCropAllowed;
+            cluster.mIntent = mIntent;
+            cluster.mCacheTimeMs = mCacheTimeMs;
+            cluster.mClusterItems.addAll(mClusterItems);
+            cluster.mBrowseItemsUri = mBrowseItemsUri;
+            return cluster;
+        }
+
+        public Builder() {
+            mClusterItems = new ArrayList<ClusterItem>();
+            mImageCropAllowed = true;
+        }
+
+        public Builder id(long id) {
+            mId = id;
+            return this;
+        }
+
+        public Builder name(String name) {
+            mName = name;
+            return this;
+        }
+
+        public Builder displayName(CharSequence displayName) {
+            mDisplayName = displayName;
+            return this;
+        }
+
+        public Builder importance(int importance) {
+            mImportance = importance;
+            return this;
+        }
+
+        public Builder visibleCount(int visibleCount) {
+            mVisibleCount = visibleCount;
+            return this;
+        }
+
+        public Builder imageCropAllowed(boolean allowed) {
+            mImageCropAllowed = allowed;
+            return this;
+        }
+
+        public Builder cacheTimeMs(long cacheTimeMs) {
+            mCacheTimeMs = cacheTimeMs;
+            return this;
+        }
+
+        public Builder intent(Intent intent) {
+            mIntent = intent;
+            return this;
+        }
+
+        public Builder browseItemsUri(Uri uri) {
+            mBrowseItemsUri = uri;
+            return this;
+        }
+
+        public Builder addItem(Uri imageUri) {
+            ClusterItem item = new ClusterItem(imageUri);
+            mClusterItems.add(item);
+            return this;
+        }
+    }
+}
diff --git a/src/com/google/android/canvas/data/util/UriUtils.java b/src/com/google/android/pano/data/util/UriUtils.java
similarity index 89%
rename from src/com/google/android/canvas/data/util/UriUtils.java
rename to src/com/google/android/pano/data/util/UriUtils.java
index 7b7b73c..5d973c1 100644
--- a/src/com/google/android/canvas/data/util/UriUtils.java
+++ b/src/com/google/android/pano/data/util/UriUtils.java
@@ -1,6 +1,6 @@
 // Copyright 2012 Google Inc. All Rights Reserved.
 
-package com.google.android.canvas.data.util;
+package com.google.android.pano.data.util;
 
 import android.content.ContentResolver;
 import android.content.Context;
@@ -83,6 +83,15 @@
     }
 
     /**
+     * Returns {@code true} if the URI refers to a content URI which can be opened via
+     * {@link ContentResolver#openInputStream(Uri)}.
+     */
+    public static boolean isContentUri(Uri uri) {
+        return ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) ||
+                ContentResolver.SCHEME_FILE.equals(uri.getScheme());
+    }
+
+    /**
      * Checks if the URI refers to an shortcut icon resource.
      */
     public static boolean isShortcutIconResourceUri(Uri uri) {
@@ -118,7 +127,8 @@
      * Returns {@code true} if this is a web URI.
      */
     public static boolean isWebUri(Uri resourceUri) {
-        String scheme = resourceUri.getScheme().toLowerCase();
+        String scheme = resourceUri.getScheme() == null ? null
+                : resourceUri.getScheme().toLowerCase();
         return HTTP_PREFIX.equals(scheme) || HTTPS_PREFIX.equals(scheme);
     }
 }
diff --git a/src/com/google/android/canvas/provider/CanvasContract.java b/src/com/google/android/pano/provider/PanoContract.java
similarity index 75%
rename from src/com/google/android/canvas/provider/CanvasContract.java
rename to src/com/google/android/pano/provider/PanoContract.java
index f6a1741..cfce17a 100644
--- a/src/com/google/android/canvas/provider/CanvasContract.java
+++ b/src/com/google/android/pano/provider/PanoContract.java
@@ -1,4 +1,4 @@
-package com.google.android.canvas.provider;
+package com.google.android.pano.provider;
 
 import android.content.ContentUris;
 import android.content.Intent;
@@ -6,13 +6,13 @@
 import android.provider.BaseColumns;
 
 /**
- * The contract between Canvas and ContentProviders that allow access to Canvas
- * browsing data. All apps that wish to interact with Canvas should use these
+ * The contract between Pano and ContentProviders that allow access to Pano
+ * browsing data. All apps that wish to interact with Pano should use these
  * definitions.
  *
  * TODO add more details
  */
-public final class CanvasContract {
+public final class PanoContract {
 
     // Base for content uris
     public static final String CONTENT = "content://";
@@ -30,37 +30,50 @@
     public static final String PATH_BROWSE_HEADERS = "headers";
 
     /**
-     * This tag is used to identify the authority to be used for a canvas
-     * launcher app.
-     *
-     * TODO: this is obsolete: remove.
+     * Pano will search for activities with the action {@link Intent#ACTION_MAIN} and this
+     * category in order to find the activities for the home screen.
      */
-    public static final String METADATA_TAG = "com.google.android.canvas.data.launcher";
+    public static final String CATEGORY_BROWSE_LAUNCHER =
+            "com.google.android.pano.category.BROWSE_LAUNCHER";
 
     /**
-     * This tag is used to identify the launcher info data file to be used for a canvas
+     * This tag is used to identify the launcher info data file to be used for a Pano
      * launcher app.
      */
     public static final String METADATA_LAUNCHER_INFO_TAG =
-            "com.google.android.canvas.data.launcher_info";
+            "com.google.android.pano.data.launcher_info";
 
     /**
-     * This tag is used to denote a background color hint for the activity.
-     * <p>
-     * This can either be a reference to an @color or else a string (e.g. #ff001100).
-     *
-     * TODO: this is obsolete: remove.
-     */
-    public static final String METADATA_COLOR_HINT =
-            "com.google.android.canvas.ui.launcher_color_hint";
-
-    /**
-     * An intent action for browsing app content in Canvas. Apps receiving this
+     * An intent action for browsing app content in Pano. Apps receiving this
      * intent should call {@link Intent#getData()} to retrieve the base Uri and
      * {@link #EXTRA_START_INDEX} or {@link #EXTRA_START_ID} to find which header
      * to start at (default 0).
      */
-    public static final String ACTION_BROWSE = "com.google.android.canvas.action.BROWSE";
+    public static final String ACTION_BROWSE = "com.google.android.pano.action.BROWSE";
+
+    /**
+     * An intent action for picking app content in Pano.
+     * <p>
+     * Any intents launched from here will be returned to the calling activity instead of being
+     * directly launched.
+     * <p>
+     * This can be used to select an item.
+     */
+    public static final String ACTION_BROWSE_PICKER =
+            "com.google.android.pano.action.BROWSE_PICKER";
+
+    /**
+     * An intent action for picking app content while keeping the launching app running.
+     * <p>
+     * Pano Browse will appear over the app.
+     * <p>
+     * Any intents launched from here will be returned to the calling activity instead of being
+     * directly launched.
+     * <p>
+     * This can be used to select an item.
+     */
+    public static final String ACTION_BROWSE_PICKER_TRANSLUCENT =
+            "com.google.android.pano.action.BROWSE_PICKER_TRANSLUCENT";
 
     /**
      * The index of the header to focus on initially when the browse is launched.
@@ -76,10 +89,25 @@
     public static final String EXTRA_START_ID = "start_id";
 
     /**
-     * An intent action for viewing detail content in Canvas. Apps receiving this
+     * An intent action for viewing detail content in Pano. Apps receiving this
      * intent should call {@link Intent#getData()} to retrieve the base Uri.
      */
-    public static final String ACTION_DETAIL = "com.google.android.canvas.action.DETAIL";
+    public static final String ACTION_DETAIL = "com.google.android.pano.action.DETAIL";
+
+    /**
+     * The name of the section to focus on initially when {@link #ACTION_DETAIL} is launched.
+     * <p>
+     * Using name allows targeting a sub section.
+     */
+    public static final String EXTRA_START_NAME = "start_name";
+
+    /**
+     * Index of a child to focus on initially when {@link #ACTION_DETAIL} is launched.
+     * <p>
+     * Requires a start section to be specified using {@link #EXTRA_START_INDEX} or
+     * {@link #EXTRA_START_NAME}.
+     */
+    public static final String EXTRA_START_CHILD_INDEX = "start_child_index";
 
     /**
      * Path for querying details for an item.
@@ -99,11 +127,33 @@
     public static final String PATH_DETAIL_ACTIONS = "actions";
 
     /**
-     * Action for searching a Canvas provider. Apps receiving this
+     * Action for searching a Pano provider. Apps receiving this
      * intent should call {@link Intent#getData()} to retrieve the base Uri and
      * {@link #EXTRA_QUERY} to find query.
      */
-    public static final String ACTION_SEARCH = "com.google.android.canvas.action.SEARCH";
+    public static final String ACTION_SEARCH = "com.google.android.pano.action.SEARCH";
+
+    /**
+     * Action for searching a Pano provider.
+     * <p>
+     * Any intent selected off this activity will be returned to the calling activity instead of
+     * being launched directly.
+     */
+    public static final String ACTION_SEARCH_PICKER =
+            "com.google.android.pano.action.SEARCH_PICKER";
+
+    /**
+     * An intent action for searching app content while keeping the launching app running.
+     * <p>
+     * Pano Search will appear over the app.
+     * <p>
+     * Any intents launched from here will be returned to the calling activity instead of being
+     * directly launched.
+     * <p>
+     * This can be used to select an item.
+     */
+    public static final String ACTION_SEARCH_PICKER_TRANSLUCENT =
+            "com.google.android.pano.action.SEARCH_PICKER_TRANSLUCENT";
 
     /**
      * The query to be executed when search activity is launched
@@ -112,6 +162,13 @@
     public static final String EXTRA_QUERY = "query";
 
     /**
+     * Optional String extra for meta information. This must be supplied as a string, but must be a valid URI.
+     * <p>
+     * Used with {@link #ACTION_SEARCH}.
+     */
+    public static final String EXTRA_META_URI = "meta_uri";
+
+    /**
      * Optional int extra for setting the display mode of the search activity.
      *
      * @see #DISPLAY_MODE_ROW
@@ -121,9 +178,10 @@
 
     public static final int DISPLAY_MODE_ROW = 0;
     public static final int DISPLAY_MODE_GRID = 1;
+    public static final int DISPLAY_MODE_BROWSE = 2;
 
     /**
-     * Value for the root Canvas URI when this activity should be excluded from the Canvas top level
+     * Value for the root Pano URI when this activity should be excluded from the Pano top level
      * and the legacy apps area.
      *
      * TODO: this is obsolete: remove.
@@ -187,10 +245,27 @@
         public static final String CACHE_TIME_MS = "cache_time_ms";
 
         /**
+         * Content URI pointing to a list of items in the {@link PanoContract.BrowseItemsColumns}
+         * schema.
+         * <p>
+         * This is optional but highly recommended if the cluster represents a browse row.
+         * <p>
+         * In this case, the cluster items will be read from this URI instead of from the cluster
+         * items.
+         * <p>
+         * If this is filled in, the intent_uri must have the action
+         * {@link PanoContract#ACTION_BROWSE}. The row with this URI will automatically be
+         * selected when the activity starts up.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String BROWSE_ITEMS_URI = "browse_items_uri";
+
+        /**
          * A standard Intent Uri to be launched when this cluster is selected.
-         * This may be a {@link CanvasContract#ACTION_BROWSE} intent or an
+         * This may be a {@link PanoContract#ACTION_BROWSE} intent or an
          * intent to launch directly into an app. You can also use
-         * {@link CanvasContract#getBrowseIntent(Uri, int)} to generate a
+         * {@link PanoContract#getBrowseIntent(Uri, int)} to generate a
          * browse intent for a given root Uri. Use {@link Intent#toUri(int)}
          * with a flag of {@link Intent#URI_INTENT_SCHEME}.
          *
@@ -208,7 +283,7 @@
         public static final String NOTIFICATION_TEXT = "notification_text";
 
         /**
-         * An optional Uri for querying progresss for any ongoing actions, such
+         * An optional Uri for querying progress for any ongoing actions, such
          * as an active download.
          *
          * <P>Type: String (Uri)</P>
@@ -232,6 +307,16 @@
          * <P>Type: INTEGER</P>
          */
         public static final String PROGRESS = "progress";
+
+        /**
+         * The smallest value that is a valid {@link #PROGRESS}.
+         */
+        public static final int PROGRESS_MIN = 0;
+
+        /**
+         * The largest value that is a valid {@link #PROGRESS}.
+         */
+        public static final int PROGRESS_MAX = 100;
     }
 
     public static final class Progress implements BaseColumns, ProgressColumns {
@@ -305,6 +390,16 @@
         public static final String DISPLAY_NAME = "display_name";
 
         /**
+         * Optional Uri pointing to the data for the items for this header.
+         * <p>
+         * If this is not provided, a Uri will be constructed using
+         * {@link BrowseItems#getBrowseItemsUri(Uri, long)}.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String ITEMS_URI = "items_uri";
+
+        /**
          * Uri pointing to an icon to be used as part of the header. This
          * String should be generated using {@link Uri#toString()}
          *
@@ -346,6 +441,20 @@
          *
          * <P>Type: String (Uri)</P>
          */
+        public static final String BACKGROUND_IMAGE_URI = "background_image_uri";
+
+        /**
+         * Uri pointing to an image to display in the background when on this
+         * tab. Be sure the image contrasts enough with the text color hint and
+         * is of high enough quality to be displayed at 1080p. This String
+         * should be generated using {@link Uri#toString()}.  The URI will be either
+         * a resource uri in format of android:resource:// or an external URL
+         * like file://, http://, https://.
+         *
+         * <P>Type: String (Uri)</P>
+         * <P>This is the obsolete version of {@link #BACKGROUND_IMAGE_URI}</P>
+         * TODO: remove obsolete version when all clients have upgraded.
+         */
         public static final String BG_IMAGE_URI = "bg_image_uri";
 
         /**
@@ -481,22 +590,30 @@
 
     protected interface UserRatingColumns {
         /**
-         * The average rating for this item. (Optional)
-         *
-         * <P>Type: Double</P>
+         * A custom rating String for this item, such as "78 points" or
+         * "20/100". (Optional)
+         * <p>
+         * A null or the absence of this column indicates there is no custom
+         * rating available.
+         * <P>Type: String</P>
          */
-        public static final String USER_RATING_AVERAGE = "user_rating_average";
+        public static final String USER_RATING_CUSTOM = "user_rating_custom";
 
         /**
-         * A simple rating for this item as an integer in the range
-         * [0-10] inclusive. (Optional)
-         *
-         * <P>Type: INTEGER</P>
+         * A scaled rating for this item as a float in the range [0-10]
+         * inclusive. Pano will be responsible for visualizing this
+         * value.(Optional)
+         * <p>
+         * A -1 or the absence of this column indicates there is no rating
+         * available.
+         * <P>Type: FLOAT</P>
          */
-        public static final String USER_RATING_SIMPLE = "user_rating_simple";
+        public static final String USER_RATING = "user_rating";
 
         /**
          * The number of reviews included in the average rating. (Optional)
+         * <p>
+         * A value of 0 indicates the count is not available.
          *
          * <P>Type: INTEGER</P>
          */
@@ -568,6 +685,13 @@
     protected interface DetailSectionsColumns {
 
         /**
+         * Text ID for a section. Can be used to target the section
+         *
+         * <P>Type: String</P>
+         */
+        public static final String NAME = "name";
+
+        /**
          * Text that will be shown to the user for navigating between sections.
          *
          * <P>Type: String</P>
@@ -776,6 +900,23 @@
         public static final String DISPLAY_NUMBER = "display_number";
 
         /**
+         * Hint for how to display the item. This is either {@link #ITEM_DISPLAY_TYPE_NORMAL} or
+         * {@link #ITEM_DISPLAY_TYPE_SINGLE_LINE}.
+         */
+        public static final String ITEM_DISPLAY_TYPE = "item_display_type";
+
+        /**
+         * Value for {@link #ITEM_DISPLAY_TYPE} which allows for multiple lines.
+         */
+        public static final int ITEM_DISPLAY_TYPE_NORMAL = 0;
+
+        /**
+         * Value for {@link #ITEM_DISPLAY_TYPE} which hints that the content should be displayed
+         * on a single line.
+         */
+        public static final int ITEM_DISPLAY_TYPE_SINGLE_LINE = 1;
+
+        /**
          * The uri for retrieving the image to show for this item. This string
          * should be created using {@link Uri#toString()}. (Optional)
          *
@@ -795,6 +936,23 @@
         public static final String ACTION_URI = "action_uri";
     }
 
+    protected interface SearchBrowseResult {
+        public static final String RESULTS_URI = "results_uri";
+        /**
+         * The default width of the expanded image
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String DEFAULT_ITEM_WIDTH = "default_item_width";
+
+        /**
+         * The default height of the expanded image
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String DEFAULT_ITEM_HEIGHT = "default_item_height";
+    }
+
     public static final class DetailChildren
             implements BaseColumns, ItemChildrenColumns, UserRatingColumns {
 
@@ -805,7 +963,7 @@
     }
 
     public static final class SearchResults
-            implements BaseColumns, ItemChildrenColumns, UserRatingColumns {
+            implements BaseColumns, ItemChildrenColumns, UserRatingColumns, SearchBrowseResult {
 
         /**
          * Non instantiable.
@@ -813,6 +971,40 @@
         private SearchResults() {}
     }
 
+    protected interface MetaColumns {
+
+        /**
+         * The uri for retrieving the background image to show for this item. This string
+         * should be created using {@link Uri#toString()}.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String BACKGROUND_IMAGE_URI = "background_image_uri";
+        /**
+         * Uri pointing to an icon to be used for app branding on this tab.
+         * This String should be generated using {@link Uri#toString()}
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String BADGE_URI = "badge_uri";
+
+        /**
+         * A 0xAARRGGBB color that should be applied to the background when on
+         * this tab.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String COLOR_HINT = "color_hint";
+    }
+
+    public static final class MetaSchema implements BaseColumns, MetaColumns {
+
+        /**
+         * Non instantiable.
+         */
+        private MetaSchema() {}
+    }
+
     protected interface DetailActionsColumns {
 
         /**
@@ -892,4 +1084,4 @@
         intent.setData(root);
         return intent;
     }
-}
\ No newline at end of file
+}
diff --git a/src_pd/com/android/gallery3d/filtershow/editors/EditorManager.java b/src_pd/com/android/gallery3d/filtershow/editors/EditorManager.java
index 92962cb..2a39b68 100644
--- a/src_pd/com/android/gallery3d/filtershow/editors/EditorManager.java
+++ b/src_pd/com/android/gallery3d/filtershow/editors/EditorManager.java
@@ -29,6 +29,10 @@
         editorPlaceHolder.addEditor(new EditorTinyPlanet());
         editorPlaceHolder.addEditor(new EditorDraw());
         editorPlaceHolder.addEditor(new EditorVignette());
+        editorPlaceHolder.addEditor(new EditorFlip());
+        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
index 988cf2d..d6b8718 100644
--- a/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java
+++ b/src_pd/com/android/gallery3d/filtershow/filters/FiltersManager.java
@@ -16,13 +16,26 @@
 
 package com.android.gallery3d.filtershow.filters;
 
+import java.util.HashMap;
+import java.util.Vector;
+
 public class FiltersManager extends BaseFiltersManager {
-    private static FiltersManager gInstance = null;
+    private static FiltersManager sInstance = null;
+
+    protected FiltersManager() {
+        mFilters = new HashMap<Class, ImageFilter>();
+        addFilters(mFilters);
+    }
 
     public static FiltersManager getManager() {
-        if (gInstance == null) {
-            gInstance = new FiltersManager();
+        if (sInstance == null) {
+            sInstance = new FiltersManager();
         }
-        return gInstance;
+        return sInstance;
     }
+
+    public static void reset() {
+        sInstance = null;
+    }
+
 }
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/AndroidManifest.xml b/tests/AndroidManifest.xml
index ef63cc4..b98b5e0 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -36,4 +36,8 @@
     <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/src/com/android/photos/data/DataTestRunner.java b/tests/src/com/android/photos/data/DataTestRunner.java
new file mode 100644
index 0000000..4322585
--- /dev/null
+++ b/tests/src/com/android/photos/data/DataTestRunner.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 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);
+        return suite;
+    }
+
+    @Override
+    public ClassLoader getLoader() {
+        return DataTestRunner.class.getClassLoader();
+    }
+}
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..70edee2
--- /dev/null
+++ b/tests/src/com/android/photos/data/PhotoDatabaseTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.sqlite.SQLiteDatabase;
+import android.test.InstrumentationTestCase;
+
+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 = getWriteableDB();
+        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 = getWriteableDB();
+        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 = getWriteableDB();
+        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 = getWriteableDB();
+        db.beginTransaction();
+        try {
+            assertFalse(PhotoDatabaseUtils.insertAccount(db, null));
+            assertTrue(PhotoDatabaseUtils.insertAccount(db, "hello"));
+            assertTrue(PhotoDatabaseUtils.insertAccount(db, "hello"));
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    private SQLiteDatabase getReadableDB() {
+        return mDBHelper.getReadableDatabase();
+    }
+
+    private SQLiteDatabase getWriteableDB() {
+        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..97db8bf
--- /dev/null
+++ b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 + " = ?";
+
+    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 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..39abff4
--- /dev/null
+++ b/tests/src/com/android/photos/data/PhotoProviderTest.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.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 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 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.insertAlbum(db, ALBUM_PARENT_ID, ALBUM_TITLE,
+                    Albums.VISIBILITY_PRIVATE, 100L);
+            mAlbumId = PhotoDatabaseUtils.queryAlbumIdFromParentId(db, ALBUM_PARENT_ID);
+            PhotoDatabaseUtils.insertPhoto(db, 100, 100, System.currentTimeMillis(), mAlbumId,
+                    MIME_TYPE, 100L);
+            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 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);
+                }
+            }
+        }
+    }
+
+}