am b62f354d: am 582c3bb9: Merge "Gallery2: Use ICS alert drawable"

* commit 'b62f354d4f890a20f6bec37eac83226f75a4a475':
  Gallery2: Use ICS alert drawable
diff --git a/Android.mk b/Android.mk
index 0f5170f..ea71b39 100644
--- a/Android.mk
+++ b/Android.mk
@@ -1,4 +1,5 @@
 LOCAL_PATH:= $(call my-dir)
+
 include $(CLEAR_VARS)
 
 LOCAL_MODULE_TAGS := optional
@@ -8,17 +9,32 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_SRC_FILES += $(call all-java-files-under, src_pd)
+LOCAL_SRC_FILES += $(call all-java-files-under, ../Camera/src)
+
+LOCAL_RESOURCE_DIR += $(LOCAL_PATH)/res packages/apps/Camera/res
+LOCAL_AAPT_FLAGS := --auto-add-overlay --extra-packages com.android.camera
 
 LOCAL_PACKAGE_NAME := Gallery2
 
 LOCAL_OVERRIDES_PACKAGES := Gallery Gallery3D GalleryNew3D
 
-# We mark this out until Mtp and MediaMetadataRetriever is unhidden.
-LOCAL_SDK_VERSION := current
+#LOCAL_SDK_VERSION := current
+
+LOCAL_JNI_SHARED_LIBRARIES := libjni_mosaic libjni_eglfence
+
+LOCAL_REQUIRED_MODULES := libjni_mosaic libjni_eglfence
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
 include $(BUILD_PACKAGE)
 
-# Use the following include to make our test apk.
-include $(call all-makefiles-under,$(LOCAL_PATH))
+include $(call all-makefiles-under, jni)
+
+ifeq ($(strip $(LOCAL_PACKAGE_OVERRIDES)),)
+# Use the following include to make gallery test apk.
+include $(call all-makefiles-under, $(LOCAL_PATH))
+
+# Use the following include to make camera test apk.
+include $(call all-makefiles-under, ../Camera)
+
+endif
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 187bac9..2a69ce1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,26 +1,33 @@
 <?xml version="1.0" encoding="utf-8"?>
 
-<manifest android:versionCode="30682"
-        android:versionName="1.1.30682"
+<manifest android:versionCode="40000"
+        android:versionName="1.1.40000"
         xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.gallery3d">
 
     <original-package android:name="com.android.gallery3d" />
 
+    <uses-sdk android:minSdkVersion="14" />
+
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.NFC" />
+    <uses-permission android:name="android.permission.READ_SMS" />
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.SET_WALLPAPER" />
     <uses-permission android:name="android.permission.USE_CREDENTIALS" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
 
     <supports-screens android:smallScreens="false"
             android:normalScreens="true" android:largeScreens="true"
@@ -28,7 +35,10 @@
 
     <application android:icon="@mipmap/ic_launcher_gallery" android:label="@string/app_name"
             android:name="com.android.gallery3d.app.GalleryAppImpl"
-            android:theme="@style/Theme.Gallery">
+            android:theme="@style/Theme.Gallery"
+            android:logo="@mipmap/ic_launcher_gallery"
+            android:hardwareAccelerated="true">
+        <uses-library android:name="com.google.android.media.effects" android:required="false" />
         <activity android:name="com.android.gallery3d.app.MovieActivity"
                 android:label="@string/movie_view_label"
                 android:configChanges="orientation|keyboardHidden|screenSize">
@@ -61,6 +71,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.BROWSABLE" />
                 <data android:scheme="http" />
+                <data android:scheme="https" />
                 <data android:mimeType="audio/x-mpegurl" />
                 <data android:mimeType="audio/mpegurl" />
                 <data android:mimeType="application/vnd.apple.mpegurl" />
@@ -197,10 +208,11 @@
             </intent-filter>
         </activity>
         <activity android:name="com.android.gallery3d.photoeditor.PhotoEditor"
-                android:label="@string/photoeditor_name"
+                android:label="@string/app_name"
                 android:theme="@android:style/Theme.Holo.NoActionBar.Fullscreen"
                 android:configChanges="keyboardHidden|orientation|screenSize"
-                android:hardwareAccelerated="true">
+                android:hardwareAccelerated="true"
+                android:process=":edit">
             <intent-filter>
                 <action android:name="android.intent.action.EDIT" />
                 <data android:mimeType="image/*" />
@@ -238,8 +250,62 @@
                 android:theme="@style/DialogPickerTheme"/>
         <activity android:name="com.android.gallery3d.gadget.WidgetTypeChooser"
                 android:configChanges="keyboardHidden|orientation|screenSize"
-                android:theme="@style/DialogPickerTheme"/>
+                android:theme="@android:style/Theme.Holo.Dialog"/>
 
+        <activity android:name="com.android.camera.Camera"
+                android:taskAffinity="com.android.camera"
+                android:label="@string/camera_label"
+                android:theme="@style/ThemeCamera"
+                android:icon="@mipmap/ic_launcher_camera"
+                android:configChanges="orientation|screenSize|keyboardHidden"
+                android:clearTaskOnLaunch="true"
+                android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+            <intent-filter>
+                <action android:name="android.media.action.IMAGE_CAPTURE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.media.action.STILL_IMAGE_CAMERA" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity-alias android:icon="@mipmap/ic_launcher_camera"
+                        android:label="@string/camera_label"
+                        android:name="com.android.camera.CameraLauncher"
+                        android:targetActivity="com.android.camera.Camera" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
+
+        <activity android:name="com.android.camera.VideoCamera"
+                android:taskAffinity="com.android.camera"
+                android:label="@string/video_camera_label"
+                android:theme="@style/ThemeCamera"
+                android:configChanges="orientation|screenSize|keyboardHidden"
+                android:icon="@mipmap/ic_launcher_video_camera"
+                android:clearTaskOnLaunch="true"
+                android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+            <intent-filter>
+                <action android:name="android.media.action.VIDEO_CAMERA" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.media.action.VIDEO_CAPTURE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity android:name="com.android.camera.PanoramaActivity"
+                android:taskAffinity="com.android.camera"
+                android:label="@string/pano_dialog_title"
+                android:theme="@style/ThemeCamera"
+                android:configChanges="orientation|screenSize|keyboardHidden"
+                android:clearTaskOnLaunch="true"
+                android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+        </activity>
         <receiver android:name="com.android.gallery3d.gadget.PhotoAppWidgetProvider"
                 android:label="@string/appwidget_title">
             <intent-filter>
@@ -256,6 +322,16 @@
                 <data android:scheme="package"/>
             </intent-filter>
         </receiver>
+        <receiver android:name="com.android.camera.CameraButtonIntentReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.CAMERA_BUTTON"/>
+            </intent-filter>
+        </receiver>
+        <receiver android:name="com.android.camera.DisableCameraReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
         <service android:name="com.android.gallery3d.gadget.WidgetService"
                 android:permission="android.permission.BIND_REMOTEVIEWS"/>
         <activity android:name="com.android.gallery3d.gadget.WidgetConfigure"
diff --git a/CleanSpec.mk b/CleanSpec.mk
new file mode 100644
index 0000000..cc930a1
--- /dev/null
+++ b/CleanSpec.mk
@@ -0,0 +1,53 @@
+# Copyright (C) 2007 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list.  These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+#     $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+#     $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list.  E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
+
+# For example:
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/AndroidTests_intermediates)
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/core_intermediates)
+#$(call add-clean-step, find $(OUT_DIR) -type f -name "IGTalkSession*" -print0 | xargs -0 rm -f)
+#$(call add-clean-step, rm -rf $(PRODUCT_OUT)/data/*)
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/APPS/Camera*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Camera*)
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/APPS/Gallery*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Gallery*)
+
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
index 2192cf8..a671ed2 100644
--- a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
+++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
@@ -23,6 +23,7 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.os.Build;
+import android.util.FloatMath;
 import android.util.Log;
 
 import java.io.ByteArrayOutputStream;
@@ -31,7 +32,7 @@
 
 public class BitmapUtils {
     private static final String TAG = "BitmapUtils";
-    private static final int COMPRESS_JPEG_QUALITY = 90;
+    private static final int DEFAULT_JPEG_QUALITY = 90;
     public static final int UNCONSTRAINED = -1;
 
     private BitmapUtils(){}
@@ -71,7 +72,7 @@
                 && minSideLength == UNCONSTRAINED) return 1;
 
         int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
-                (int) Math.ceil(Math.sqrt((double) (w * h) / maxNumOfPixels));
+                (int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels));
 
         if (minSideLength == UNCONSTRAINED) {
             return lowerBound;
@@ -93,9 +94,9 @@
                 : initialSize / 8 * 8;
     }
 
-    // Fin the min x that 1 / x <= scale
+    // Find the min x that 1 / x >= scale
     public static int computeSampleSizeLarger(float scale) {
-        int initialSize = (int) Math.floor(1f / scale);
+        int initialSize = (int) FloatMath.floor(1f / scale);
         if (initialSize <= 1) return 1;
 
         return initialSize <= 8
@@ -103,25 +104,15 @@
                 : initialSize / 8 * 8;
     }
 
-    // Find the max x that 1 / x >= scale.
+    // Find the max x that 1 / x <= scale.
     public static int computeSampleSize(float scale) {
         Utils.assertTrue(scale > 0);
-        int initialSize = Math.max(1, (int) Math.ceil(1 / scale));
+        int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale));
         return initialSize <= 8
                 ? Utils.nextPowerOf2(initialSize)
                 : (initialSize + 7) / 8 * 8;
     }
 
-    public static Bitmap resizeDownToPixels(
-            Bitmap bitmap, int targetPixels, boolean recycle) {
-        int width = bitmap.getWidth();
-        int height = bitmap.getHeight();
-        float scale = (float) Math.sqrt(
-                (double) targetPixels / (width * height));
-        if (scale >= 1.0f) return bitmap;
-        return resizeBitmapByScale(bitmap, scale, recycle);
-    }
-
     public static Bitmap resizeBitmapByScale(
             Bitmap bitmap, float scale, boolean recycle) {
         int width = Math.round(bitmap.getWidth() * scale);
@@ -155,43 +146,15 @@
         return resizeBitmapByScale(bitmap, scale, recycle);
     }
 
-    // Resize the bitmap if each side is >= targetSize * 2
-    public static Bitmap resizeDownIfTooBig(
-            Bitmap bitmap, int targetSize, boolean recycle) {
-        int srcWidth = bitmap.getWidth();
-        int srcHeight = bitmap.getHeight();
-        float scale = Math.max(
-                (float) targetSize / srcWidth, (float) targetSize / srcHeight);
-        if (scale > 0.5f) return bitmap;
-        return resizeBitmapByScale(bitmap, scale, recycle);
-    }
-
-    // Crops a square from the center of the original image.
-    public static Bitmap cropCenter(Bitmap bitmap, boolean recycle) {
-        int width = bitmap.getWidth();
-        int height = bitmap.getHeight();
-        if (width == height) return bitmap;
-        int size = Math.min(width, height);
-
-        Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
-        Canvas canvas = new Canvas(target);
-        canvas.translate((size - width) / 2, (size - height) / 2);
-        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
-        canvas.drawBitmap(bitmap, 0, 0, paint);
-        if (recycle) bitmap.recycle();
-        return target;
-    }
-
-    public static Bitmap resizeDownAndCropCenter(Bitmap bitmap, int size,
-            boolean recycle) {
+    public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) {
         int w = bitmap.getWidth();
         int h = bitmap.getHeight();
-        int minSide = Math.min(w, h);
-        if (w == h && minSide <= size) return bitmap;
-        size = Math.min(size, minSide);
+        if (w == size && h == size) return bitmap;
 
-        float scale = Math.max((float) size / bitmap.getWidth(),
-                (float) size / bitmap.getHeight());
+        // scale the image so that the shorter side equals to the target;
+        // the longer side will be center-cropped.
+        float scale = (float) size / Math.min(w,  h);
+
         Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
         int width = Math.round(scale * bitmap.getWidth());
         int height = Math.round(scale * bitmap.getHeight());
@@ -272,11 +235,14 @@
         return null;
     }
 
-    public static byte[] compressBitmap(Bitmap bitmap) {
-        ByteArrayOutputStream os = new ByteArrayOutputStream();
-        bitmap.compress(Bitmap.CompressFormat.JPEG,
-                COMPRESS_JPEG_QUALITY, os);
-        return os.toByteArray();
+    public static byte[] compressToBytes(Bitmap bitmap) {
+        return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
+    }
+
+    public static byte[] compressToBytes(Bitmap bitmap, int quality) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
+        bitmap.compress(CompressFormat.JPEG, quality, baos);
+        return baos.toByteArray();
     }
 
     public static boolean isSupportedByRegionDecoder(String mimeType) {
@@ -291,10 +257,4 @@
         mimeType = mimeType.toLowerCase();
         return mimeType.equals("image/jpeg");
     }
-
-    public static byte[] compressToBytes(Bitmap bitmap, int quality) {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
-        bitmap.compress(CompressFormat.JPEG, quality, baos);
-        return baos.toByteArray();
-    }
 }
diff --git a/gallerycommon/src/com/android/gallery3d/common/Entry.java b/gallerycommon/src/com/android/gallery3d/common/Entry.java
index b8cc512..3f1644e 100644
--- a/gallerycommon/src/com/android/gallery3d/common/Entry.java
+++ b/gallerycommon/src/com/android/gallery3d/common/Entry.java
@@ -48,6 +48,8 @@
         boolean fullText() default false;
 
         String defaultValue() default "";
+
+        boolean unique() default false;
     }
 
     public void clear() {
diff --git a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
index 46de03f..7bf7e43 100644
--- a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
+++ b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
@@ -300,6 +300,7 @@
         StringBuilder sql = new StringBuilder("CREATE TABLE ");
         sql.append(tableName);
         sql.append(" (_id INTEGER PRIMARY KEY AUTOINCREMENT");
+        StringBuilder unique = new StringBuilder();
         for (ColumnInfo column : mColumnInfo) {
             if (!column.isId()) {
                 sql.append(',');
@@ -310,8 +311,18 @@
                     sql.append(" DEFAULT ");
                     sql.append(column.defaultValue);
                 }
+                if (column.unique) {
+                    if (unique.length() == 0) {
+                        unique.append(column.name);
+                    } else {
+                        unique.append(',').append(column.name);
+                    }
+                }
             }
         }
+        if (unique.length() > 0) {
+            sql.append(",UNIQUE(").append(unique).append(')');
+        }
         sql.append(");");
         logExecSql(db, sql.toString());
         sql.setLength(0);
@@ -493,7 +504,7 @@
 
             // Add the column to the array.
             int index = columns.size();
-            columns.add(new ColumnInfo(info.value(), type, info.indexed(),
+            columns.add(new ColumnInfo(info.value(), type, info.indexed(), info.unique(),
                     info.fullText(), info.defaultValue(), field, index));
         }
     }
@@ -504,16 +515,18 @@
         public final String name;
         public final int type;
         public final boolean indexed;
+        public final boolean unique;
         public final boolean fullText;
         public final String defaultValue;
         public final Field field;
         public final int projectionIndex;
 
-        public ColumnInfo(String name, int type, boolean indexed,
+        public ColumnInfo(String name, int type, boolean indexed, boolean unique,
                 boolean fullText, String defaultValue, Field field, int projectionIndex) {
             this.name = name.toLowerCase();
             this.type = type;
             this.indexed = indexed;
+            this.unique = unique;
             this.fullText = fullText;
             this.defaultValue = defaultValue;
             this.field = field;
diff --git a/gallerycommon/src/com/android/gallery3d/common/FileCache.java b/gallerycommon/src/com/android/gallery3d/common/FileCache.java
index 938ef8c..a69487f 100644
--- a/gallerycommon/src/com/android/gallery3d/common/FileCache.java
+++ b/gallerycommon/src/com/android/gallery3d/common/FileCache.java
@@ -191,7 +191,6 @@
 
     private synchronized void initialize() {
         if (mInitialized) return;
-        mInitialized = true;
 
         if (!mRootDir.isDirectory()) {
             mRootDir.mkdirs();
@@ -209,6 +208,10 @@
             cursor.close();
         }
         if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT);
+
+        // Mark initialized when everything above went through. If an exception was thrown,
+        // initialize() will be retried later.
+        mInitialized = true;
     }
 
     private void freeSomeSpaceIfNeed(int maxDeleteFileCount) {
diff --git a/gallerycommon/src/com/android/gallery3d/common/Utils.java b/gallerycommon/src/com/android/gallery3d/common/Utils.java
index ea289a6..391b225 100644
--- a/gallerycommon/src/com/android/gallery3d/common/Utils.java
+++ b/gallerycommon/src/com/android/gallery3d/common/Utils.java
@@ -16,22 +16,17 @@
 
 package com.android.gallery3d.common;
 
-import android.app.PendingIntent;
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.database.Cursor;
 import android.os.Build;
-import android.os.Environment;
-import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
-import android.os.StatFs;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.Closeable;
 import java.io.InterruptedIOException;
-import java.util.Random;
 
 public class Utils {
     private static final String TAG = "Utils";
@@ -54,12 +49,14 @@
         }
     }
 
-    // Throws AssertionError if the input is false.
-    public static void assertTrue(boolean cond, String message, Object ... args) {
-        if (!cond) {
-            throw new AssertionError(
-                    args.length == 0 ? message : String.format(message, args));
-        }
+    // Throws AssertionError with the message. We had a method having the form
+    //   assertTrue(boolean cond, String message, Object ... args);
+    // However a call to that method will cause memory allocation even if the
+    // condition is false (due to autoboxing generated by "Object ... args"),
+    // so we don't use that anymore.
+    public static void fail(String message, Object ... args) {
+        throw new AssertionError(
+                args.length == 0 ? message : String.format(message, args));
     }
 
     // Throws NullPointerException if the input is null.
@@ -74,13 +71,6 @@
         return (a == b) || (a == null ? false : a.equals(b));
     }
 
-    // Returns true if the input is power of 2.
-    // Throws IllegalArgumentException if the input is <= 0.
-    public static boolean isPowerOf2(int n) {
-        if (n <= 0) throw new IllegalArgumentException();
-        return (n & -n) == n;
-    }
-
     // Returns the next power of two.
     // Returns the input if it is already power of 2.
     // Throws IllegalArgumentException if the input is <= 0 or
@@ -104,13 +94,6 @@
         return Integer.highestOneBit(n);
     }
 
-    // Returns the euclidean distance between (x, y) and (sx, sy).
-    public static float distance(float x, float y, float sx, float sy) {
-        float dx = x - sx;
-        float dy = y - sy;
-        return (float) Math.hypot(dx, dy);
-    }
-
     // Returns the input value x clamped to the range [min, max].
     public static int clamp(int x, int min, int max) {
         if (x > max) return max;
@@ -136,12 +119,6 @@
         return color >>> 24 == 0xFF;
     }
 
-    public static <T> void swap(T[] array, int i, int j) {
-        T temp = array[i];
-        array[i] = array[j];
-        array[j] = temp;
-    }
-
     public static void swap(int[] array, int i, int j) {
         int temp = array[i];
         array[i] = array[j];
@@ -259,15 +236,6 @@
         return value == null ? "" : value;
     }
 
-    // Used for debugging. Should be removed before submitting.
-    public static void debug(String format, Object ... args) {
-        if (args.length == 0) {
-            Log.d(DEBUG_TAG, format);
-        } else {
-            Log.d(DEBUG_TAG, String.format(format, args));
-        }
-    }
-
     public static float parseFloatSafely(String content, float defaultValue) {
         if (content == null) return defaultValue;
         try {
@@ -290,22 +258,6 @@
         return TextUtils.isEmpty(exifMake);
     }
 
-    public static boolean hasSpaceForSize(long size) {
-        String state = Environment.getExternalStorageState();
-        if (!Environment.MEDIA_MOUNTED.equals(state)) {
-            return false;
-        }
-
-        String path = Environment.getExternalStorageDirectory().getPath();
-        try {
-            StatFs stat = new StatFs(path);
-            return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
-        } catch (Exception e) {
-            Log.i(TAG, "Fail to access external storage", e);
-        }
-        return false;
-    }
-
     public static void waitWithoutInterrupt(Object object) {
         try {
             object.wait();
@@ -314,16 +266,6 @@
         }
     }
 
-    public static void shuffle(int array[], Random random) {
-        for (int i = array.length; i > 0; --i) {
-            int t = random.nextInt(i);
-            if (t == i - 1) continue;
-            int tmp = array[i - 1];
-            array[i - 1] = array[t];
-            array[t] = tmp;
-        }
-    }
-
     public static boolean handleInterrruptedException(Throwable e) {
         // A helper to deal with the interrupt exception
         // If an interrupt detected, we will setup the bit again.
@@ -380,34 +322,6 @@
         return result;
     }
 
-    public static PendingIntent deserializePendingIntent(byte[] rawPendingIntent) {
-        Parcel parcel = null;
-        try {
-            if (rawPendingIntent != null) {
-                parcel = Parcel.obtain();
-                parcel.unmarshall(rawPendingIntent, 0, rawPendingIntent.length);
-                return PendingIntent.readPendingIntentOrNullFromParcel(parcel);
-            } else {
-                return null;
-            }
-        } catch (Exception e) {
-            throw new IllegalArgumentException("error parsing PendingIntent");
-        } finally {
-            if (parcel != null) parcel.recycle();
-        }
-    }
-
-    public static byte[] serializePendingIntent(PendingIntent pendingIntent) {
-        Parcel parcel = null;
-        try {
-            parcel = Parcel.obtain();
-            PendingIntent.writePendingIntentOrNullToParcel(pendingIntent, parcel);
-            return parcel.marshall();
-        } finally {
-            if (parcel != null) parcel.recycle();
-        }
-    }
-
     // Mask information for debugging only. It returns <code>info.toString()</code> directly
     // for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****")
     // in release build to protect the information (e.g. for privacy issue).
@@ -417,4 +331,9 @@
         int length = Math.min(s.length(), MASK_STRING.length());
         return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length);
     }
+
+    // This method should be ONLY used for debugging.
+    public static void debug(String message, Object ... args) {
+        Log.v(DEBUG_TAG, String.format(message, args));
+    }
 }
diff --git a/jni/Android.mk b/jni/Android.mk
new file mode 100644
index 0000000..eedfa0b
--- /dev/null
+++ b/jni/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_CFLAGS += -DEGL_EGLEXT_PROTOTYPES
+
+LOCAL_SRC_FILES := jni_egl_fence.cpp
+
+LOCAL_SHARED_LIBRARIES := libcutils libEGL
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := libjni_eglfence
+
+include $(BUILD_SHARED_LIBRARY)
+
diff --git a/jni/jni_egl_fence.cpp b/jni/jni_egl_fence.cpp
new file mode 100644
index 0000000..8d557df
--- /dev/null
+++ b/jni/jni_egl_fence.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "jni_egl_fence.h"
+
+#include <cutils/log.h>
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+void
+Java_com_android_gallery3d_photoeditor_FilterStack_nativeEglSetFenceAndWait(JNIEnv* env,
+                                                                          jobject thiz) {
+  EGLDisplay display = eglGetCurrentDisplay();
+
+  // Create a egl fence and wait for egl to return it.
+  // Additional reference on egl fence sync can be found in:
+  // http://www.khronos.org/registry/vg/extensions/KHR/EGL_KHR_fence_sync.txt
+  EGLSyncKHR fence = eglCreateSyncKHR(display, EGL_SYNC_FENCE_KHR, NULL);
+  if (fence == EGL_NO_SYNC_KHR) {
+    return;
+  }
+
+  EGLint result = eglClientWaitSyncKHR(display,
+                                       fence,
+                                       EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
+                                       EGL_FOREVER_KHR);
+  if (result == EGL_FALSE) {
+    ALOGE("EGL FENCE: error waiting for fence: %#x", eglGetError());
+  }
+  eglDestroySyncKHR(display, fence);
+}
diff --git a/jni/jni_egl_fence.h b/jni/jni_egl_fence.h
new file mode 100644
index 0000000..6b2c20a
--- /dev/null
+++ b/jni/jni_egl_fence.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef COM_ANDROID_GALLERY3D_PHOTOEDITOR_JNI_EGL_FENSE_H
+#define COM_ANDROID_GALLERY3D_PHOTOEDITOR_JNI_EGL_FENSE_H
+
+#include <jni.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT void JNICALL
+Java_com_android_gallery3d_photoeditor_FilterStack_nativeEglSetFenceAndWait(JNIEnv* env,
+                                                                            jobject thiz);
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* COM_ANDROID_GALLERY3D_PHOTOEDITOR_JNI_EGL_FENSE_H */
diff --git a/proguard.flags b/proguard.flags
index a8cb363..8250933 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -8,3 +8,27 @@
 -keep class * extends com.android.gallery3d.common.Entry {
     @com.android.gallery3d.common.Entry$Column <fields>;
 }
+
+# ctors of subclasses of CameraPreference are called with Java reflection.
+-keep class * extends com.android.camera.CameraPreference {
+  <init>(...);
+}
+
+-keep class com.android.camera.ActivityBase {
+  public int getResultCode();
+  public android.content.Intent getResultData();
+}
+
+-keep class com.android.camera.VideoCamera {
+  public boolean isRecording();
+  public void onCancelBgTraining(...);
+  public void onProtectiveCurtainClick(...);
+}
+
+-keep class * extends android.app.Activity {
+  @com.android.camera.OnClickAttr <methods>;
+}
+
+-keep class com.android.camera.CameraHolder {
+  public static void injectMockCamera(...);
+}
diff --git a/res/drawable-hdpi/camera_crop_holo.png b/res/drawable-hdpi/camera_crop_holo.png
index 477e67c..0c3506c 100644
--- a/res/drawable-hdpi/camera_crop_holo.png
+++ b/res/drawable-hdpi/camera_crop_holo.png
Binary files differ
diff --git a/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png b/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png
new file mode 100644
index 0000000..06e5b47
--- /dev/null
+++ b/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/dropdown_normal_holo_dark.9.png b/res/drawable-hdpi/dropdown_normal_holo_dark.9.png
deleted file mode 100644
index 5525025..0000000
--- a/res/drawable-hdpi/dropdown_normal_holo_dark.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/grid_pressed.9.png b/res/drawable-hdpi/grid_pressed.9.png
index 5e1d2e3..dff62c7 100644
--- a/res/drawable-hdpi/grid_pressed.9.png
+++ b/res/drawable-hdpi/grid_pressed.9.png
Binary files differ
diff --git a/res/drawable-hdpi/grid_selected.9.png b/res/drawable-hdpi/grid_selected.9.png
index 24a78d2..d8867ac 100644
--- a/res/drawable-hdpi/grid_selected.9.png
+++ b/res/drawable-hdpi/grid_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi/navstrip_translucent.9.png b/res/drawable-hdpi/navstrip_translucent.9.png
deleted file mode 100644
index 5854af9..0000000
--- a/res/drawable-hdpi/navstrip_translucent.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/thumb_selected.9.png b/res/drawable-hdpi/thumb_selected.9.png
deleted file mode 100644
index 3e762f2..0000000
--- a/res/drawable-hdpi/thumb_selected.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/camera_crop_holo.png b/res/drawable-mdpi/camera_crop_holo.png
index ac56823..7cf744e 100644
--- a/res/drawable-mdpi/camera_crop_holo.png
+++ b/res/drawable-mdpi/camera_crop_holo.png
Binary files differ
diff --git a/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png b/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png
new file mode 100644
index 0000000..81de1bb
--- /dev/null
+++ b/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/dropdown_normal_holo_dark.9.png b/res/drawable-mdpi/dropdown_normal_holo_dark.9.png
deleted file mode 100644
index 5525025..0000000
--- a/res/drawable-mdpi/dropdown_normal_holo_dark.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/grid_pressed.9.png b/res/drawable-mdpi/grid_pressed.9.png
index 74ba39c..68a957f 100644
--- a/res/drawable-mdpi/grid_pressed.9.png
+++ b/res/drawable-mdpi/grid_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi/grid_selected.9.png b/res/drawable-mdpi/grid_selected.9.png
index 0310273..a729636 100644
--- a/res/drawable-mdpi/grid_selected.9.png
+++ b/res/drawable-mdpi/grid_selected.9.png
Binary files differ
diff --git a/res/drawable-mdpi/navstrip_translucent.9.png b/res/drawable-mdpi/navstrip_translucent.9.png
deleted file mode 100644
index c3a0dc0..0000000
--- a/res/drawable-mdpi/navstrip_translucent.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/thumb_selected.9.png b/res/drawable-mdpi/thumb_selected.9.png
deleted file mode 100644
index b1c8fac..0000000
--- a/res/drawable-mdpi/thumb_selected.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/bg_vidcontrol.png b/res/drawable-sw600dp-mdpi/bg_vidcontrol.png
deleted file mode 100644
index dfe2da1..0000000
--- a/res/drawable-sw600dp-mdpi/bg_vidcontrol.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/ic_vidcontrol_pause.png b/res/drawable-sw600dp-mdpi/ic_vidcontrol_pause.png
deleted file mode 100644
index 3d1a8bf..0000000
--- a/res/drawable-sw600dp-mdpi/ic_vidcontrol_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/ic_vidcontrol_play.png b/res/drawable-sw600dp-mdpi/ic_vidcontrol_play.png
deleted file mode 100644
index cc02166..0000000
--- a/res/drawable-sw600dp-mdpi/ic_vidcontrol_play.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/ic_vidcontrol_reload.png b/res/drawable-sw600dp-mdpi/ic_vidcontrol_reload.png
deleted file mode 100644
index bf8d529..0000000
--- a/res/drawable-sw600dp-mdpi/ic_vidcontrol_reload.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/scrubber_knob.png b/res/drawable-sw600dp-mdpi/scrubber_knob.png
deleted file mode 100644
index 426e3da..0000000
--- a/res/drawable-sw600dp-mdpi/scrubber_knob.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp/bg_vidcontrol.png b/res/drawable-sw600dp/bg_vidcontrol.png
index 0eb8148..dfe2da1 100644
--- a/res/drawable-sw600dp/bg_vidcontrol.png
+++ b/res/drawable-sw600dp/bg_vidcontrol.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/ic_pan_thumb.9.png b/res/drawable-sw600dp/ic_pan_thumb.9.png
similarity index 100%
rename from res/drawable-sw600dp-mdpi/ic_pan_thumb.9.png
rename to res/drawable-sw600dp/ic_pan_thumb.9.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_vidcontrol_pause.png b/res/drawable-sw600dp/ic_vidcontrol_pause.png
index 4d274c0..3d1a8bf 100644
--- a/res/drawable-sw600dp/ic_vidcontrol_pause.png
+++ b/res/drawable-sw600dp/ic_vidcontrol_pause.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_vidcontrol_play.png b/res/drawable-sw600dp/ic_vidcontrol_play.png
index 6f97a64..cc02166 100644
--- a/res/drawable-sw600dp/ic_vidcontrol_play.png
+++ b/res/drawable-sw600dp/ic_vidcontrol_play.png
Binary files differ
diff --git a/res/drawable-sw600dp/ic_vidcontrol_reload.png b/res/drawable-sw600dp/ic_vidcontrol_reload.png
index 2aaf491..bf8d529 100644
--- a/res/drawable-sw600dp/ic_vidcontrol_reload.png
+++ b/res/drawable-sw600dp/ic_vidcontrol_reload.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/ic_video_thumb.png b/res/drawable-sw600dp/ic_video_thumb.png
similarity index 100%
rename from res/drawable-sw600dp-mdpi/ic_video_thumb.png
rename to res/drawable-sw600dp/ic_video_thumb.png
Binary files differ
diff --git a/res/drawable-sw600dp/scrubber_knob.png b/res/drawable-sw600dp/scrubber_knob.png
index 4e415a2..426e3da 100644
--- a/res/drawable-sw600dp/scrubber_knob.png
+++ b/res/drawable-sw600dp/scrubber_knob.png
Binary files differ
diff --git a/res/drawable-xhdpi/camera_crop_holo.png b/res/drawable-xhdpi/camera_crop_holo.png
index 2c4eb5c..2987410 100644
--- a/res/drawable-xhdpi/camera_crop_holo.png
+++ b/res/drawable-xhdpi/camera_crop_holo.png
Binary files differ
diff --git a/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png b/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png
new file mode 100644
index 0000000..36d8cf4
--- /dev/null
+++ b/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/grid_pressed.9.png b/res/drawable-xhdpi/grid_pressed.9.png
index 4e50532..ea317b5 100644
--- a/res/drawable-xhdpi/grid_pressed.9.png
+++ b/res/drawable-xhdpi/grid_pressed.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/grid_selected.9.png b/res/drawable-xhdpi/grid_selected.9.png
index 5609df0..dec94ce 100644
--- a/res/drawable-xhdpi/grid_selected.9.png
+++ b/res/drawable-xhdpi/grid_selected.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/thumb_selected.9.png b/res/drawable-xhdpi/thumb_selected.9.png
deleted file mode 100644
index 9ddbd05..0000000
--- a/res/drawable-xhdpi/thumb_selected.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/dark_strip.9.png b/res/drawable/dark_strip.9.png
deleted file mode 100644
index dba0cae..0000000
--- a/res/drawable/dark_strip.9.png
+++ /dev/null
Binary files differ
diff --git a/res/layout/action_bar_text.xml b/res/layout/action_bar_text.xml
index 06a2ac9..2a1f031 100644
--- a/res/layout/action_bar_text.xml
+++ b/res/layout/action_bar_text.xml
@@ -16,6 +16,7 @@
 
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@android:id/text1"
+    android:background="?android:attr/activatedBackgroundIndicator"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:textAppearance="?android:attr/textAppearanceMedium"
diff --git a/res/layout/action_mode.xml b/res/layout/action_mode.xml
index d012b72..d4b3c23 100644
--- a/res/layout/action_mode.xml
+++ b/res/layout/action_mode.xml
@@ -20,14 +20,24 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="horizontal">
-    <Button android:id="@+id/selection_menu"
-            android:divider="?android:attr/listDividerAlertDialog"
-            style="?android:attr/borderlessButtonStyle"
-            android:singleLine="true"
-            android:gravity="left|center_vertical"
+
+    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:layout_width="wrap_content"
-            android:layout_height="match_parent" />
-    <ImageView android:layout_marginLeft="8dip"
+            android:layout_height="match_parent">
+        <ImageView android:layout_gravity="right"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:src="@drawable/dropdown_ic_arrow_normal_holo_dark" />
+        <Button android:id="@+id/selection_menu"
+                android:divider="?android:attr/listDividerAlertDialog"
+                style="?android:attr/borderlessButtonStyle"
+                android:singleLine="true"
+                android:gravity="left|center_vertical"
+                android:paddingRight="25dip"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent" />
+    </FrameLayout>
+    <ImageView android:layout_marginLeft="16dip"
             android:layout_marginRight="8dip"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
diff --git a/res/layout/cropimage.xml b/res/layout/cropimage.xml
index aefebe8..c434fb6 100644
--- a/res/layout/cropimage.xml
+++ b/res/layout/cropimage.xml
@@ -17,9 +17,5 @@
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
-    <view class="com.android.gallery3d.ui.GLRootView"
-            android:id="@+id/gl_root_view"
-            android:background="@null"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
+    <include layout="@layout/gl_root_group"/>
 </FrameLayout>
diff --git a/res/layout/dialog_picker.xml b/res/layout/dialog_picker.xml
index ba3f500..4a625a1 100644
--- a/res/layout/dialog_picker.xml
+++ b/res/layout/dialog_picker.xml
@@ -18,8 +18,7 @@
         android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
-    <com.android.gallery3d.ui.GLRootView
-            android:id="@+id/gl_root_view"
+    <include layout="@layout/gl_root_group"
             android:layout_weight="1"
             android:layout_height="0dp"
             android:layout_width="match_parent"/>
diff --git a/res/layout/gl_root_group.xml b/res/layout/gl_root_group.xml
new file mode 100644
index 0000000..76ff33b
--- /dev/null
+++ b/res/layout/gl_root_group.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <com.android.gallery3d.ui.GLRootView
+            android:id="@+id/gl_root_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+    <View android:id="@+id/gl_root_cover"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@android:color/black"/>
+</merge>
diff --git a/res/layout/main.xml b/res/layout/main.xml
index b71ea50..d367301 100644
--- a/res/layout/main.xml
+++ b/res/layout/main.xml
@@ -1,16 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/gallery_root"
         android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
-    <com.android.gallery3d.ui.GLRootView
-            android:id="@+id/gl_root_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_alignParentBottom="true"
-            android:layout_alignParentLeft="true"
-            android:layout_alignParentRight="true"
-            android:layout_alignParentTop="true"/>
+    <include layout="@layout/gl_root_group"/>
     <FrameLayout android:id="@+id/footer"
             android:visibility="gone"
             android:layout_alignParentBottom="true"
diff --git a/res/layout/movie_view.xml b/res/layout/movie_view.xml
index bd0415c..75b8dfd 100644
--- a/res/layout/movie_view.xml
+++ b/res/layout/movie_view.xml
@@ -15,13 +15,13 @@
 -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/root"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
+        android:id="@+id/movie_view_root"
+        android:background="@android:color/black"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
     <VideoView android:id="@+id/surface_view"
+            android:visibility="invisible"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_centerInParent="true" />
-
 </RelativeLayout>
diff --git a/res/layout/photoeditor_actionbar.xml b/res/layout/photoeditor_actionbar.xml
index 8bd51ed..e0adbbe 100644
--- a/res/layout/photoeditor_actionbar.xml
+++ b/res/layout/photoeditor_actionbar.xml
@@ -36,11 +36,11 @@
 
     <LinearLayout style="@style/ActionBarLinearLayout" android:layout_alignParentRight="true">
 
-        <ImageButton
+        <com.android.gallery3d.photoeditor.ImageActionButton
             android:id="@+id/undo_button"
             style="@style/ImageActionButton"
             android:src="@drawable/photoeditor_undo"/>
-        <ImageButton
+        <com.android.gallery3d.photoeditor.ImageActionButton
             android:id="@+id/redo_button"
             style="@style/ImageActionButton"
             android:src="@drawable/photoeditor_redo"/>
@@ -54,7 +54,7 @@
                 style="@style/TextActionButton"
                 android:layout_width="fill_parent"
                 android:text="@string/save"/>
-            <ImageButton
+            <com.android.gallery3d.photoeditor.ImageActionButton
                 android:id="@+id/share_button"
                 style="@style/ImageActionButton"
                 android:layout_width="fill_parent"
diff --git a/res/layout/photoeditor_color_seekbar.xml b/res/layout/photoeditor_color_seekbar.xml
index fb93869..ca8509d 100644
--- a/res/layout/photoeditor_color_seekbar.xml
+++ b/res/layout/photoeditor_color_seekbar.xml
@@ -16,9 +16,4 @@
 
 <com.android.gallery3d.photoeditor.actions.ColorSeekBar
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/seekbar_width"
-    android:layout_height="@dimen/seekbar_height"
-    android:layout_marginBottom="@dimen/seekbar_margin_bottom"
-    android:minHeight="@dimen/seekbar_height"
-    android:maxHeight="@dimen/seekbar_height"
-    android:progressDrawable="@android:color/transparent"/>
+    style="@style/SeekBar"/>
diff --git a/res/layout/photoeditor_crop_view.xml b/res/layout/photoeditor_crop_view.xml
index 151e6a6..bf5cacb 100644
--- a/res/layout/photoeditor_crop_view.xml
+++ b/res/layout/photoeditor_crop_view.xml
@@ -16,5 +16,4 @@
 
 <com.android.gallery3d.photoeditor.actions.CropView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/fullscreen_effect_tool"
     style="@style/FullscreenToolView"/>
diff --git a/res/layout/photoeditor_doodle_view.xml b/res/layout/photoeditor_doodle_view.xml
index d8e0868..c202f14 100644
--- a/res/layout/photoeditor_doodle_view.xml
+++ b/res/layout/photoeditor_doodle_view.xml
@@ -16,5 +16,4 @@
 
 <com.android.gallery3d.photoeditor.actions.DoodleView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/fullscreen_effect_tool"
     style="@style/FullscreenToolView"/>
diff --git a/res/layout/photoeditor_effect_tool_fullscreen.xml b/res/layout/photoeditor_effect_tool_fullscreen.xml
new file mode 100644
index 0000000..a6dd323
--- /dev/null
+++ b/res/layout/photoeditor_effect_tool_fullscreen.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"/>
diff --git a/res/layout/photoeditor_effect_tool_panel.xml b/res/layout/photoeditor_effect_tool_panel.xml
index 7fd9257..4ffd52d 100644
--- a/res/layout/photoeditor_effect_tool_panel.xml
+++ b/res/layout/photoeditor_effect_tool_panel.xml
@@ -18,12 +18,13 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
-    android:padding="@dimen/effect_tool_panel_padding"
+    android:paddingTop="@dimen/effect_tool_panel_padding_top"
+    android:paddingBottom="@dimen/effect_tool_panel_padding_bottom"
     android:background="@color/translucent_black"
     android:gravity="center_horizontal"
     android:orientation="vertical">
 
     <TextView
         android:id="@+id/effect_label"
-        style="@style/EffectLabel"/>
+        style="@style/EffectLabelInToolPanel"/>
 </LinearLayout>
diff --git a/res/layout/photoeditor_effects_artistic.xml b/res/layout/photoeditor_effects_artistic.xml
index f5eb63a..89cb88c 100644
--- a/res/layout/photoeditor_effects_artistic.xml
+++ b/res/layout/photoeditor_effects_artistic.xml
@@ -19,8 +19,7 @@
     style="@style/EffectsContainer">
 
     <com.android.gallery3d.photoeditor.actions.CrossProcessAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_crossprocess"/>
         <TextView
@@ -29,8 +28,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.CrossProcessAction>
     <com.android.gallery3d.photoeditor.actions.PosterizeAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_posterize"/>
         <TextView
@@ -39,8 +37,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.PosterizeAction>
     <com.android.gallery3d.photoeditor.actions.LomoishAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_lomoish"/>
         <TextView
@@ -49,8 +46,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.LomoishAction>
     <com.android.gallery3d.photoeditor.actions.DocumentaryAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_documentary"/>
         <TextView
@@ -59,8 +55,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.DocumentaryAction>
     <com.android.gallery3d.photoeditor.actions.VignetteAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_vignette"/>
         <TextView
@@ -69,8 +64,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.VignetteAction>
     <com.android.gallery3d.photoeditor.actions.GrainAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_grain"/>
         <TextView
@@ -79,8 +73,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.GrainAction>
     <com.android.gallery3d.photoeditor.actions.FisheyeAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_fisheye"/>
         <TextView
diff --git a/res/layout/photoeditor_effects_color.xml b/res/layout/photoeditor_effects_color.xml
index c078dfa..72cfe7a 100644
--- a/res/layout/photoeditor_effects_color.xml
+++ b/res/layout/photoeditor_effects_color.xml
@@ -19,8 +19,7 @@
     style="@style/EffectsContainer">
 
     <com.android.gallery3d.photoeditor.actions.ColorTemperatureAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_temperature"/>
         <TextView
@@ -29,8 +28,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.ColorTemperatureAction>
     <com.android.gallery3d.photoeditor.actions.SaturationAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_saturation"/>
         <TextView
@@ -39,8 +37,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.SaturationAction>
     <com.android.gallery3d.photoeditor.actions.GrayscaleAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_grayscale"/>
         <TextView
@@ -49,8 +46,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.GrayscaleAction>
     <com.android.gallery3d.photoeditor.actions.SepiaAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_sepia"/>
         <TextView
@@ -59,8 +55,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.SepiaAction>
     <com.android.gallery3d.photoeditor.actions.NegativeAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_negative"/>
         <TextView
@@ -69,8 +64,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.NegativeAction>
     <com.android.gallery3d.photoeditor.actions.TintAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_tint"/>
         <TextView
@@ -79,8 +73,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.TintAction>
     <com.android.gallery3d.photoeditor.actions.DuotoneAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_duotone"/>
         <TextView
@@ -90,8 +83,7 @@
     </com.android.gallery3d.photoeditor.actions.DuotoneAction>
     <com.android.gallery3d.photoeditor.actions.DoodleAction style="@style/Effect"
         android:tag="@string/doodle_tooltip">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_doodle"/>
         <TextView
diff --git a/res/layout/photoeditor_effects_exposure.xml b/res/layout/photoeditor_effects_exposure.xml
index 5a4fb29..b353489 100644
--- a/res/layout/photoeditor_effects_exposure.xml
+++ b/res/layout/photoeditor_effects_exposure.xml
@@ -19,8 +19,7 @@
     style="@style/EffectsContainer">
 
     <com.android.gallery3d.photoeditor.actions.FillLightAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_filllight"/>
         <TextView
@@ -29,8 +28,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.FillLightAction>
     <com.android.gallery3d.photoeditor.actions.HighlightAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_highlight"/>
         <TextView
@@ -39,8 +37,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.HighlightAction>
     <com.android.gallery3d.photoeditor.actions.ShadowAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_shadow"/>
         <TextView
@@ -49,8 +46,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.ShadowAction>
     <com.android.gallery3d.photoeditor.actions.AutoFixAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_autofix"/>
         <TextView
diff --git a/res/layout/photoeditor_effects_fix.xml b/res/layout/photoeditor_effects_fix.xml
index 9a2843c..924190b 100644
--- a/res/layout/photoeditor_effects_fix.xml
+++ b/res/layout/photoeditor_effects_fix.xml
@@ -20,8 +20,7 @@
 
     <com.android.gallery3d.photoeditor.actions.CropAction style="@style/Effect"
         android:tag="@string/crop_tooltip">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_crop"/>
         <TextView
@@ -31,8 +30,7 @@
     </com.android.gallery3d.photoeditor.actions.CropAction>
     <com.android.gallery3d.photoeditor.actions.RedEyeAction style="@style/Effect"
         android:tag="@string/redeye_tooltip">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_redeye"/>
         <TextView
@@ -41,8 +39,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.RedEyeAction>
     <com.android.gallery3d.photoeditor.actions.FaceliftAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_facelift"/>
         <TextView
@@ -51,8 +48,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.FaceliftAction>
     <com.android.gallery3d.photoeditor.actions.FaceTanAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_facetan"/>
         <TextView
@@ -62,8 +58,7 @@
     </com.android.gallery3d.photoeditor.actions.FaceTanAction>
     <com.android.gallery3d.photoeditor.actions.StraightenAction style="@style/Effect"
         android:tag="@string/straighten_tooltip">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_straighten"/>
         <TextView
@@ -73,8 +68,7 @@
     </com.android.gallery3d.photoeditor.actions.StraightenAction>
     <com.android.gallery3d.photoeditor.actions.RotateAction style="@style/Effect"
         android:tag="@string/rotate_tooltip">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_rotate"/>
         <TextView
@@ -84,8 +78,7 @@
     </com.android.gallery3d.photoeditor.actions.RotateAction>
     <com.android.gallery3d.photoeditor.actions.FlipAction style="@style/Effect"
         android:tag="@string/flip_tooltip">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_flip"/>
         <TextView
@@ -94,8 +87,7 @@
             style="@style/EffectLabel"/>
     </com.android.gallery3d.photoeditor.actions.FlipAction>
     <com.android.gallery3d.photoeditor.actions.SharpenAction style="@style/Effect">
-        <ImageButton
-            android:id="@+id/effect_button"
+        <ImageView
             style="@style/EffectIcon"
             android:src="@drawable/photoeditor_effect_sharpen"/>
         <TextView
diff --git a/res/layout/photoeditor_effects_menu.xml b/res/layout/photoeditor_effects_menu.xml
index 1688a90..a0b102d 100644
--- a/res/layout/photoeditor_effects_menu.xml
+++ b/res/layout/photoeditor_effects_menu.xml
@@ -23,19 +23,19 @@
         android:id="@+id/toggles"
         style="@style/EffectsMenuContainer">
 
-        <ImageButton
+        <com.android.gallery3d.photoeditor.ImageActionButton
             android:id="@+id/exposure_button"
             style="@style/EffectsMenuActionButton"
             android:src="@drawable/photoeditor_exposure"/>
-        <ImageButton
+        <com.android.gallery3d.photoeditor.ImageActionButton
             android:id="@+id/artistic_button"
             style="@style/EffectsMenuActionButton"
             android:src="@drawable/photoeditor_artistic"/>
-        <ImageButton
+        <com.android.gallery3d.photoeditor.ImageActionButton
             android:id="@+id/color_button"
             style="@style/EffectsMenuActionButton"
             android:src="@drawable/photoeditor_color"/>
-        <ImageButton
+        <com.android.gallery3d.photoeditor.ImageActionButton
             android:id="@+id/fix_button"
             style="@style/EffectsMenuActionButton"
             android:src="@drawable/photoeditor_fix"/>
diff --git a/res/layout/photoeditor_flip_view.xml b/res/layout/photoeditor_flip_view.xml
index 0b7a7a7..150b24e 100644
--- a/res/layout/photoeditor_flip_view.xml
+++ b/res/layout/photoeditor_flip_view.xml
@@ -16,5 +16,4 @@
 
 <com.android.gallery3d.photoeditor.actions.FlipView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/fullscreen_effect_tool"
     style="@style/FullscreenToolView"/>
diff --git a/res/layout/photoeditor_rotate_view.xml b/res/layout/photoeditor_rotate_view.xml
index 9dbe9d0..0e85fd4 100644
--- a/res/layout/photoeditor_rotate_view.xml
+++ b/res/layout/photoeditor_rotate_view.xml
@@ -16,5 +16,4 @@
 
 <com.android.gallery3d.photoeditor.actions.RotateView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/fullscreen_effect_tool"
     style="@style/FullscreenToolView"/>
diff --git a/res/layout/photoeditor_scale_seekbar.xml b/res/layout/photoeditor_scale_seekbar.xml
index d8df19f..15fc234 100644
--- a/res/layout/photoeditor_scale_seekbar.xml
+++ b/res/layout/photoeditor_scale_seekbar.xml
@@ -16,9 +16,4 @@
 
 <com.android.gallery3d.photoeditor.actions.ScaleSeekBar
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/seekbar_width"
-    android:layout_height="@dimen/seekbar_height"
-    android:layout_marginBottom="@dimen/seekbar_margin_bottom"
-    android:minHeight="@dimen/seekbar_height"
-    android:maxHeight="@dimen/seekbar_height"
-    android:progressDrawable="@android:color/transparent"/>
+    style="@style/SeekBar"/>
diff --git a/res/layout/photoeditor_touch_view.xml b/res/layout/photoeditor_touch_view.xml
index 39a0871..a6da078 100644
--- a/res/layout/photoeditor_touch_view.xml
+++ b/res/layout/photoeditor_touch_view.xml
@@ -16,5 +16,4 @@
 
 <com.android.gallery3d.photoeditor.actions.TouchView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/fullscreen_effect_tool"
     style="@style/FullscreenToolView"/>
diff --git a/res/menu-land/photo.xml b/res/menu-land/photo.xml
deleted file mode 100644
index 023a93b..0000000
--- a/res/menu-land/photo.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2010 Google Inc.
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@+id/action_import"
-            android:title="@string/Import"
-            android:icon="@drawable/ic_menu_ptp_holo_light"
-            android:showAsAction="always|withText"
-            android:visible="false" />
-    <item android:id="@+id/action_share"
-            android:icon="@drawable/ic_menu_share_holo_light"
-            android:title="@string/share"
-            android:enabled="true"
-            android:actionProviderClass="android.widget.ShareActionProvider"
-            android:visible="false"
-            android:showAsAction="ifRoom" />
-    <item android:id="@+id/action_delete"
-            android:icon="@drawable/ic_menu_trash_holo_light"
-            android:title="@string/delete"
-            android:visible="false"
-            android:showAsAction="ifRoom">
-        <menu>
-            <item android:id="@+id/action_confirm_delete"
-                    android:icon="@drawable/ic_menu_trash_holo_light"
-                    android:title="@string/confirm_delete" />
-            <item android:id="@+id/action_cancel_delete"
-                    android:icon="@drawable/ic_menu_cancel_holo_light"
-                    android:title="@string/cancel" />
-        </menu>
-    </item>
-    <item android:id="@+id/action_slideshow"
-            android:icon="@drawable/ic_menu_slideshow_holo_light"
-            android:title="@string/slideshow"
-            android:showAsAction="never" />
-    <item android:id="@+id/action_edit"
-            android:title="@string/edit"
-            android:showAsAction="never"
-            android:visible="false" />
-    <item android:id="@+id/action_rotate_ccw"
-            android:showAsAction="never"
-            android:title="@string/rotate_left" />
-    <item android:id="@+id/action_rotate_cw"
-            android:showAsAction="never"
-            android:title="@string/rotate_right" />
-    <item android:id="@+id/action_crop"
-            android:title="@string/crop_action"
-            android:showAsAction="never" />
-    <item android:id="@+id/action_setas"
-            android:title="@string/set_image"
-            android:showAsAction="never" />
-    <item android:id="@+id/action_details"
-            android:title="@string/details"
-            android:showAsAction="never" />
-    <item android:id="@+id/action_show_on_map"
-            android:title="@string/show_on_map"
-            android:showAsAction="never" />
-</menu>
diff --git a/res/menu/album.xml b/res/menu/album.xml
index 1e1f6ef..93abcca 100644
--- a/res/menu/album.xml
+++ b/res/menu/album.xml
@@ -14,6 +14,10 @@
      limitations under the License.
 -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_camera"
+            android:icon="@drawable/ic_menu_camera_holo_light"
+            android:title="@string/switch_to_camera"
+            android:showAsAction="ifRoom" />
     <item android:id="@+id/action_slideshow"
             android:icon="@drawable/ic_menu_slideshow_holo_light"
             android:title="@string/slideshow"
diff --git a/res/menu/albumset.xml b/res/menu/albumset.xml
index 3bb46f7..8ac8cbb 100644
--- a/res/menu/albumset.xml
+++ b/res/menu/albumset.xml
@@ -30,4 +30,8 @@
     <item android:id="@+id/action_settings"
             android:title="@string/settings"
             android:showAsAction="never" />
+    <item android:id="@+id/action_general_help"
+            android:title="@string/help"
+            android:visible="false"
+            android:showAsAction="never" />
 </menu>
diff --git a/res/menu/operation.xml b/res/menu/operation.xml
index e935684..3225e1e 100644
--- a/res/menu/operation.xml
+++ b/res/menu/operation.xml
@@ -30,16 +30,7 @@
             android:icon="@drawable/ic_menu_trash_holo_light"
             android:title="@string/delete"
             android:visible="false"
-            android:showAsAction="ifRoom">
-        <menu>
-            <item android:id="@+id/action_confirm_delete"
-                    android:icon="@drawable/ic_menu_trash_holo_light"
-                    android:title="@string/confirm_delete" />
-            <item android:id="@+id/action_cancel_delete"
-                    android:icon="@drawable/ic_menu_cancel_holo_light"
-                    android:title="@string/cancel" />
-        </menu>
-    </item>
+            android:showAsAction="ifRoom" />
     <item android:id="@+id/action_edit"
             android:title="@string/edit"
             android:showAsAction="never"
diff --git a/res/menu/photo.xml b/res/menu/photo.xml
index 0ae2549..21f802a 100644
--- a/res/menu/photo.xml
+++ b/res/menu/photo.xml
@@ -30,16 +30,7 @@
             android:icon="@drawable/ic_menu_trash_holo_light"
             android:title="@string/delete"
             android:visible="false"
-            android:showAsAction="never">
-        <menu>
-            <item android:id="@+id/action_confirm_delete"
-                    android:icon="@drawable/ic_menu_trash_holo_light"
-                    android:title="@string/confirm_delete" />
-            <item android:id="@+id/action_cancel_delete"
-                    android:icon="@drawable/ic_menu_cancel_holo_light"
-                    android:title="@string/cancel" />
-        </menu>
-    </item>
+            android:showAsAction="ifRoom" />
     <item android:id="@+id/action_slideshow"
             android:icon="@drawable/ic_menu_slideshow_holo_light"
             android:title="@string/slideshow"
@@ -57,12 +48,12 @@
     <item android:id="@+id/action_crop"
             android:title="@string/crop_action"
             android:showAsAction="never" />
-    <item android:id="@+id/action_details"
-            android:title="@string/details"
-            android:showAsAction="never" />
     <item android:id="@+id/action_setas"
             android:title="@string/set_image"
             android:showAsAction="never" />
+    <item android:id="@+id/action_details"
+            android:title="@string/details"
+            android:showAsAction="never" />
     <item android:id="@+id/action_show_on_map"
             android:title="@string/show_on_map"
             android:showAsAction="never" />
diff --git a/res/mipmap-hdpi/ic_launcher_gallery.png b/res/mipmap-hdpi/ic_launcher_gallery.png
index 34410f8..23ea998 100644
--- a/res/mipmap-hdpi/ic_launcher_gallery.png
+++ b/res/mipmap-hdpi/ic_launcher_gallery.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_gallery.png b/res/mipmap-mdpi/ic_launcher_gallery.png
index 3a701bc..e1a9949 100644
--- a/res/mipmap-mdpi/ic_launcher_gallery.png
+++ b/res/mipmap-mdpi/ic_launcher_gallery.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_gallery.png b/res/mipmap-xhdpi/ic_launcher_gallery.png
index 0c3c2ef..79544a2 100644
--- a/res/mipmap-xhdpi/ic_launcher_gallery.png
+++ b/res/mipmap-xhdpi/ic_launcher_gallery.png
Binary files differ
diff --git a/res/values-af/photoeditor_strings.xml b/res/values-af/photoeditor_strings.xml
index f9e87d8..c180110 100644
--- a/res/values-af/photoeditor_strings.xml
+++ b/res/values-af/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Fotoateljee"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Foto kan nie gelaai word nie"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Foto kan nie gestoor word nie"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto gestoor in <xliff:g id="ALBUM_NAME">%s</xliff:g> album"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Geredigeer"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Stoor geredigeerde foto?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Kon nie die foto laai nie"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Kon nie geredigeerde foto stoor nie"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Geredigeerde foto gestoor in <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Raak ontslae van ongestoorde veranderinge?"</string>
     <string name="yes" msgid="5402582493291792293">"Ja"</string>
-    <string name="no" msgid="5595408018304861875">"Nee"</string>
     <string name="save" msgid="5516670392524294967">"Stoor"</string>
     <string name="autofix" msgid="1663414996270538748">"Outokorrigeer"</string>
     <string name="crop" msgid="7598378507763334041">"Snoei"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Trek merkers om foto te sny"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Teken op foto om te krabbel"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Trek foto om dit om te keer"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Tik om rooi oë te verwyder"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Tik op rooi oë om hulle te verwyder"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Trek foto om dit te draai"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Trek foto om dit reguit te trek"</string>
 </resources>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 8feab87..e3c1e8c 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Speel verder"</string>
     <string name="loading" msgid="7038208555304563571">"Laai tans…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Kon nie laai nie"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kon nie die prent laai nie"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Geen kleinkiekie nie"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Begin van voor af"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Snoei"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Raak aan \'n gesig om te begin."</string>
     <string name="saving_image" msgid="7270334453636349407">"Stoor tans prent…"</string>
     <string name="save_error" msgid="6857408774183654970">"Kon nie gesnoeide prent stoor nie."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Kies album"</string>
     <string name="select_group" msgid="6744208543323307114">"Kies groep"</string>
     <string name="set_image" msgid="2331476809308010401">"Stel prent as"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Stel muurpapier"</string>
     <string name="wallpaper" msgid="140165383777262070">"Stel tans muurpapier in..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Muurpapier"</string>
     <string name="delete" msgid="2839695998251824487">"Vee uit"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Vee uit"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vee gekose item uit?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vee gekose items uit?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bevestig"</string>
     <string name="cancel" msgid="3637516880917356226">"Kanselleer"</string>
     <string name="share" msgid="3619042788254195341">"Deling"</string>
     <string name="select_all" msgid="3403283025220282175">"Kies almal"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Draai na regs"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Kon nie die item vind nie."</string>
     <string name="edit" msgid="1502273844748580847">"Redigeer"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Geen program is beskikbaar om die handeling te voltooi nie."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Verwerk kasversoeke"</string>
     <string name="caching_label" msgid="4521059045896269095">"Kas tans..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Snoei"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Ongemerk"</string>
     <string name="no_location" msgid="4043624857489331676">"Geen ligging nie"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Sommige liggings kon nie geïdentifiseer word nie weens netwerkprobleme."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Kon nie die foto\'s in hierdie album aflaai nie. Probeer later weer."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Kon nie die lys van albums aflaai nie. Probeer later weer."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Kon nie die foto\'s in hierdie album aflaai nie. Probeer later weer."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Net prente"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Net video\'s"</string>
-    <string name="show_all" msgid="4780647751652596980">"Prente en video\'s"</string>
+    <string name="show_all" msgid="6963292714584735149">"Prente en video\'s"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalery"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Geen foto\'s nie."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Gesnoeide prent gestoor in Aflaaisels."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Gesnoeide prent is nie gestoor nie."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Gesnoeide prent gestoor in <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Geen albums beskikbaar nie."</string>
     <string name="empty_album" msgid="4542880442593595494">"O prente/video\'s beskikbaar."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Plasings"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Groepeer volgens"</string>
     <string name="settings" msgid="1534847740615665736">"Instellings"</string>
     <string name="add_account" msgid="4271217504968243974">"Voeg rekening by"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Laai af"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Ingevoer"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skermkiekie"</string>
+    <string name="help" msgid="7368960711153618354">"Hulp"</string>
 </resources>
diff --git a/res/values-am/photoeditor_strings.xml b/res/values-am/photoeditor_strings.xml
index 83971d6..7575930 100644
--- a/res/values-am/photoeditor_strings.xml
+++ b/res/values-am/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"ፎቶ ሊሰቀል አይችልም"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"ፎቶ ሊሰቀል አልተቻለም"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"ፎቶ ወደ <xliff:g id="ALBUM_NAME">%s</xliff:g> አልበም ተቀምጧል"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"አርትእ ተደርጓል"</string>
-    <string name="save_photo" msgid="3125109368779997862">"ማስተካከያ የተደረገበት ፎቶ ይቀመጥ?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"ፎቶውን መጫን አልተቻለም"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"አርትዖት የተደረገበት ፎቶ ማስቀመጥ አልተቻለም"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"አርትዖት የተደረገበት ፎቶ <xliff:g id="FOLDER_NAME">%s</xliff:g> ላይ ተቀምጧል"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"ያልተቀመጡ ለውጦች ይወገዱ?"</string>
     <string name="yes" msgid="5402582493291792293">"አዎ"</string>
-    <string name="no" msgid="5595408018304861875">"አይ"</string>
     <string name="save" msgid="5516670392524294967">"አስቀምጥ"</string>
     <string name="autofix" msgid="1663414996270538748">"በራስ-ጠግን"</string>
     <string name="crop" msgid="7598378507763334041">"ክፈፍ"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"ፎቶ ለመከርከም አመልካቾችን ጎትት"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"ፎቶውን doodle ለማድረግ ሳል"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"ፎቶውን በፍጥነት ለመግለጥ ጎትት"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"ቀይነጥብ ለማስወገድ ሁለቴ ንካ"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"ቀይ አይኖችን ለማስወገድ እነሱ ላይ መታ አድርግ"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"ፎቶውንለማሽከርከር ጎትት"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"ፎቶውንቀጥ ለማደረግ ጎትት"</string>
 </resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index e93b7d9..b88187f 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"ማጫወት ቀጥል"</string>
     <string name="loading" msgid="7038208555304563571">"በመስቀል ላይ…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"መስቀል አልተቻለም"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"ምስሉን መጫን አልተቻለም"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"ምንም ጥፍርአከል የለም"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"እንደገና ጀምር"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"ከርክም"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"እሺ"</string>
+    <string name="ok" msgid="5296833083983263293">"እሺ"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"ለመጀመር ፊት ንካ::"</string>
     <string name="saving_image" msgid="7270334453636349407">"ምስል በማስቀመጥ ላይ..."</string>
     <string name="save_error" msgid="6857408774183654970">"የተቀመቀመውን ምስል ማስቀመጥ አልተቻለም::"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"አልበም ምረጥ"</string>
     <string name="select_group" msgid="6744208543323307114">"ቡድን ምረጥ"</string>
     <string name="set_image" msgid="2331476809308010401">"ምስል እንደ አዘጋጅ"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"ልጣፍ አዘጋጅ"</string>
     <string name="wallpaper" msgid="140165383777262070">"ልጣፍ በማቀናበር ላይ...."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"ልጣፍ"</string>
     <string name="delete" msgid="2839695998251824487">"ሰርዝ"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"ሰርዝ"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"የተመረጠው ንጥል ይሰረዝ?"</item>
+    <item quantity="other" msgid="5874316486520635333">"የተመረጡት ንጥሎች ይሰረዙ?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"አረጋግጥ"</string>
     <string name="cancel" msgid="3637516880917356226">"ይቅር"</string>
     <string name="share" msgid="3619042788254195341">"አጋራ"</string>
     <string name="select_all" msgid="3403283025220282175">"ሁሉንም ምረጥ"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"ወደ ቀኝ አሽከርክር"</string>
     <string name="no_such_item" msgid="5315144556325243400">"ሊገኙ አልተቻለም::"</string>
     <string name="edit" msgid="1502273844748580847">"አርትዕ"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"ይህን እርምጃ ለማጠናቀቅ ምንም መተግበሪያ አልተገኘም::"</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"መሸጎጫ ጥየቃዎች ሂደት"</string>
     <string name="caching_label" msgid="4521059045896269095">"በመሸጎጥ ላይ..."</string>
     <string name="crop_action" msgid="3427470284074377001">"ከርክም"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"ያልተለጠፈ"</string>
     <string name="no_location" msgid="4043624857489331676">"ምንም ስፍራ የለም"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"አንዳንድ ስፍራዎች በአውታረ መረብ  ችግር ምክንያት ለየቶ ማወቅ አልተቻለም::"</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"በዚህ አልበም ላይ ያሉ ፎቶዎችን ለማውረድ አልተሳካም:: እባክህ እንደገና ሞክር::"</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"የአልበሞችን ዝርዝር ለማውረድ አልተሳካም:: እባክህ እንደገና ሞክር::"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"በዚህ አልበም ላይ ያሉ ፎቶዎችን ለማውረድ አልተቻለም፡፡ በኋላ  ደግመህ ሞክር፡፡"</string>
     <string name="show_images_only" msgid="7263218480867672653">"ምስሎች ብቻ"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"ቪዲዮዎች ብቻ"</string>
-    <string name="show_all" msgid="4780647751652596980">"ምስሎች እና ቪዲዮዎች"</string>
+    <string name="show_all" msgid="6963292714584735149">"ምስሎች &amp; ቪዲዮዎች"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"የፎቶ ሥነ ጥበብ ማዕከል"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"ምንም ፎቶዎች የሉም፡፡"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"የተከረከመ ምስል ወደ አውርዶች ተቀምጧል::"</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"የተከረከመው ምስል አልተቀመጠም::"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"የተከረከመ ምስል <xliff:g id="FOLDER_NAME">%s</xliff:g> ላይ ተቀምጧል።"</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"ምንም አልበሞች አልተገኙም::"</string>
     <string name="empty_album" msgid="4542880442593595494">"O ምስሎች/ ቪዲዮዎች ማግኘት ይቻላል::"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"ልኡክ ጽሁፎች"</string>
@@ -160,6 +164,11 @@
     <string name="people" msgid="4114003823747292747">"ሰዎች"</string>
     <string name="tags" msgid="5539648765482935955">"መለያዎች"</string>
     <string name="group_by" msgid="4308299657902209357">"በቡድን አስቀምጥ"</string>
-    <string name="settings" msgid="1534847740615665736">"ቅንጅቶች"</string>
+    <string name="settings" msgid="1534847740615665736">"ቅንብሮች"</string>
     <string name="add_account" msgid="4271217504968243974">"መለያ አክል"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"ካሜራ"</string>
+    <string name="folder_download" msgid="7186215137642323932">"አውርድ"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"ከውጭ የገባ"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"ቅጽበታዊ ገጽ እይታ"</string>
+    <string name="help" msgid="7368960711153618354">"እገዛ"</string>
 </resources>
diff --git a/res/values-ar/photoeditor_strings.xml b/res/values-ar/photoeditor_strings.xml
index 301481c..7f75cc7 100644
--- a/res/values-ar/photoeditor_strings.xml
+++ b/res/values-ar/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"لا يمكن تحميل الصورة"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"لا يمكن حفظ الصورة"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"تم حفظ الصورة إلى الألبوم <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"صور معدلة"</string>
-    <string name="save_photo" msgid="3125109368779997862">"هل تريد حفظ الصورة التي تم تعديلها؟"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"تعذر تحميل الصورة"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"تعذر حفظ الصورة المعدلة"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"تم حفظ الصورة المعدلة في <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"هل تريد إلغاء أي تغييرات غير محفوظة؟"</string>
     <string name="yes" msgid="5402582493291792293">"نعم"</string>
-    <string name="no" msgid="5595408018304861875">"لا"</string>
     <string name="save" msgid="5516670392524294967">"حفظ"</string>
     <string name="autofix" msgid="1663414996270538748">"إصلاح تلقائي"</string>
     <string name="crop" msgid="7598378507763334041">"اقتصاص"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"اسحب العلامات لاقتصاص الصورة"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"اسحب الصورة لإنشاء رسومات مبتكرة"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"اسحب الصورة لقلبها"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"انقر لإزالة الأعين الحمراء"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"انقر على العيون الحمراء لإزالتها"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"اسحب الصورة لتدويرها"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"اسحب الصورة لتسويتها"</string>
 </resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 922919c..fe3a211 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"استئناف التشغيل"</string>
     <string name="loading" msgid="7038208555304563571">"جارٍ التحميل…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"تعذر التحميل"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"تعذر تحميل الصورة"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"بلا صورة مصغرة"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"البدء من جديد"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"اقتصاص"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"موافق"</string>
+    <string name="ok" msgid="5296833083983263293">"موافق"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"المس وجهًا للبدء."</string>
     <string name="saving_image" msgid="7270334453636349407">"جارٍ حفظ الصورة..."</string>
     <string name="save_error" msgid="6857408774183654970">"تعذر حفظ الصورة التي تم اقتصاصها."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"تحديد ألبوم"</string>
     <string name="select_group" msgid="6744208543323307114">"تحديد مجموعة"</string>
     <string name="set_image" msgid="2331476809308010401">"تعيين الصورة كـ"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"تعيين خلفية"</string>
     <string name="wallpaper" msgid="140165383777262070">"جارٍ تعيين الخلفية..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"الخلفية"</string>
     <string name="delete" msgid="2839695998251824487">"حذف"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"حذف"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"هل تريد حذف العنصر المحدد؟"</item>
+    <item quantity="other" msgid="5874316486520635333">"هل تريد حذف العناصر المحددة؟"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"تأكيد"</string>
     <string name="cancel" msgid="3637516880917356226">"إلغاء"</string>
     <string name="share" msgid="3619042788254195341">"مشاركة"</string>
     <string name="select_all" msgid="3403283025220282175">"تحديد الكل"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"تدوير لليمين"</string>
     <string name="no_such_item" msgid="5315144556325243400">"تعذر العثور على العنصر."</string>
     <string name="edit" msgid="1502273844748580847">"تعديل"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"ليس هناك تطبيق متاح لإكمال الإجراء."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"معالجة طلبات التخزين المؤقت"</string>
     <string name="caching_label" msgid="4521059045896269095">"تخزين مؤقت..."</string>
     <string name="crop_action" msgid="3427470284074377001">"اقتصاص"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"بلا علامات"</string>
     <string name="no_location" msgid="4043624857489331676">"لا موقع"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"تعذر تحديد بعض المواقع بسبب مشاكل في الشبكة."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"أخفق تنزيل الصور في هذا الألبوم. الرجاء إعادة المحاولة."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"أخفق تنزيل قائمة الألبومات. الرجاء إعادة المحاولة."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"تعذر تحميل الصور في هذا الألبوم. أعد المحاولة لاحقًا."</string>
     <string name="show_images_only" msgid="7263218480867672653">"الصور فقط"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"مقاطع الفيديو فقط"</string>
-    <string name="show_all" msgid="4780647751652596980">"الصور ومقاطع الفيديو"</string>
+    <string name="show_all" msgid="6963292714584735149">"الصور ومقاطع الفيديو"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"معرض الصور"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"ليست هناك أية صور."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"تم حفظ الصورة التي تم اقتصاصها إلى التنزيلات."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"لم يتم حفظ الصورة التي تم اقتصاصها."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"تم حفظ الصورة التي تم اقتصاصها في <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"ليست هناك أية ألبومات متاحة."</string>
     <string name="empty_album" msgid="4542880442593595494">"لا تتوفر أية صور/مقاطع فيديو."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"المشاركات"</string>
@@ -155,11 +159,16 @@
     <string name="widget_type" msgid="1364653978966343448">"اختيار صور"</string>
     <string name="slideshow_dream_name" msgid="6915963319933437083">"عرض الشرائح"</string>
     <string name="albums" msgid="7320787705180057947">"ألبومات"</string>
-    <string name="times" msgid="2023033894889499219">"المرات"</string>
+    <string name="times" msgid="2023033894889499219">"التواريخ"</string>
     <string name="locations" msgid="6649297994083130305">"المواقع"</string>
     <string name="people" msgid="4114003823747292747">"الأشخاص"</string>
     <string name="tags" msgid="5539648765482935955">"العلامات"</string>
     <string name="group_by" msgid="4308299657902209357">"تجميع بحسب"</string>
     <string name="settings" msgid="1534847740615665736">"الإعدادات"</string>
     <string name="add_account" msgid="4271217504968243974">"إضافة حساب"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"الكاميرا"</string>
+    <string name="folder_download" msgid="7186215137642323932">"التنزيل"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"المستوردة"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"لقطة شاشة"</string>
+    <string name="help" msgid="7368960711153618354">"المساعدة"</string>
 </resources>
diff --git a/res/values-be/photoeditor_strings.xml b/res/values-be/photoeditor_strings.xml
index 8984b14..6fa477b 100644
--- a/res/values-be/photoeditor_strings.xml
+++ b/res/values-be/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Фотастудыя"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Немагчыма загрузіць фатаграфію"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Немагчыма захаваць фатаграфію"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Фатаграфія захаваная ў альбом <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Рэдагаванае"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Захаваць адрэдагаванае фота?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Немагчыма загрузіць фота"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Немагчыма захаваць адрэдагаванае фота"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Адрэдагаванае фота захавана ў тэчцы <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Скасаваць незахаваныя змены?"</string>
     <string name="yes" msgid="5402582493291792293">"Так"</string>
-    <string name="no" msgid="5595408018304861875">"Не"</string>
     <string name="save" msgid="5516670392524294967">"ЗАХАВАЦЬ"</string>
     <string name="autofix" msgid="1663414996270538748">"Аўтафікс."</string>
     <string name="crop" msgid="7598378507763334041">"Абрэзаць"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Перацягнуць маркеры, каб абрэзаць фота"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Перац. фота, каб пераўтв. яго ў святоч. лагатып"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Перацягніце фатаграфію, каб перавярнуць яе"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Націсніце, каб лiквiдаваць эфект чырвоных вачэй"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Нац. на \"чырвоныя вочы\", каб выдаліць іх"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Перацягніце фатаграфію, каб павярнуць яе"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Перацягніце фатаграфію, каб выпраміць яе"</string>
 </resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 718c81f..1454704 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Працягнуць прайграванне"</string>
     <string name="loading" msgid="7038208555304563571">"Загрузка..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Не атрымалася загрузiць"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Не атрымалася загрузіць малюнак"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Няма паменшанай выявы"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Пачаць зноў"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Абрэзаць"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ОК"</string>
+    <string name="ok" msgid="5296833083983263293">"ОК"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Дакранiцеся да твару, каб пачаць."</string>
     <string name="saving_image" msgid="7270334453636349407">"Захаванне выявы..."</string>
     <string name="save_error" msgid="6857408774183654970">"Немагчыма захаваць абрэзаную выяву."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Выбраць альбом"</string>
     <string name="select_group" msgid="6744208543323307114">"Выбар групы"</string>
     <string name="set_image" msgid="2331476809308010401">"Усталяваць малюнак як"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Усталяваць шпалеры"</string>
     <string name="wallpaper" msgid="140165383777262070">"Усталяванне шпалер..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Шпалеры"</string>
     <string name="delete" msgid="2839695998251824487">"Выдаліць"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Выдаліць"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Выдаліць выбраны элемент?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Выдаліць выбраныя элементы?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Пацвердзіць"</string>
     <string name="cancel" msgid="3637516880917356226">"Адмяніць"</string>
     <string name="share" msgid="3619042788254195341">"Апублікаваць"</string>
     <string name="select_all" msgid="3403283025220282175">"Выбраць усё"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Павярнуць направа"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Элемент не знойдзены."</string>
     <string name="edit" msgid="1502273844748580847">"Рэдагаваць"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Няма даступных прыкладанняў, каб завершыць гэта дзеянне."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Запыты на кэшаванне працэсу"</string>
     <string name="caching_label" msgid="4521059045896269095">"Кэшаванне..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Абрэзаць"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Неазначаныя"</string>
     <string name="no_location" msgid="4043624857489331676">"Няма месцазнаходж."</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Некаторыя месцы не могуць быць вызначаны з-за праблем сеткі."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Не атрымлiваецца загрузiць фатаграфii з гэтага альбома. Паспрабуйце пазней."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Не атрымлiваецца загрузіць спіс альбомаў. Паспрабуйце пазней."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Немагчыма спампаваць фатаграфіі ў гэтым альбоме. Паўтарыце спробу пазней."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Толькі малюнкі"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Толькі відэа"</string>
-    <string name="show_all" msgid="4780647751652596980">"Выявы і відэа"</string>
+    <string name="show_all" msgid="6963292714584735149">"Выявы і відэа"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Фотагалерэя"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Фатаграфій няма"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Абрэзаная выява захоўваецца ў тэчцы спампаванняў."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Абрэзаная выява не захавана."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Абрэзаная выява захавана ў тэчцы <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Няма альбомаў."</string>
     <string name="empty_album" msgid="4542880442593595494">"Выяў/вiдэа: 0."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Паведамленнi"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Групаваць па"</string>
     <string name="settings" msgid="1534847740615665736">"Налады"</string>
     <string name="add_account" msgid="4271217504968243974">"Дадаць уліковы запіс"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Спампаваць"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Імпартаваныя"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Скрыншот"</string>
+    <string name="help" msgid="7368960711153618354">"Даведка"</string>
 </resources>
diff --git a/res/values-bg/photoeditor_strings.xml b/res/values-bg/photoeditor_strings.xml
index facb0d1..b31bf21 100644
--- a/res/values-bg/photoeditor_strings.xml
+++ b/res/values-bg/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Снимката не може да бъде заредена"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Снимката не може да бъде запазена"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Снимката е запазена в албума „<xliff:g id="ALBUM_NAME">%s</xliff:g>“"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Редактирани"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Да се запази ли редактираната снимка?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Снимката не можа да бъде заредена"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Редакт. снимка не можа да бъде запазена"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Редактираната снимка е запазена в/ъв „<xliff:g id="FOLDER_NAME">%s</xliff:g>“"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Да се отхвърлят ли незапазените промени?"</string>
     <string name="yes" msgid="5402582493291792293">"Да"</string>
-    <string name="no" msgid="5595408018304861875">"Нe"</string>
     <string name="save" msgid="5516670392524294967">"ЗАПАЗВ."</string>
     <string name="autofix" msgid="1663414996270538748">"Автокоригиране"</string>
     <string name="crop" msgid="7598378507763334041">"Подрязване"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Плъзнете маркерите, за да подрежете снимката"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Рисувайте директно върху снимката"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Плъзнете с пръст върху снимката, за да я обърнете"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Докоснете, за да премахнете „червени очи“"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Докоснете „черв. очи“ за премахването им"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Плъзнете с пръст върху снимката, за да я завъртите"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Плъзнете с пръст върху снимката, за да я изправите"</string>
 </resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index b4f7474..da21938 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Продължаване"</string>
     <string name="loading" msgid="7038208555304563571">"Зарежда се…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Не можаха да се заредят"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Изображението не можа да бъде заредено"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Няма миниизображение"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Стартиране отначало"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Подрязване"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Докоснете лице за начало."</string>
     <string name="saving_image" msgid="7270334453636349407">"Снимката се запазва..."</string>
     <string name="save_error" msgid="6857408774183654970">"Подрязаното изобр. не можа да се запази."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Изберете албум"</string>
     <string name="select_group" msgid="6744208543323307114">"Изберете група"</string>
     <string name="set_image" msgid="2331476809308010401">"Задаване на снимката като"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Задаване на тапет"</string>
     <string name="wallpaper" msgid="140165383777262070">"Тапетът се задава..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Тапет"</string>
     <string name="delete" msgid="2839695998251824487">"Изтриване"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Изтриване"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Да се изтрие ли избраното?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Да се изтрият ли избраните?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Потвърждаване"</string>
     <string name="cancel" msgid="3637516880917356226">"Отказ"</string>
     <string name="share" msgid="3619042788254195341">"Споделяне"</string>
     <string name="select_all" msgid="3403283025220282175">"Избиране на всички"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Завъртане надясно"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Елементът не можа да бъде намерен."</string>
     <string name="edit" msgid="1502273844748580847">"Редактиране"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Няма налични прилож. за завършване на действието."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Заявките за кеширане се обработват"</string>
     <string name="caching_label" msgid="4521059045896269095">"Кешира се..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Подрязване"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Немаркирани"</string>
     <string name="no_location" msgid="4043624857489331676">"Няма местоположение"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Някои местоположения не можаха да бъдат идентифицирани поради проблеми с мрежата."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Изтеглянето на снимките в този албум не бе успешно. Моля, опитайте отново по-късно."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Изтеглянето на списъка с албуми не бе успешно. Моля, опитайте отново по-късно."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Снимките в този албум не можаха да се изтеглят. Опитайте отново по-късно."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Само изображения"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Само видеоклипове"</string>
-    <string name="show_all" msgid="4780647751652596980">"Изображения и видеоклипове"</string>
+    <string name="show_all" msgid="6963292714584735149">"Изображения и видеоклипове"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерия"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Няма снимки."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Подрязаното изображение е запазено в „Изтегляния“."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Подрязаното изображение не бе запазено."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Подрязаното изображение е запазено в/ъв „<xliff:g id="FOLDER_NAME">%s</xliff:g>“."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Няма налични албуми."</string>
     <string name="empty_album" msgid="4542880442593595494">"0 изображения/видеоклипове са налице."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Публикации"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Групиране по"</string>
     <string name="settings" msgid="1534847740615665736">"Настройки"</string>
     <string name="add_account" msgid="4271217504968243974">"Добавяне на профил"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Изтегляне"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Импортирани"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Екранна снимка"</string>
+    <string name="help" msgid="7368960711153618354">"Помощ"</string>
 </resources>
diff --git a/res/values-ca/photoeditor_strings.xml b/res/values-ca/photoeditor_strings.xml
index 9c2d31d..8313965 100644
--- a/res/values-ca/photoeditor_strings.xml
+++ b/res/values-ca/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Estudi de fotografia"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"La foto no es pot carregar"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"La foto no es pot desar"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"S\'ha desat la foto a l\'àlbum <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Editades"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Vols desar la foto editada?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"No s\'ha pogut carregar la foto"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"No s\'ha pogut desar la foto editada"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"S\'ha desat la foto editada a <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Vols descartar els canvis no desats?"</string>
     <string name="yes" msgid="5402582493291792293">"Sí"</string>
-    <string name="no" msgid="5595408018304861875">"No"</string>
     <string name="save" msgid="5516670392524294967">"DESA"</string>
     <string name="autofix" msgid="1663414996270538748">"Correcció automàtica"</string>
     <string name="crop" msgid="7598378507763334041">"Retalla"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Arrossega els marcadors per retallar la foto"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Arrossega el cursor sobre la foto per dibuixar"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Arrossega la foto per invertir-la"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Toca per eliminar els ulls vermells"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Toca els ulls vermells per eliminar-los"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Arrossega la foto per girar-la"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Arrossega la foto per redreçar-la"</string>
 </resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index e053e4e..e2a8359 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Reprèn la reproducció"</string>
     <string name="loading" msgid="7038208555304563571">"S\'està carregant…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"No s\'ha pogut carregar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"No s\'ha pogut carregar la imatge"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"No hi ha cap miniatura"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Torna a començar"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Retalla"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"D\'acord"</string>
+    <string name="ok" msgid="5296833083983263293">"Accepta"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Toca una cara per començar."</string>
     <string name="saving_image" msgid="7270334453636349407">"S\'està desant la imatge..."</string>
     <string name="save_error" msgid="6857408774183654970">"No s\'ha pogut desar la imatge retallada."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Selecciona un àlbum"</string>
     <string name="select_group" msgid="6744208543323307114">"Selecciona un grup"</string>
     <string name="set_image" msgid="2331476809308010401">"Defineix la imatge com a"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Defineix el fons de pantalla"</string>
     <string name="wallpaper" msgid="140165383777262070">"S\'està establint el fons de pantalla..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fons de pantalla"</string>
     <string name="delete" msgid="2839695998251824487">"Suprimeix"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Suprimeix"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vols supr. l\'element selecc.?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vols supr. els elements sel.?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirma"</string>
     <string name="cancel" msgid="3637516880917356226">"Cancel·la"</string>
     <string name="share" msgid="3619042788254195341">"Comparteix"</string>
     <string name="select_all" msgid="3403283025220282175">"Selecciona-ho tot"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Gira a la dreta"</string>
     <string name="no_such_item" msgid="5315144556325243400">"No s\'ha trobat l\'element."</string>
     <string name="edit" msgid="1502273844748580847">"Edita"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"No hi ha cap aplicació disponible per completar l\'acció."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"S\'estan processant les sol·licituds de memòria cau"</string>
     <string name="caching_label" msgid="4521059045896269095">"S\'està desant a la memòria cau..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Retalla"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Sense etiquetar"</string>
     <string name="no_location" msgid="4043624857489331676">"Sense ubicació"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Hi ha ubicacions que no s\'han pogut identificar a causa de problemes amb la xarxa."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"No s\'han pogut baixar les fotos en aquest àlbum. Torna-ho a provar més tard."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"No s\'ha pogut baixar la llista d\'àlbums. Torna-ho a provar més tard."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"No s\'han pogut baixar les fotos en aquest àlbum. Torna-ho a provar més tard."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Només imatges"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Només vídeos"</string>
-    <string name="show_all" msgid="4780647751652596980">"Imatges i vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imatges i vídeos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotos"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Sense fotos."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"S\'ha desat la imatge retallada a Baixades."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"La imatge retallada no s\'ha desat."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"S\'ha desat la imatge retallada a <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"No hi ha àlbums disponibles."</string>
     <string name="empty_album" msgid="4542880442593595494">"O imatges/vídeos disponibles."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Publicacions"</string>
@@ -155,11 +159,16 @@
     <string name="widget_type" msgid="1364653978966343448">"Selecció d\'imatges"</string>
     <string name="slideshow_dream_name" msgid="6915963319933437083">"Pres. diapositives"</string>
     <string name="albums" msgid="7320787705180057947">"Àlbums"</string>
-    <string name="times" msgid="2023033894889499219">"Vegades"</string>
+    <string name="times" msgid="2023033894889499219">"Dates"</string>
     <string name="locations" msgid="6649297994083130305">"Ubicacions"</string>
     <string name="people" msgid="4114003823747292747">"Persones"</string>
     <string name="tags" msgid="5539648765482935955">"Etiquetes"</string>
     <string name="group_by" msgid="4308299657902209357">"Agrupa per"</string>
     <string name="settings" msgid="1534847740615665736">"Configuració"</string>
     <string name="add_account" msgid="4271217504968243974">"Afegeix un compte"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Càmera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Baixades"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importades"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captura de pantalla"</string>
+    <string name="help" msgid="7368960711153618354">"Ajuda"</string>
 </resources>
diff --git a/res/values-cs/photoeditor_strings.xml b/res/values-cs/photoeditor_strings.xml
index 48ef932..187c236 100644
--- a/res/values-cs/photoeditor_strings.xml
+++ b/res/values-cs/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotku nelze nahrát."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotku nelze uložit."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotka uložena do alba <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Upraveno"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Chcete upravenou fotografii uložit?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotografii nelze načíst"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Upravená fotografie nelze uložit"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Upravená fotografie uložena do složky <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Zahodit neuložené změny?"</string>
     <string name="yes" msgid="5402582493291792293">"Ano"</string>
-    <string name="no" msgid="5595408018304861875">"Ne"</string>
     <string name="save" msgid="5516670392524294967">"ULOŽIT"</string>
     <string name="autofix" msgid="1663414996270538748">"Automat. oprava"</string>
     <string name="crop" msgid="7598378507763334041">"Oříznout"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Táhnutím značek fotografii oříznete"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Na fotografii můžete kreslit prstem"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Táhnutím prstem fotografii převrátíte"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Klepnutím odstraníte červené oči"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Červené oči odstraníte klepnutím na oči"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Táhnutím prstem fotografii otočíte"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Táhnutím prstem fotografii narovnáte"</string>
 </resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 780295e..9cb8835 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -29,23 +29,30 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Pokračovat v přehrávání"</string>
     <string name="loading" msgid="7038208555304563571">"Načítání..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nelze načíst"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Obrázek nelze načíst"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Miniatura není dostupná"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Začít znovu"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Oříznutí"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Začněte klepnutím na obličej."</string>
     <string name="saving_image" msgid="7270334453636349407">"Ukládání fotografie..."</string>
     <string name="save_error" msgid="6857408774183654970">"Oříznutý obrázek nelze uložit."</string>
     <string name="crop_label" msgid="521114301871349328">"Oříznout fotografii"</string>
-    <string name="select_image" msgid="7841406150484742140">"Vyberte fotografii"</string>
-    <string name="select_video" msgid="4859510992798615076">"Vyberte video"</string>
-    <string name="select_item" msgid="2816923896202086390">"Vyberte položku"</string>
-    <string name="select_album" msgid="1557063764849434077">"Vyberte album"</string>
-    <string name="select_group" msgid="6744208543323307114">"Vyberte skupinu"</string>
-    <string name="set_image" msgid="2331476809308010401">"Fotografie bude použita jako"</string>
+    <string name="select_image" msgid="7841406150484742140">"Vybrat fotografii"</string>
+    <string name="select_video" msgid="4859510992798615076">"Vybrat video"</string>
+    <string name="select_item" msgid="2816923896202086390">"Vybrat položku"</string>
+    <string name="select_album" msgid="1557063764849434077">"Vybrat album"</string>
+    <string name="select_group" msgid="6744208543323307114">"Vybrat skupinu"</string>
+    <string name="set_image" msgid="2331476809308010401">"Nastavit fotografii jako"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nastavit tapetu"</string>
     <string name="wallpaper" msgid="140165383777262070">"Nastavování tapety..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
     <string name="delete" msgid="2839695998251824487">"Smazat"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Smazat"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Smazat vybranou položku?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Smazat vybrané položky?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potvrdit"</string>
     <string name="cancel" msgid="3637516880917356226">"Zrušit"</string>
     <string name="share" msgid="3619042788254195341">"Sdílet"</string>
     <string name="select_all" msgid="3403283025220282175">"Vybrat vše"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Otočit doprava"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Položku nelze najít."</string>
     <string name="edit" msgid="1502273844748580847">"Upravit"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Není k dispozici žádná aplikace k dokončení akce."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Zpracování požadavků na uložení do mezipaměti"</string>
     <string name="caching_label" msgid="4521059045896269095">"Ukládání do mezipaměti..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Oříznout"</string>
@@ -84,26 +90,24 @@
     <string name="group_by_location" msgid="316641628989023253">"Podle místa"</string>
     <string name="group_by_time" msgid="9046168567717963573">"Podle času"</string>
     <string name="group_by_tags" msgid="3568731317210676160">"Podle tagů"</string>
-    <string name="group_by_faces" msgid="1566351636227274906">"Podle osob"</string>
+    <string name="group_by_faces" msgid="1566351636227274906">"Podle lidí"</string>
     <string name="group_by_album" msgid="1532818636053818958">"Podle alba"</string>
     <string name="group_by_size" msgid="153766174950394155">"Podle velikosti"</string>
     <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string>
     <string name="no_location" msgid="4043624857489331676">"Neznámá poloha"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Některá umístění se nepodařilo identifikovat kvůli problémům se sítí."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Stažení fotografií v tomto albu se nezdařilo. Zkuste to prosím později."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Stažení seznamu alb se nezdařilo. Zkuste to prosím později."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Fotografie v tomto albu nelze stáhnout. Zkuste to znovu později."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Pouze obrázky"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Pouze videa"</string>
-    <string name="show_all" msgid="4780647751652596980">"Obrázky a videa"</string>
+    <string name="show_all" msgid="6963292714584735149">"Obrázky a videa"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galerie fotografií"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Žádné fotky."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Ořezaný snímek uložen do složky stažených souborů."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Ořezaný snímek není uložen."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Ořezaný snímek byl uložen do složky <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Žádné album není k dispozici."</string>
     <string name="empty_album" msgid="4542880442593595494">"Žádné obrázky ani videa nejsou k dispozici."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Příspěvky"</string>
     <string name="make_available_offline" msgid="5157950985488297112">"Zpřístupnit offline"</string>
-    <string name="sync_picasa_albums" msgid="8522572542111169872">"Obnovit"</string>
+    <string name="sync_picasa_albums" msgid="8522572542111169872">"Aktualizovat"</string>
     <string name="done" msgid="217672440064436595">"Hotovo"</string>
     <string name="sequence_in_set" msgid="7235465319919457488">"%1$d z %2$d položek:"</string>
     <string name="title" msgid="7622928349908052569">"Název"</string>
@@ -121,9 +125,9 @@
     <string name="model" msgid="8240207064064337366">"Model"</string>
     <string name="flash" msgid="2816779031261147723">"Blesk"</string>
     <string name="aperture" msgid="5920657630303915195">"Clona"</string>
-    <string name="focal_length" msgid="1291383769749877010">"Ohnisk. vzdál."</string>
+    <string name="focal_length" msgid="1291383769749877010">"Ohnisk. vzdálenost"</string>
     <string name="white_balance" msgid="1582509289994216078">"Vyvážení bílé"</string>
-    <string name="exposure_time" msgid="3990163680281058826">"Doba expozice"</string>
+    <string name="exposure_time" msgid="3990163680281058826">"Expoziční čas"</string>
     <string name="iso" msgid="5028296664327335940">"ISO"</string>
     <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
     <string name="manual" msgid="6608905477477607865">"Ručně"</string>
@@ -131,7 +135,7 @@
     <string name="flash_on" msgid="7891556231891837284">"S bleskem"</string>
     <string name="flash_off" msgid="1445443413822680010">"Bez blesku"</string>
   <plurals name="make_albums_available_offline">
-    <item quantity="one" msgid="2171596356101611086">"Zpřístupnění alba offline."</item>
+    <item quantity="one" msgid="2171596356101611086">"Zpřístupňování alba offline"</item>
     <item quantity="other" msgid="4948604338155959389">"Zpřístupnění alb offline"</item>
   </plurals>
     <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Tato položka je uložena v místním úložišti a je k dispozici offline."</string>
@@ -159,7 +163,12 @@
     <string name="locations" msgid="6649297994083130305">"Lokality"</string>
     <string name="people" msgid="4114003823747292747">"Lidé"</string>
     <string name="tags" msgid="5539648765482935955">"Tagy"</string>
-    <string name="group_by" msgid="4308299657902209357">"Seskupit podle"</string>
+    <string name="group_by" msgid="4308299657902209357">"Seskupit podle..."</string>
     <string name="settings" msgid="1534847740615665736">"Nastavení"</string>
     <string name="add_account" msgid="4271217504968243974">"Přidat účet"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparát"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Stahování"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importováno"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Snímky obrazovky"</string>
+    <string name="help" msgid="7368960711153618354">"Nápověda"</string>
 </resources>
diff --git a/res/values-da/photoeditor_strings.xml b/res/values-da/photoeditor_strings.xml
index 447fabf..0d96d43 100644
--- a/res/values-da/photoeditor_strings.xml
+++ b/res/values-da/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Fotostudie"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Billedet kan ikke indlæses"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Billedet kan ikke gemmes"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Billedet gemmes i albummet <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Redigeret"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Vil du gemme det redigerede billede?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotoet kunne ikke indlæses"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Det redigerede foto kunne ikke gemmes"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Det redigerede foto er gemt i <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Kassér ændringer, som ikke er gemt?"</string>
     <string name="yes" msgid="5402582493291792293">"Ja"</string>
-    <string name="no" msgid="5595408018304861875">"Nej"</string>
     <string name="save" msgid="5516670392524294967">"GEM"</string>
     <string name="autofix" msgid="1663414996270538748">"Autojustering"</string>
     <string name="crop" msgid="7598378507763334041">"Beskær"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Træk markører for at beskære billedet"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Tegn på billedet for at lave kruseduller"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Træk billedet for at vende det"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Tryk for at fjerne røde øjne"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Tryk på røde øjne for at fjerne dem"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Træk billedet for at rotere det"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Træk i billedet for at rette det op"</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 0c68b03..2fcb2fc 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -29,11 +29,13 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Genoptag afspilning"</string>
     <string name="loading" msgid="7038208555304563571">"Indlæser..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Kunne ikke indlæses"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Billedet kunne ikke indlæses"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniature"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Start igen"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Beskær"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Tryk på et ansigt for at begynde."</string>
-    <string name="saving_image" msgid="7270334453636349407">"Gemmer billede ..."</string>
+    <string name="saving_image" msgid="7270334453636349407">"Gemmer billede..."</string>
     <string name="save_error" msgid="6857408774183654970">"Det beskårne billede kunne ikke gemmes."</string>
     <string name="crop_label" msgid="521114301871349328">"Beskær billede"</string>
     <string name="select_image" msgid="7841406150484742140">"Vælg foto"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Vælg album"</string>
     <string name="select_group" msgid="6744208543323307114">"Vælg gruppe"</string>
     <string name="set_image" msgid="2331476809308010401">"Angiv billedet som"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Angiv baggrund"</string>
     <string name="wallpaper" msgid="140165383777262070">"Angiver baggrund..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapet"</string>
     <string name="delete" msgid="2839695998251824487">"Slet"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Slet"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vil du slette det valgte element?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vil du slette valgte elementer?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bekræft"</string>
     <string name="cancel" msgid="3637516880917356226">"Annuller"</string>
     <string name="share" msgid="3619042788254195341">"Del"</string>
     <string name="select_all" msgid="3403283025220282175">"Markér alle"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Roter til højre"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Elementet blev ikke fundet."</string>
     <string name="edit" msgid="1502273844748580847">"Rediger"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Der er ingen apps, der kan gennemføre handlingen."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Håndterer anmodninger om cachelagring"</string>
     <string name="caching_label" msgid="4521059045896269095">"Cachelagrer..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Beskær"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Utagget"</string>
     <string name="no_location" msgid="4043624857489331676">"Ingen placering"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Nogle placeringer kunne ikke identificeres på grund af netværksproblemer."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Billederne i dette album kunne ikke downloades. Prøv igen senere."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Listen over albummer kunne ikke downloades. Prøv igen senere."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Billederne i dette album kunne ikke hentes. Prøv igen senere."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Kun billeder"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Kun videoer"</string>
-    <string name="show_all" msgid="4780647751652596980">"Billeder og videoer"</string>
+    <string name="show_all" msgid="6963292714584735149">"Billeder og videoer"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Billedgalleri"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Ingen billeder."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Beskåret billede gemmes i Downloads."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Det beskårne billede blev ikke gemt."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Det beskårne billede er gemt i <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Ingen tilgængelige albummer."</string>
     <string name="empty_album" msgid="4542880442593595494">"O tilgængelige billeder/videoer."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Indlæg"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupper efter"</string>
     <string name="settings" msgid="1534847740615665736">"Indstillinger"</string>
     <string name="add_account" msgid="4271217504968243974">"Tilføj konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Download"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importeret"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skærmbillede"</string>
+    <string name="help" msgid="7368960711153618354">"Hjælp"</string>
 </resources>
diff --git a/res/values-de/photoeditor_strings.xml b/res/values-de/photoeditor_strings.xml
index b57502c..a00fa53 100644
--- a/res/values-de/photoeditor_strings.xml
+++ b/res/values-de/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Foto kann nicht geladen werden."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Foto kann nicht gespeichert werden."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto in Album <xliff:g id="ALBUM_NAME">%s</xliff:g> gespeichert"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Bearbeitet"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Bearbeitetes Foto speichern?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Foto konnte nicht geladen werden."</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Speichern des bearbeiteten Fotos nicht möglich"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Das bearbeitete Foto wurde in <xliff:g id="FOLDER_NAME">%s</xliff:g> gespeichert."</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Nicht gespeicherte Änderungen verwerfen?"</string>
     <string name="yes" msgid="5402582493291792293">"Ja"</string>
-    <string name="no" msgid="5595408018304861875">"Nein"</string>
     <string name="save" msgid="5516670392524294967">"Speichern"</string>
     <string name="autofix" msgid="1663414996270538748">"Auto-Fix"</string>
     <string name="crop" msgid="7598378507763334041">"Zuschneiden"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Zum Zuschneiden des Fotos Markierungen ziehen"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Zum Malen auf dem Foto malen"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Foto ziehen, um es umzudrehen"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Zum Entfernen roter Augen tippen"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Zur Korrektur auf rote Augen tippen"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Foto ziehen, um es zu drehen"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Foto ziehen, um es zu strecken"</string>
 </resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 3cfe33b..cce0dd7 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Mit Wiedergabe fortfahren"</string>
     <string name="loading" msgid="7038208555304563571">"Wird geladen..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Konnte nicht geladen werden."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Bild konnte nicht geladen werden."</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Keine Miniaturansicht"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Starten"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Zuschneiden"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Zum Beginnen auf ein Gesicht tippen"</string>
     <string name="saving_image" msgid="7270334453636349407">"Bild wird gespeichert..."</string>
     <string name="save_error" msgid="6857408774183654970">"Zugeschnittenes Bild nicht gespeichert"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Album auswählen"</string>
     <string name="select_group" msgid="6744208543323307114">"Gruppe auswählen"</string>
     <string name="set_image" msgid="2331476809308010401">"Bild festlegen als"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Hintergrund wählen"</string>
     <string name="wallpaper" msgid="140165383777262070">"Hintergrund wird festgelegt..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Hintergrund"</string>
     <string name="delete" msgid="2839695998251824487">"Löschen"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Löschen"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ausgewähltes Element löschen?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Ausgewählte Elemente löschen?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bestätigen"</string>
     <string name="cancel" msgid="3637516880917356226">"Abbrechen"</string>
     <string name="share" msgid="3619042788254195341">"Teilen"</string>
     <string name="select_all" msgid="3403283025220282175">"Alle auswählen"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Nach rechts drehen"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Bild wurde nicht gefunden."</string>
     <string name="edit" msgid="1502273844748580847">"Bearbeiten"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Für diese Aktion ist keine App verfügbar."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Caching-Anfragen werden verarbeitet."</string>
     <string name="caching_label" msgid="4521059045896269095">"Caching läuft..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Zuschneiden"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Nicht getaggt"</string>
     <string name="no_location" msgid="4043624857489331676">"Kein Aufnahmeort"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Einige Standorte konnten aufgrund von Netzwerkproblemen nicht identifiziert werden."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Die Fotos aus diesem Album konnten nicht heruntergeladen werden. Bitte versuchen Sie es später erneut."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Die Albenliste konnte nicht heruntergeladen werden. Bitte versuchen Sie es später erneut."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Die Fotos aus diesem Album konnten nicht heruntergeladen werden. Bitte versuchen Sie es später erneut."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Nur Bilder"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Nur Videos"</string>
-    <string name="show_all" msgid="4780647751652596980">"Bilder und Videos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Bilder und Videos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerie"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Keine Fotos vorhanden"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Zugeschnittenes Bild unter Downloads gespeichert"</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Zugeschnittenes Bild wurde nicht gespeichert."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Zugeschnittenes Bild gespeichert in <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Keine Alben verfügbar"</string>
     <string name="empty_album" msgid="4542880442593595494">"Keine Bilder oder Videos vorhanden"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Posts"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Gruppieren nach"</string>
     <string name="settings" msgid="1534847740615665736">"Einstellungen"</string>
     <string name="add_account" msgid="4271217504968243974">"Konto hinzufügen"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Downloads"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importiert"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshots"</string>
+    <string name="help" msgid="7368960711153618354">"Hilfe"</string>
 </resources>
diff --git a/res/values-el/photoeditor_strings.xml b/res/values-el/photoeditor_strings.xml
index 88ece26..8ec8c5a 100644
--- a/res/values-el/photoeditor_strings.xml
+++ b/res/values-el/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Αδύν.φόρτ.φωτογραφ."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Αδύν.αποθ.φωτογρ."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Η φωτ. αποθηκεύτηκε στο λεύκωμα <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Επεξεργασμ."</string>
-    <string name="save_photo" msgid="3125109368779997862">"Αποθήκευση επεξεργασμένης φωτογραφίας;"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Αδύνατη η φόρτωση της φωτογραφίας"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Αδύνατη  η αποθήκευση της επεξ. φωτογρ."</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Η επεξ. φωτογραφία αποθηκ. στον φάκελο <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Απόρριψη μη αποθηκευμένων αλλαγών;"</string>
     <string name="yes" msgid="5402582493291792293">"Ναι"</string>
-    <string name="no" msgid="5595408018304861875">"Όχι"</string>
     <string name="save" msgid="5516670392524294967">"ΑΠΟΘΗΚΕΥΣΗ"</string>
     <string name="autofix" msgid="1663414996270538748">"Αυτ.επιδ."</string>
     <string name="crop" msgid="7598378507763334041">"Περικοπή"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Σύρετε τους δείκτες για την περικοπή της φωτογραφίας"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Σχεδιάστε στη φωτογραφία για να δημιουργήσετε ένα σκετσάκι"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Σύρετε τη φωτογραφία για να την αναστρέψετε"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Πατήστε για να αφαιρέσετε τα κόκκινα μάτια"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Πατήστε στα κόκκινα μάτια για διόρθωση"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Σύρετε τη φωτογραφία για να την περιστρέψετε"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Σύρετε τη φωτογραφία για να την ευθυγραμμίσετε"</string>
 </resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 4d1e2e0..7435abe 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Συνέχιση αναπαραγωγής"</string>
     <string name="loading" msgid="7038208555304563571">"Φόρτωση..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Δεν ήταν δυνατή η φόρτωση"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Δεν ήταν δυνατή η φόρτωση της εικόνας"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Δεν υπάρχει μικρογραφία"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Έναρξη από την αρχή"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Περικοπή"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"ΟΚ"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Αγγίξτε κάποιο πρόσωπο για να ξεκινήσετε."</string>
     <string name="saving_image" msgid="7270334453636349407">"Αποθήκευση εικόνας..."</string>
     <string name="save_error" msgid="6857408774183654970">"Αδυναμία αποθήκευσης αποκομμένης εικόνας"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Επιλογή λευκώματος"</string>
     <string name="select_group" msgid="6744208543323307114">"Επιλογή ομάδας"</string>
     <string name="set_image" msgid="2331476809308010401">"Ορισμός εικόνας ως"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Ορισμός ταπετσαρίας"</string>
     <string name="wallpaper" msgid="140165383777262070">"Ορισμός ταπετσαρίας..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Ταπετσαρία"</string>
     <string name="delete" msgid="2839695998251824487">"Διαγραφή"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Διαγραφή"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Διαγραφή επιλεγμέν. στοιχείου;"</item>
+    <item quantity="other" msgid="5874316486520635333">"Διαγραφή επιλεγμέν. στοιχείων;"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Επιβεβαίωση"</string>
     <string name="cancel" msgid="3637516880917356226">"Ακύρωση"</string>
     <string name="share" msgid="3619042788254195341">"Κοινή χρήση"</string>
     <string name="select_all" msgid="3403283025220282175">"Επιλογή όλων"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Δεξιά περιστροφή"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Δεν ήταν δυνατή η εύρεση."</string>
     <string name="edit" msgid="1502273844748580847">"Επεξεργασία"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Δεν υπάρχουν διαθέσιμες εφαρμογές για την ολοκλήρωση της ενέργειας."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Επεξεργασία αιτημάτων προσωρινής αποθήκευσης"</string>
     <string name="caching_label" msgid="4521059045896269095">"Προσωρ. αποθ..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Περικοπή"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Χωρίς ετικέτα"</string>
     <string name="no_location" msgid="4043624857489331676">"Καμία τοποθεσία"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Δεν ήταν δυνατός ο προσδιορισμός ορισμένων τοποθεσιών λόγω προβλημάτων δικτύου."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Αποτυχία λήψης των φωτογραφιών σε αυτό το λεύκωμα. Δοκιμάστε ξανά αργότερα."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Αποτυχία λήψης της λίστας λευκωμάτων. Δοκιμάστε ξανά αργότερα."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Δεν ήταν δυνατή η λήψη των φωτογραφιών σε αυτό το λεύκωμα. Δοκιμάστε ξανά αργότερα."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Μόνο εικόνες"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Μόνο βίντεο"</string>
-    <string name="show_all" msgid="4780647751652596980">"Εικόνες και βίντεο"</string>
+    <string name="show_all" msgid="6963292714584735149">"Εικόνες και βίντεο"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Συλλογή φωτογραφιών"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Δεν υπάρχουν φωτογραφίες."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Η αποκομμένη εικόνα αποθηκεύτηκε στις \"Λήψεις\"."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Δεν αποθηκεύτηκε η αποκομένη εικόνα."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Η αποκομμένη εικόνα αποθηκεύτηκε στον φάκελο <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Δεν υπάρχουν διαθέσιμα λευκώματα."</string>
     <string name="empty_album" msgid="4542880442593595494">"Δεν υπάρχουν διαθέσιμες εικόνες/βίντεο."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Αναρτήσεις"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Ομαδοποίηση κατά"</string>
     <string name="settings" msgid="1534847740615665736">"Ρυθμίσεις"</string>
     <string name="add_account" msgid="4271217504968243974">"Προσθήκη λογαριασμού"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Φωτογραφική μηχανή"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Λήψη"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Έγινε εισαγωγή"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Στιγμιότυπο οθόνης"</string>
+    <string name="help" msgid="7368960711153618354">"Βοήθεια"</string>
 </resources>
diff --git a/res/values-en-rGB/photoeditor_strings.xml b/res/values-en-rGB/photoeditor_strings.xml
index 04660ae..6dde2f8 100644
--- a/res/values-en-rGB/photoeditor_strings.xml
+++ b/res/values-en-rGB/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Photo cannot be loaded."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Photo cannot be saved"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Photo saved to <xliff:g id="ALBUM_NAME">%s</xliff:g> album"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Edited"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Save edited photo?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Couldn\'t load the photo"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Couldn\'t save edited photo"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Edited photo saved to <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Discard unsaved changes?"</string>
     <string name="yes" msgid="5402582493291792293">"Yes"</string>
-    <string name="no" msgid="5595408018304861875">"No"</string>
     <string name="save" msgid="5516670392524294967">"SAVE"</string>
     <string name="autofix" msgid="1663414996270538748">"Auto-fix"</string>
     <string name="crop" msgid="7598378507763334041">"Crop"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Drag markers to crop photo"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Draw on photo to doodle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Drag photo to flip it"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Tap to remove red eyes"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Tap on red eyes to remove them"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Drag photo to rotate it"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Drag photo to straighten it"</string>
 </resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index ca72583..c7bbcea 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Resume playing"</string>
     <string name="loading" msgid="7038208555304563571">"Loading…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Couldn\'t load"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Couldn\'t load the image"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"No thumbnail"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Start again"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Crop"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Touch a face to begin."</string>
     <string name="saving_image" msgid="7270334453636349407">"Saving picture…"</string>
     <string name="save_error" msgid="6857408774183654970">"Couldn\'t save cropped image."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Select album"</string>
     <string name="select_group" msgid="6744208543323307114">"Select group"</string>
     <string name="set_image" msgid="2331476809308010401">"Set picture as"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Set wallpaper"</string>
     <string name="wallpaper" msgid="140165383777262070">"Setting wallpaper..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string>
     <string name="delete" msgid="2839695998251824487">"Delete"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Delete"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Delete selected item?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Delete selected items?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirm"</string>
     <string name="cancel" msgid="3637516880917356226">"Cancel"</string>
     <string name="share" msgid="3619042788254195341">"Share"</string>
     <string name="select_all" msgid="3403283025220282175">"Select all"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Rotate right"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Couldn\'t find item."</string>
     <string name="edit" msgid="1502273844748580847">"Edit"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"No app is available to complete the action."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Process Caching Requests"</string>
     <string name="caching_label" msgid="4521059045896269095">"Caching..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Crop"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Untagged"</string>
     <string name="no_location" msgid="4043624857489331676">"No location"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Some locations couldn\'t be identified due to network problems."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Failed to download the photos in this album. Please retry later."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Failed to download the list of albums. Please retry later."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Couldn\'t download the photos in this album. Retry later."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Images only"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Videos only"</string>
-    <string name="show_all" msgid="4780647751652596980">"Images and videos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Images &amp; videos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Photo Gallery"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"No photos."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Cropped image saved to Downloads."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Cropped image wasn\'t saved."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Cropped image saved to <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"No albums available."</string>
     <string name="empty_album" msgid="4542880442593595494">"O images/videos available."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Posts"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Group by"</string>
     <string name="settings" msgid="1534847740615665736">"Settings"</string>
     <string name="add_account" msgid="4271217504968243974">"Add account"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Camera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Download"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Imported"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshot"</string>
+    <string name="help" msgid="7368960711153618354">"Help"</string>
 </resources>
diff --git a/res/values-es-rUS/photoeditor_strings.xml b/res/values-es-rUS/photoeditor_strings.xml
index d6bd2b8..f0a38ed 100644
--- a/res/values-es-rUS/photoeditor_strings.xml
+++ b/res/values-es-rUS/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Estudio de fotografía"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"No se puede cargar la foto."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"No se puede guardar la foto."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"La foto se guardó en el álbum <xliff:g id="ALBUM_NAME">%s</xliff:g>."</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Editadas"</string>
-    <string name="save_photo" msgid="3125109368779997862">"¿Guardar foto editada?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"No se puede cargar la foto"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"No se pudo guardar la foto editada"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"La foto editada se guardó en <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"¿Descartar cambios sin guardar?"</string>
     <string name="yes" msgid="5402582493291792293">"Sí"</string>
-    <string name="no" msgid="5595408018304861875">"No"</string>
     <string name="save" msgid="5516670392524294967">"GUARDAR"</string>
     <string name="autofix" msgid="1663414996270538748">"Autocorrección"</string>
     <string name="crop" msgid="7598378507763334041">"Recortar"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Arrastrar los marcadores para recortar la foto"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Dibuja en la foto para hacer garabatos"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Arrastra la foto para darla vuelta"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Toca para eliminar los ojos rojos"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Elimina los ojos rojos tocando el icono"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Arrastra la foto para hacerla girar"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Arrastra la foto para enderezarla"</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 200e1a6..cf800b7 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar la reproducción"</string>
     <string name="loading" msgid="7038208555304563571">"Cargando…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"No se pudo cargar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"No se pudo cargar la imagen."</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Sin miniatura"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Empezar de nuevo"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Cortar"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Aceptar"</string>
+    <string name="ok" msgid="5296833083983263293">"Aceptar"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Toca una cara para comenzar."</string>
     <string name="saving_image" msgid="7270334453636349407">"Guardando imagen..."</string>
     <string name="save_error" msgid="6857408774183654970">"No se pudo guardar la imagen recortada."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Seleccionar álbum"</string>
     <string name="select_group" msgid="6744208543323307114">"Seleccionar un grupo"</string>
     <string name="set_image" msgid="2331476809308010401">"Establecer imagen como"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Establecer como fondo de pantalla"</string>
     <string name="wallpaper" msgid="140165383777262070">"Estableciendo fondo de pantalla..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Papel tapiz"</string>
     <string name="delete" msgid="2839695998251824487">"Eliminar"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Borrar"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"¿Eliminar elemento?"</item>
+    <item quantity="other" msgid="5874316486520635333">"¿Eliminar elementos?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Compartir"</string>
     <string name="select_all" msgid="3403283025220282175">"Seleccionar todo"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Rotar hacia la derecha"</string>
     <string name="no_such_item" msgid="5315144556325243400">"No se pudo encontrar el elemento."</string>
     <string name="edit" msgid="1502273844748580847">"Editar"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"No hay aplicaciones para completar la acción."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Procesando solicitudes de almacenamiento en caché"</string>
     <string name="caching_label" msgid="4521059045896269095">"Alm en caché..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Recortar"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"No etiquetado"</string>
     <string name="no_location" msgid="4043624857489331676">"No hay ubicación"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"No se pudieron identificar algunas ubicaciones debido a problemas de la red."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Se produjo un error al descargar las fotos en este álbum. Vuelve a intentarlo más tarde."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Se produjo un error al descargar la lista de álbumes. Vuelve a intentarlo más tarde."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"No se pudieron descargar las fotos de este álbum. Vuelve a intentarlo más adelante."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Sólo imágenes"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Sólo videos"</string>
-    <string name="show_all" msgid="4780647751652596980">"Imágenes y videos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imágenes y videos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galería de fotos"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"No hay fotos."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"La imagen recortada se guardó en Descargas."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"No se ha guardado la imagen recortada."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"La imagen recortada se guardó en <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"No hay álbumes disponibles."</string>
     <string name="empty_album" msgid="4542880442593595494">"No hay imágenes/videos disponibles."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Publicaciones"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
     <string name="settings" msgid="1534847740615665736">"Configuración"</string>
     <string name="add_account" msgid="4271217504968243974">"Agregar cuenta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Cámara"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Descargas"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importadas"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Capturas de pantalla"</string>
+    <string name="help" msgid="7368960711153618354">"Ayuda"</string>
 </resources>
diff --git a/res/values-es/photoeditor_strings.xml b/res/values-es/photoeditor_strings.xml
index e86d69f..4b4f7eb 100644
--- a/res/values-es/photoeditor_strings.xml
+++ b/res/values-es/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"No se puede cargar la foto."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"No se puede guardar la foto."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto guardada en el álbum <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Editadas"</string>
-    <string name="save_photo" msgid="3125109368779997862">"¿Quieres guardar la foto editada?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"No se puede cargar la foto."</string>
+    <string name="saving_failure" msgid="8229491575433743974">"No se ha podido guardar la foto editada."</string>
+    <string name="photo_saved" msgid="6163006724627682202">"La foto editada se ha guardado en <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"¿Descartar los cambios no guardados?"</string>
     <string name="yes" msgid="5402582493291792293">"Sí"</string>
-    <string name="no" msgid="5595408018304861875">"No"</string>
     <string name="save" msgid="5516670392524294967">"GUARDAR"</string>
     <string name="autofix" msgid="1663414996270538748">"Ajuste automático"</string>
     <string name="crop" msgid="7598378507763334041">"Recortar"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Arrastra los marcadores para recortar la foto"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Dibuja en la foto para hacer garabatos"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Arrastra la foto para darle la vuelta"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Toca para eliminar los ojos rojos"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Elimina los ojos rojos tocando el icono"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Arrastra la foto para girarla"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Arrastra la foto para enderezarla"</string>
 </resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index b0b2178..eb473ab 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Reanudar reproducción"</string>
     <string name="loading" msgid="7038208555304563571">"Cargando..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Error al cargar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"No se puede cargar la imagen."</string>
     <string name="no_thumbnail" msgid="284723185546429750">"No hay miniaturas."</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Volver a reproducir"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Recortar"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Aceptar"</string>
+    <string name="ok" msgid="5296833083983263293">"Aceptar"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Toca una cara para comenzar."</string>
     <string name="saving_image" msgid="7270334453636349407">"Guardando imagen..."</string>
     <string name="save_error" msgid="6857408774183654970">"Error al guardar la imagen recortada"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Seleccionar álbum"</string>
     <string name="select_group" msgid="6744208543323307114">"Seleccionar grupo"</string>
     <string name="set_image" msgid="2331476809308010401">"Establecer imagen como"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Establecer fondo de pantalla"</string>
     <string name="wallpaper" msgid="140165383777262070">"Estableciendo fondo de pantalla..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fondo de pantalla"</string>
     <string name="delete" msgid="2839695998251824487">"Borrar"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Eliminar"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"¿Eliminar elemento seleccionado?"</item>
+    <item quantity="other" msgid="5874316486520635333">"¿Eliminar elementos seleccionados?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Compartir"</string>
     <string name="select_all" msgid="3403283025220282175">"Seleccionar todo"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Girar a la derecha"</string>
     <string name="no_such_item" msgid="5315144556325243400">"No se ha podido encontrar el elemento."</string>
     <string name="edit" msgid="1502273844748580847">"Editar"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"No hay aplicaciones para completar la acción."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Procesando solicitudes de almacenamiento en caché"</string>
     <string name="caching_label" msgid="4521059045896269095">"Almacenando en caché..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Recortar"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Sin etiquetas"</string>
     <string name="no_location" msgid="4043624857489331676">"Sin ubicación"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"No se ha podido identificar algunas ubicaciones debido a errores en la red."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Error al descargar las fotos del álbum. Inténtalo de nuevo más tarde."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Error al descargar la lista de álbumes. Inténtalo de nuevo más tarde."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"No se ha podido descargar las fotos del álbum. Inténtalo de nuevo más tarde."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Solo imágenes"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Solo vídeos"</string>
-    <string name="show_all" msgid="4780647751652596980">"Imágenes y vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imágenes y vídeos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galería de fotos"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"No hay fotos."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"La imagen recortada se ha guardado en Descargas."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"No se ha guardado la imagen recortada."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"La imagen recortada se ha guardado en <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"No hay álbumes disponibles."</string>
     <string name="empty_album" msgid="4542880442593595494">"No hay imágenes ni vídeos disponibles."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Publicaciones"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
     <string name="settings" msgid="1534847740615665736">"Ajustes"</string>
     <string name="add_account" msgid="4271217504968243974">"Añadir cuenta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Cámara"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Descargadas"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importadas"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Capturas de pantalla"</string>
+    <string name="help" msgid="7368960711153618354">"Ayuda"</string>
 </resources>
diff --git a/res/values-et/photoeditor_strings.xml b/res/values-et/photoeditor_strings.xml
index eea16a2..c924486 100644
--- a/res/values-et/photoeditor_strings.xml
+++ b/res/values-et/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Fotostuudio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotot ei saa laadida"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotot ei saa salvestada"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto salvestati albumisse <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Muudetud"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Kas salvestada muudetud foto?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotot ei õnnestunud laadida"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Muudetud fotot ei õnnestunud salvestada"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Muudetud foto salvestati kausta <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Kas loobute salvestamata muudatustest?"</string>
     <string name="yes" msgid="5402582493291792293">"Jah"</string>
-    <string name="no" msgid="5595408018304861875">"Ei"</string>
     <string name="save" msgid="5516670392524294967">"SALVESTA"</string>
     <string name="autofix" msgid="1663414996270538748">"Automaatparandus"</string>
     <string name="crop" msgid="7598378507763334041">"Kärpimine"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Foto kärpimiseks lohistage markereid"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Kritseldamiseks joonistage fotol"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Ümberpööramiseks lohistage fotot"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Puudutage punasilmsuse eemaldamiseks"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Punasilmsuse eemaldam. puudutage silma"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Pööramiseks lohistage fotot"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Rihtimiseks lohistage fotot"</string>
 </resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 1072aa7..6ae9bba 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Jätka esitust"</string>
     <string name="loading" msgid="7038208555304563571">"Laadimine ..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Laadimine ebaõnnestus"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kujutist ei õnnestunud laadida"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Pisipilti pole"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Alusta uuesti"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Kärbi"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Alustamiseks näo puudutamine."</string>
     <string name="saving_image" msgid="7270334453636349407">"Pildi salvestamine ..."</string>
     <string name="save_error" msgid="6857408774183654970">"Kärbitud kujutist ei saanud salvestada."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Albumi valimine"</string>
     <string name="select_group" msgid="6744208543323307114">"Rühma valimine"</string>
     <string name="set_image" msgid="2331476809308010401">"Seadke pilt kui"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Taustapildiks määr."</string>
     <string name="wallpaper" msgid="140165383777262070">"Taustapildi määramine ..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Taustapilt"</string>
     <string name="delete" msgid="2839695998251824487">"Kustuta"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Kustuta"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Kas kustutada valitud üksus?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Kas kustutada valitud üksused?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Kinnitamine"</string>
     <string name="cancel" msgid="3637516880917356226">"Tühista"</string>
     <string name="share" msgid="3619042788254195341">"Jaga"</string>
     <string name="select_all" msgid="3403283025220282175">"Vali kõik"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Pööra paremale"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Üksust ei leitud."</string>
     <string name="edit" msgid="1502273844748580847">"Muuda"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Toimingu lõpetamiseks pole sobivat rakendust saadaval."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Vahemällu lisamise taotluste töötlemine"</string>
     <string name="caching_label" msgid="4521059045896269095">"Vahemällu ..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Kärbi"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Märgendita"</string>
     <string name="no_location" msgid="4043624857489331676">"Asukoht puudub"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Mõnda asukohta ei suudetud võrguprobleemide tõttu tuvastada."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Selle albumi fotode allalaadimine ebaõnnestus. Proovige hiljem uuesti."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Albumite loetelu allalaadimine ebaõnnestus. Proovige hiljem uuesti."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Fotosid ei saa sellesse albumisse alla laadida. Proovige hiljem uuesti."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Ainult kujutised"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Ainult videod"</string>
-    <string name="show_all" msgid="4780647751652596980">"Kujutised ja videod"</string>
+    <string name="show_all" msgid="6963292714584735149">"Kujutised ja videod"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerii"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Fotod puuduvad."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Kärbitud kujutis salvestati allalaadimiste kausta."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Kärbitud kujutist ei salvestatud."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Kärbitud kujutis salvestati kausta <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Ühtegi albumit pole saadaval."</string>
     <string name="empty_album" msgid="4542880442593595494">"O kujutist/videot on saadaval."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Postitused"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupeerimisalus:"</string>
     <string name="settings" msgid="1534847740615665736">"Seaded"</string>
     <string name="add_account" msgid="4271217504968243974">"Lisa konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kaamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Alla laaditud"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Imporditud"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekraanipilt"</string>
+    <string name="help" msgid="7368960711153618354">"Abi"</string>
 </resources>
diff --git a/res/values-fa/photoeditor_strings.xml b/res/values-fa/photoeditor_strings.xml
index 16b1546..177c646 100644
--- a/res/values-fa/photoeditor_strings.xml
+++ b/res/values-fa/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"عکس بارگیری نمی‌شود"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"عکس ذخیره نمی‌شود"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"عکس در آلبوم <xliff:g id="ALBUM_NAME">%s</xliff:g> ذخیره شد"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"ویرایش شده"</string>
-    <string name="save_photo" msgid="3125109368779997862">"عکس ویرایش شده ذخیره شود؟"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"بارگیری عکس انجام نشد"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"عکس ویرایش شده ذخیره نشد"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"عکس ویرایش شده در <xliff:g id="FOLDER_NAME">%s</xliff:g> ذخیره شد"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"از تغییرات ذخیره نشده صرفنظر شود؟"</string>
     <string name="yes" msgid="5402582493291792293">"بله"</string>
-    <string name="no" msgid="5595408018304861875">"خیر"</string>
     <string name="save" msgid="5516670392524294967">"ذخیره"</string>
     <string name="autofix" msgid="1663414996270538748">"تصحیح خودکار"</string>
     <string name="crop" msgid="7598378507763334041">"برش"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"برای بریدن عکس نشانگرها را بکشید"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"برای doodle کردن روی عکس رسم کنید"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"برای وارونه کردن عکس آن را بکشید"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"برای حذف قرمزی چشم ضربه بزنید"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"روی قرمزی چشم برای رفع آن ضربه بزنید"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"برای چرخاندن عکس آن را بکشید"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"برای صاف کردن عکس آن را بکشید"</string>
 </resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 9017b40..c51d8c4 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"از سرگیری پخش"</string>
     <string name="loading" msgid="7038208555304563571">"در حال بارگیری…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"بارگیری نشد"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"تصویر بارگیری نمی‌شود"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"تصویر کوچکی وجود ندارد"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"شروع مجدد"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"برش"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"تائید"</string>
+    <string name="ok" msgid="5296833083983263293">"تأیید"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"برای شروع یک چهره را لمس کنید."</string>
     <string name="saving_image" msgid="7270334453636349407">"در حال ذخیره عکس..."</string>
     <string name="save_error" msgid="6857408774183654970">"ذخیره تصویر برش‌خورده امکان‌پذیر نیست."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"آلبوم را انتخاب کنید"</string>
     <string name="select_group" msgid="6744208543323307114">"انتخاب گروه"</string>
     <string name="set_image" msgid="2331476809308010401">"تنظیم تصویر بعنوان"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"تنظیم تصویر زمینه"</string>
     <string name="wallpaper" msgid="140165383777262070">"تنظیم تصویر زمینه..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"تصویر زمینه"</string>
     <string name="delete" msgid="2839695998251824487">"حذف"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"حذف"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"مورد انتخابی حذف شود؟"</item>
+    <item quantity="other" msgid="5874316486520635333">"موارد انتخابی حذف شوند؟"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"تأیید"</string>
     <string name="cancel" msgid="3637516880917356226">"لغو"</string>
     <string name="share" msgid="3619042788254195341">"اشتراک گذاری"</string>
     <string name="select_all" msgid="3403283025220282175">"انتخاب همه"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"چرخش به راست"</string>
     <string name="no_such_item" msgid="5315144556325243400">"مورد یافت نشد."</string>
     <string name="edit" msgid="1502273844748580847">"ویرایش"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"هیچ برنامه‌ای برای انجام این عملکرد موجود نیست."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"پردازش درخواست‌های ذخیره در حافظه پنهان"</string>
     <string name="caching_label" msgid="4521059045896269095">"در حال ذخیره در حافظه پنهان..."</string>
     <string name="crop_action" msgid="3427470284074377001">"برش"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"بدون برچسب گذاری"</string>
     <string name="no_location" msgid="4043624857489331676">"مکانی موجود نیست"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"شناسایی برخی از مکان‌ها به دلیل مشکلات شبکه امکان‌پذیر نیست."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"عکس‌ها در این آلبوم دانلود نشد. لطفاً بعداً دوباره امتحان کنید."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"لیست آلبوم‌ها دانلود نشد. لطفاً بعداً دوباره امتحان کنید."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"عکس‌های این آلبوم را نمی‌توان دانلود کرد. بعداً دوباره امتحان کنید."</string>
     <string name="show_images_only" msgid="7263218480867672653">"فقط تصاویر"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"فقط ویدیوها"</string>
-    <string name="show_all" msgid="4780647751652596980">"تصاویر و ویدیوها"</string>
+    <string name="show_all" msgid="6963292714584735149">"تصاویر و ویدیوها"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"گالری عکس"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"عکسی موجود نیست."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"تصویر بریده شده در \"دانلودها\" ذخیره شد."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"تصویر برش‌خورده ذخیره نشد."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"تصویر بریده شده در <xliff:g id="FOLDER_NAME">%s</xliff:g> ذخیره شد."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"آلبومی موجود نیست."</string>
     <string name="empty_album" msgid="4542880442593595494">"O تصویر/ویدیو موجود است."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"پست‌ها"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"گروه بندی براساس"</string>
     <string name="settings" msgid="1534847740615665736">"تنظیمات"</string>
     <string name="add_account" msgid="4271217504968243974">"افزودن حساب"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"دوربین"</string>
+    <string name="folder_download" msgid="7186215137642323932">"دانلود"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"وارد شده"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"عکس صفحه"</string>
+    <string name="help" msgid="7368960711153618354">"راهنما"</string>
 </resources>
diff --git a/res/values-fi/photoeditor_strings.xml b/res/values-fi/photoeditor_strings.xml
index 2bb6485..ce60dc7 100644
--- a/res/values-fi/photoeditor_strings.xml
+++ b/res/values-fi/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Valokuvastudio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Kuvaa ei voi ladata."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Kuvaa ei voi tallentaa."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Kuva tallennettu albumiin <xliff:g id="ALBUM_NAME">%s</xliff:g>."</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Muokattu"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Tallennetaanko muokattu valokuva?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Kuvaa ei voi ladata"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Muokatun kuvan tallentaminen epäonnistui"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Muokattu kuva tallennettu kansioon <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Hylätäänkö tallentamattomat muutokset?"</string>
     <string name="yes" msgid="5402582493291792293">"Kyllä"</string>
-    <string name="no" msgid="5595408018304861875">"Ei"</string>
     <string name="save" msgid="5516670392524294967">"Tallenna"</string>
     <string name="autofix" msgid="1663414996270538748">"Autom. korjaus"</string>
     <string name="crop" msgid="7598378507763334041">"Rajaa"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Leikkaa kuvaa vetämällä merkkejä"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Piirrä valokuvaan siirtämällä osoitin sen päälle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Käännä kuva vetämällä"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Poista punaiset silmät napauttamalla"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Poista punasilmäisyys napautt. silmiä"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Käännä valokuvaa vetämällä"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Suorista valokuva vetämällä"</string>
 </resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 2527a70..c1e7969 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Jatka toistoa"</string>
     <string name="loading" msgid="7038208555304563571">"Ladataan…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Ei voi ladata"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kuvaa ei voi ladata"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Ei pikkukuvaa"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Aloita alusta"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Rajaa"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Aloita koskettamalla kasvoja."</string>
     <string name="saving_image" msgid="7270334453636349407">"Tallennetaan kuvaa…"</string>
     <string name="save_error" msgid="6857408774183654970">"Rajattua kuvaa ei voitu tallentaa."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Valitse albumi"</string>
     <string name="select_group" msgid="6744208543323307114">"Valitse ryhmä"</string>
     <string name="set_image" msgid="2331476809308010401">"Aseta kuva"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Aseta taustakuva"</string>
     <string name="wallpaper" msgid="140165383777262070">"Asetetaan taustakuvaa..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"taustakuvaksi"</string>
     <string name="delete" msgid="2839695998251824487">"Poista"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Poista"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Poistetaanko valittu kohde?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Poistetaanko valitut kohteet?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Vahvista"</string>
     <string name="cancel" msgid="3637516880917356226">"Peruuta"</string>
     <string name="share" msgid="3619042788254195341">"Jaa"</string>
     <string name="select_all" msgid="3403283025220282175">"Valitse kaikki"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Kierrä myötäpäivään"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Kohdetta ei löytynyt."</string>
     <string name="edit" msgid="1502273844748580847">"Muokkaa"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Toiminnon suorittam. vaad. sov. ei ole saatavilla."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Käsitellään välimuistipyyntöjä"</string>
     <string name="caching_label" msgid="4521059045896269095">"Vie välimuist."</string>
     <string name="crop_action" msgid="3427470284074377001">"Rajaa"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Merkitsemättömät"</string>
     <string name="no_location" msgid="4043624857489331676">"Ei sijaintia"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Verkkoyhteysongelma – joitakin paikkoja ei voi tunnistaa."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Albumin valokuvien lataaminen epäonnistui. Yritä myöhemmin uudelleen."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Albumiluettelon lataaminen epäonnistui. Yritä myöhemmin uudelleen."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Albumin valokuvia ei voi ladata. Yritä myöhemmin uudelleen."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Vain kuvat"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Vain videot"</string>
-    <string name="show_all" msgid="4780647751652596980">"Kuvat ja videot"</string>
+    <string name="show_all" msgid="6963292714584735149">"Kuvat ja videot"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Kuvagalleria"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Ei valokuvia."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Rajattu kuva tallennetaan Lataukset-kansioon."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Rajattua kuvaa ei tallennettu."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Rajattu kuva tallennettu kansioon <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Ei albumeja käytettävissä."</string>
     <string name="empty_album" msgid="4542880442593595494">"0 kuvaa/videota saatavilla."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Julkaisut"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Ryhmittely:"</string>
     <string name="settings" msgid="1534847740615665736">"Asetukset"</string>
     <string name="add_account" msgid="4271217504968243974">"Lisää tili"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Lataus"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Tuonti"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Kuvakaappaus"</string>
+    <string name="help" msgid="7368960711153618354">"Ohje"</string>
 </resources>
diff --git a/res/values-fr/photoeditor_strings.xml b/res/values-fr/photoeditor_strings.xml
index a4d59ea..193e3f0 100644
--- a/res/values-fr/photoeditor_strings.xml
+++ b/res/values-fr/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Impossible de charger la photo."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Impossible d\'enregistrer la photo."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Photo enregistrée dans l\'album \"<xliff:g id="ALBUM_NAME">%s</xliff:g>\"."</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Retouchées"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Enregistrer photo retouchée ?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Impossible de charger la photo."</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Imposs. enregistrer photo retouchée."</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Photo retouchée enregistrée dans <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Annuler modifications non enregistrées ?"</string>
     <string name="yes" msgid="5402582493291792293">"Oui"</string>
-    <string name="no" msgid="5595408018304861875">"Non"</string>
     <string name="save" msgid="5516670392524294967">"ENREG."</string>
     <string name="autofix" msgid="1663414996270538748">"Correction auto"</string>
     <string name="crop" msgid="7598378507763334041">"Rogner"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Faire glisser les marqueurs pour rogner la photo"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Dessiner sur la photo pour gribouiller"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Faire glisser la photo pour la retourner"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Appuyer pour supprimer les yeux rouges"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Appuyer sur yeux rouges pour les effacer"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Faire glisser la photo pour la faire pivoter"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Faire glisser la photo pour la redresser"</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 4873b9f..5a5d4ca 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Reprendre la lecture"</string>
     <string name="loading" msgid="7038208555304563571">"Chargement en cours…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Impossible de charger"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Impossible de charger l\'image."</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Aucune vignette"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Démarrer"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Rogner"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Appuyer sur un visage pour commencer"</string>
     <string name="saving_image" msgid="7270334453636349407">"Enregistrement de l\'image"</string>
     <string name="save_error" msgid="6857408774183654970">"Impossible d\'enregistrer l\'image rognée."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Sélectionner album"</string>
     <string name="select_group" msgid="6744208543323307114">"Sélectionnez groupe"</string>
     <string name="set_image" msgid="2331476809308010401">"Utiliser l\'image comme"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Définir fond d\'écran"</string>
     <string name="wallpaper" msgid="140165383777262070">"Définition du fond d\'écran…"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fond d\'écran"</string>
     <string name="delete" msgid="2839695998251824487">"Supprimer"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Supprimer"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Supprimer élément sélectionné ?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Suppr. éléments sélectionnés ?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmer"</string>
     <string name="cancel" msgid="3637516880917356226">"Annuler"</string>
     <string name="share" msgid="3619042788254195341">"Partager"</string>
     <string name="select_all" msgid="3403283025220282175">"Tout sélectionner"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Faire pivoter à droite"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Impossible de trouver l\'élément."</string>
     <string name="edit" msgid="1502273844748580847">"Retoucher"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Aucune appli disponible pour effectuer l\'action."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Demandes de mise en cache en cours de traitement"</string>
     <string name="caching_label" msgid="4521059045896269095">"Mise en cache…"</string>
     <string name="crop_action" msgid="3427470284074377001">"Rogner"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Aucun tag"</string>
     <string name="no_location" msgid="4043624857489331676">"Aucun lieu"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Certains lieux n\'ont pas pu être identifiés en raison de problèmes réseau."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Impossible de télécharger les photos dans cet album. Veuillez réessayer ultérieurement."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Impossible de télécharger la liste des albums. Veuillez réessayer ultérieurement."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Impossible de télécharger les photos de cet album. Veuillez réessayer ultérieurement."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Images uniquement"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Vidéos uniquement"</string>
-    <string name="show_all" msgid="4780647751652596980">"Images et vidéos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Images et vidéos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galerie photos"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Aucune photo"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Image rognée enregistrée dans \"Téléchargements\"."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"L\'image rognée n\'a pas été enregistrée."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Image rognée enregistrée dans <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Aucun album disponible."</string>
     <string name="empty_album" msgid="4542880442593595494">"Aucune image/vidéo disponible."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Posts"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Regrouper par"</string>
     <string name="settings" msgid="1534847740615665736">"Paramètres"</string>
     <string name="add_account" msgid="4271217504968243974">"Ajouter un compte"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Appareil photo"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Téléchargements"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importations"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captures d\'écran"</string>
+    <string name="help" msgid="7368960711153618354">"Aide"</string>
 </resources>
diff --git a/res/values-hi/photoeditor_strings.xml b/res/values-hi/photoeditor_strings.xml
index 1f6ea06..f5286ec 100644
--- a/res/values-hi/photoeditor_strings.xml
+++ b/res/values-hi/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"फ़ोटो लोड नहीं हो सकता"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"फ़ोटो सहेजा नहीं जा सकता"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"फ़ोटो <xliff:g id="ALBUM_NAME">%s</xliff:g> एल्‍बम में सहेजा गया"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"संपादित"</string>
-    <string name="save_photo" msgid="3125109368779997862">"संपादित फ़ोटो सहेजें?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"फ़ोटो लोड नहीं किया जा सका"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"संपादित फ़ोटो को सहेजा नहीं जा सका"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"संपादित फ़ोटो को <xliff:g id="FOLDER_NAME">%s</xliff:g> में सहेजा गया"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"न सहेजे गए परिवर्तनों को छोड़ें?"</string>
     <string name="yes" msgid="5402582493291792293">"हां"</string>
-    <string name="no" msgid="5595408018304861875">"नहीं"</string>
     <string name="save" msgid="5516670392524294967">"सहेजें"</string>
     <string name="autofix" msgid="1663414996270538748">"स्‍वत: समायोजन"</string>
     <string name="crop" msgid="7598378507763334041">"काट-छांट करें"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"फ़ोटो की काट-छांट करने के लि‍ए मार्कर को खींचें"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"doodle के लिए फ़ोटो पर आरेखित करें"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"फ़ोटो को फ़्लिप करने के लिए उसे खींचें"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"रेड आई निकालने के लिए टैप करें"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"रेड आई निकालने के लिए उन पर टैप करें"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"घुमाने के लिए फ़ोटो को खींचें"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"सीधा करने के लिए फ़ोटो को खींचें"</string>
 </resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 7788858..0db281d 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"चलाना फिर से शुरू करें"</string>
     <string name="loading" msgid="7038208555304563571">"लोड हो रहा है..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"लोड नहीं कर सका"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"छवि लोड नहीं हो सकी"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"कोई थंबनेल नहीं"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"पुन: प्रारंभ करें"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"काट-छांट करें"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ठीक"</string>
+    <string name="ok" msgid="5296833083983263293">"ठीक"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"शुरू करने के लिए कोई चेहरा स्‍पर्श करें."</string>
     <string name="saving_image" msgid="7270334453636349407">"चित्र सहेज रहा है…"</string>
     <string name="save_error" msgid="6857408774183654970">"काट-छांट की गई छवि को नहीं सहेज सका."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"एल्बम का चयन करें"</string>
     <string name="select_group" msgid="6744208543323307114">"समूह का चयन करें"</string>
     <string name="set_image" msgid="2331476809308010401">"चित्र इस रूप में सेट करें"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"वॉलपेपर सेट करें"</string>
     <string name="wallpaper" msgid="140165383777262070">"वॉलपेपर सेट कर रहा है..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"वॉलपेपर"</string>
     <string name="delete" msgid="2839695998251824487">"हटाएं"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"हटाएं"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"चयनित आइटम हटाएं?"</item>
+    <item quantity="other" msgid="5874316486520635333">"चयनित आइटम हटाएं?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"पुष्टि करें"</string>
     <string name="cancel" msgid="3637516880917356226">"रद्द करें"</string>
     <string name="share" msgid="3619042788254195341">"शेयर करें"</string>
     <string name="select_all" msgid="3403283025220282175">"सभी का चयन करें"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"दाएं घुमाएं"</string>
     <string name="no_such_item" msgid="5315144556325243400">"आइटम नहीं ढूंढा जा सका."</string>
     <string name="edit" msgid="1502273844748580847">"संपादित करें"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"क्रिया पूर्ण करने हेतु कोई एप्‍लि. उपलब्‍ध नहीं."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"संचय अनुरोध संसाधित कर रहा है"</string>
     <string name="caching_label" msgid="4521059045896269095">"संचय कर रहा है..."</string>
     <string name="crop_action" msgid="3427470284074377001">"काट-छांट करें"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"टैग नहीं किया गया"</string>
     <string name="no_location" msgid="4043624857489331676">"कोई स्थान नहीं"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"नेटवर्क समस्‍याओं के कारण कुछ स्‍थानों को पहचाना नहीं जा सका."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"इस एल्बम में फ़ोटो डाउनलोड करना विफल हो गया है. कृपया बाद में पुनः प्रयास करें."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"एल्बम की सूची डाउनलोड करना विफल हो गया है. कृपया बाद में पुनः प्रयास करें."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"इस एल्बम के फ़ोटो डाउनलोड नहीं किए जा सके. बाद में पुन: प्रयास करें."</string>
     <string name="show_images_only" msgid="7263218480867672653">"केवल छवियां"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"केवल वीडियो"</string>
-    <string name="show_all" msgid="4780647751652596980">"छवियां और वीडियो"</string>
+    <string name="show_all" msgid="6963292714584735149">"छवियां और वीडियो"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"फ़ोटो गैलरी"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"कोई फ़ोटो नहीं."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"काट-छांट की गई छवि को डाउनलोड में सहेजा गया."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"काट-छांट की गई छवि को नहीं सहेजा गया."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"काट-छांट की गई छवि को <xliff:g id="FOLDER_NAME">%s</xliff:g> में सहेजा गया."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"कोई एल्‍बम उपलब्‍ध नहीं."</string>
     <string name="empty_album" msgid="4542880442593595494">"O छवियां/वीडियो उपलब्‍ध."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"पोस्ट"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"इसके द्वारा समूहीकृत"</string>
     <string name="settings" msgid="1534847740615665736">"सेटिंग"</string>
     <string name="add_account" msgid="4271217504968243974">"खाता जोड़ें"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"कैमरा"</string>
+    <string name="folder_download" msgid="7186215137642323932">"डाउनलोड करें"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"आयातित"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"स्क्रीनशॉट"</string>
+    <string name="help" msgid="7368960711153618354">"सहायता"</string>
 </resources>
diff --git a/res/values-hr/photoeditor_strings.xml b/res/values-hr/photoeditor_strings.xml
index 077c194..6ba85f3 100644
--- a/res/values-hr/photoeditor_strings.xml
+++ b/res/values-hr/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Fotostudio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotografiju nije moguće učitati"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotografiju nije moguće spremiti"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotografija spremljena u album <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Uređeno"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Spremiti uređenu fotografiju?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Nije moguće učitati fotografiju"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Nije moguće spremiti uređenu fotografiju"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Uređena fotografija spremljena u mapu <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Odbaciti nespremljene promjene?"</string>
     <string name="yes" msgid="5402582493291792293">"Da"</string>
-    <string name="no" msgid="5595408018304861875">"Ne"</string>
     <string name="save" msgid="5516670392524294967">"SPREMI"</string>
     <string name="autofix" msgid="1663414996270538748">"Aut. popravak"</string>
     <string name="crop" msgid="7598378507763334041">"Obreži"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Povucite oznake da biste obrezali sliku"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Crtanje na fotografiju za doodle logotip"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Povucite sliku da biste je preokrenuli"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Dotaknite za uklanjanje crvenih očiju"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Dotaknite oči da biste uklonili crvenilo"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Povucite fotografiju da biste je rotirali"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Povucite fotografiju da biste je izravnali"</string>
 </resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 6c57869..ccc3856 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Nastavak reprodukcije"</string>
     <string name="loading" msgid="7038208555304563571">"Učitavanje…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nije bilo moguće učitati"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nije moguće učitati sliku"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Nema minijatura"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Počni ispočetka"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Obreži"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"U redu"</string>
+    <string name="ok" msgid="5296833083983263293">"U redu"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Dodirnite lice za početak."</string>
     <string name="saving_image" msgid="7270334453636349407">"Spremanje slike..."</string>
     <string name="save_error" msgid="6857408774183654970">"Nije moguće spremiti obrezanu sliku."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Odaberi album"</string>
     <string name="select_group" msgid="6744208543323307114">"Odabir grupe"</string>
     <string name="set_image" msgid="2331476809308010401">"Postavi sliku kao"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Postavljanje pozadine"</string>
     <string name="wallpaper" msgid="140165383777262070">"Postavljanje pozadinske slike…"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Pozadinska slika"</string>
     <string name="delete" msgid="2839695998251824487">"Izbriši"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Izbriši"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Izbrisati odabranu stavku?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Izbrisati odabrane stavke?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potvrdi"</string>
     <string name="cancel" msgid="3637516880917356226">"Odustani"</string>
     <string name="share" msgid="3619042788254195341">"Podijeli"</string>
     <string name="select_all" msgid="3403283025220282175">"Odaberi sve"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Rotiraj udesno"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Nije moguće pronaći stavku."</string>
     <string name="edit" msgid="1502273844748580847">"Uredi"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Nema dostupnih aplikacija  za dovršavanje radnje."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Obrada zahtjeva za predmemoriju"</string>
     <string name="caching_label" msgid="4521059045896269095">"U predmemoriju…"</string>
     <string name="crop_action" msgid="3427470284074377001">"Obreži"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string>
     <string name="no_location" msgid="4043624857489331676">"Nema lokacije"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Neke lokacije nisu se mogle identificirati zbog poteškoća s mrežnom."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Preuzimanje fotografija iz ovog albuma nije uspjelo. Pokušajte opet kasnije."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Preuzimanje popisa albuma nije uspjelo. Pokušajte opet kasnije."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Preuzimanje fotografija iz ovog albuma nije uspjelo. Pokušajte ponovo kasnije."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Samo slike"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Samo videozapisi"</string>
-    <string name="show_all" msgid="4780647751652596980">"Slike i videozapisi"</string>
+    <string name="show_all" msgid="6963292714584735149">"Slike i videozapisi"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galerija fotografija"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Nema fotografija."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Obrezana slika spremljena u Preuzimanja."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Obrezana slika nije spremljena."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Obrezana slika spremljena u mapu <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nema dostupnih albuma."</string>
     <string name="empty_album" msgid="4542880442593595494">"Dostupno je 0 slika/videozapisa."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Postovi"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupiraj po"</string>
     <string name="settings" msgid="1534847740615665736">"Postavke"</string>
     <string name="add_account" msgid="4271217504968243974">"Dodaj račun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparat"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Preuzimanja"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Uvezeno"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Snimak zaslona"</string>
+    <string name="help" msgid="7368960711153618354">"Pomoć"</string>
 </resources>
diff --git a/res/values-hu/photoeditor_strings.xml b/res/values-hu/photoeditor_strings.xml
index 0b60f1c..4136478 100644
--- a/res/values-hu/photoeditor_strings.xml
+++ b/res/values-hu/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"A fotót nem lehet betölteni"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"A fotót nem lehet menteni"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotó mentve a(z) <xliff:g id="ALBUM_NAME">%s</xliff:g> albumba"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Szerkesztve"</string>
-    <string name="save_photo" msgid="3125109368779997862">"A szerkesztett kép mentése?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Nem sikerült betölteni a fotót"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"A szerkesztett fotót nem tudtuk menteni"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"A Szerkesztett fotót <xliff:g id="FOLDER_NAME">%s</xliff:g> mappába mentettük"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Elveti a nem mentett módosításokat?"</string>
     <string name="yes" msgid="5402582493291792293">"Igen"</string>
-    <string name="no" msgid="5595408018304861875">"Nem"</string>
     <string name="save" msgid="5516670392524294967">"MENTÉS"</string>
     <string name="autofix" msgid="1663414996270538748">"Autom. javítás"</string>
     <string name="crop" msgid="7598378507763334041">"Körbevágás"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Húzza a jelölőket a fotó vágásához"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Rajzoljon a fotóra embléma létrehozásához"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Húzza a fotót annak tükrözéséhez"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Érintse meg a vörös szem hatás eltávolításához"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Vörös szemek eltávolítása megérintéssel"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Húzza a fényképet annak forgatásához"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Húzza a fényképet annak kiegyenesítéséhez"</string>
 </resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index dd905ae..e0d360b 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Lejátszás folytatása"</string>
     <string name="loading" msgid="7038208555304563571">"Betöltés…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nem sikerült betölteni"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nem sikerült betölteni a képet"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Nincs indexkép"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Újrakezdés"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Körülvágás"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Érintsen meg egy arcot a kezdéshez."</string>
     <string name="saving_image" msgid="7270334453636349407">"Kép mentése..."</string>
     <string name="save_error" msgid="6857408774183654970">"Nem lehet menteni a vágott képet."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Album kiválasztása"</string>
     <string name="select_group" msgid="6744208543323307114">"Csoport kiválasztása"</string>
     <string name="set_image" msgid="2331476809308010401">"Kép beállítása, mint"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Háttérkép beállítása"</string>
     <string name="wallpaper" msgid="140165383777262070">"Háttérkép beállítása..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Háttérkép"</string>
     <string name="delete" msgid="2839695998251824487">"Törlés"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Törlés"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Törli a kiválasztott elemet?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Törli a kiválasztott elemeket?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Megerősítés"</string>
     <string name="cancel" msgid="3637516880917356226">"Mégse"</string>
     <string name="share" msgid="3619042788254195341">"Megosztás"</string>
     <string name="select_all" msgid="3403283025220282175">"Az összes kijelölése"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Forgatás jobbra"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Nem található az elem."</string>
     <string name="edit" msgid="1502273844748580847">"Szerkesztés"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Nincs elérhető alkalmazás a művelet befejezéséhez."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Tárolási kérelmek feldolgozása"</string>
     <string name="caching_label" msgid="4521059045896269095">"Gyorsítótárazás..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Körbevágás"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Címke nélküli"</string>
     <string name="no_location" msgid="4043624857489331676">"Nincs helyadat"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Néhány helyet nem lehetett azonosítani hálózati problémák miatt."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Nem sikerült letölteni az albumban található képeket. Kérjük, próbálja újra később."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Nem sikerült letölteni az albumok listáját. Kérjük, próbálja újra később."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Nem sikerült letölteni ennek az albumnak a képeit. Próbálja újra később."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Csak képek"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Csak videók"</string>
-    <string name="show_all" msgid="4780647751652596980">"Képek és videók"</string>
+    <string name="show_all" msgid="6963292714584735149">"Képek és videók"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotógaléria"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Nincsenek fényképek."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"A vágott kép a Letöltések közé került."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"A vágott kép nincs elmentve."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"A vágott képet a(z) <xliff:g id="FOLDER_NAME">%s</xliff:g> mappába mentettük."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nincs elérhető album."</string>
     <string name="empty_album" msgid="4542880442593595494">"Nincs elérhető kép/videó."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Bejegyzések"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Csoportosítás"</string>
     <string name="settings" msgid="1534847740615665736">"Beállítások"</string>
     <string name="add_account" msgid="4271217504968243974">"Fiók hozzáadása"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Letöltés"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importált"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Képernyőkép"</string>
+    <string name="help" msgid="7368960711153618354">"Súgó"</string>
 </resources>
diff --git a/res/values-in/photoeditor_strings.xml b/res/values-in/photoeditor_strings.xml
index f29c0ab..3aea629 100644
--- a/res/values-in/photoeditor_strings.xml
+++ b/res/values-in/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Foto tidak dapat dimuat"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Foto tidak dapat disimpan"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto disimpan ke album <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Hasil edit"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Simpan foto yang diedit?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Tidak dapat memuat foto"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Tidak dapat menyimpan foto yang diedit"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Foto yang diedit disimpan di <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Buang perubahan yang tidak tersimpan?"</string>
     <string name="yes" msgid="5402582493291792293">"Ya"</string>
-    <string name="no" msgid="5595408018304861875">"Tidak"</string>
     <string name="save" msgid="5516670392524294967">"SIMPAN"</string>
     <string name="autofix" msgid="1663414996270538748">"Prbaikan Otomts"</string>
     <string name="crop" msgid="7598378507763334041">"Pangkas"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Seret penanda untuk memangkas foto"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Menggambar pada foto untuk membuat orat-oret"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Seret foto untuk membaliknya"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Ketuk untuk menghapus mata merah"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Ketuk mata merah untuk menghapusnya"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Seret foto untuk memutarnya"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Seret foto untuk meluruskannya"</string>
 </resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 9f62b25..d79aa33 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Lanjutkan pemutaran"</string>
     <string name="loading" msgid="7038208555304563571">"Memuat…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Tidak dapat memuat"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Tidak dapat memuat gambar"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Tidak ada gambar mini"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Memulai"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Pangkas"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Oke"</string>
+    <string name="ok" msgid="5296833083983263293">"Oke"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Sentuh wajah untuk memulai."</string>
     <string name="saving_image" msgid="7270334453636349407">"Menyimpan gambar…"</string>
     <string name="save_error" msgid="6857408774183654970">"Tak dapat menyimpan gambar yg dipangkas."</string>
@@ -42,15 +44,20 @@
     <string name="select_album" msgid="1557063764849434077">"Pilih album"</string>
     <string name="select_group" msgid="6744208543323307114">"Pilih grup"</string>
     <string name="set_image" msgid="2331476809308010401">"Setel gambar sebagai"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Setel wallpaper"</string>
     <string name="wallpaper" msgid="140165383777262070">"Menyetel wallpaper..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string>
     <string name="delete" msgid="2839695998251824487">"Hapus"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Hapus"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Hapus item yang dipilih?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Hapus item yang dipilih?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Konfirmasi"</string>
     <string name="cancel" msgid="3637516880917356226">"Batal"</string>
     <string name="share" msgid="3619042788254195341">"Bagikan"</string>
     <string name="select_all" msgid="3403283025220282175">"Pilih semua"</string>
     <string name="deselect_all" msgid="5758897506061723684">"Batalkan semua pilihan"</string>
-    <string name="slideshow" msgid="4355906903247112975">"Rangkai salindia"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Rangkai slide"</string>
     <string name="details" msgid="8415120088556445230">"Detail"</string>
     <string name="details_title" msgid="2611396603977441273">"%1$d dari %2$d item:"</string>
     <string name="close" msgid="5585646033158453043">"Tutup"</string>
@@ -75,8 +82,7 @@
     <string name="rotate_right" msgid="6776325835923384839">"Putar ke kanan"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Tidak dapat menemukan item."</string>
     <string name="edit" msgid="1502273844748580847">"Edit"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Tak ada apl tersedia untuk menyelesaikan tindakan."</string>
-    <string name="process_caching_requests" msgid="8722939570307386071">"Memproses permintaan menyimpan ke tembolok"</string>
+    <string name="process_caching_requests" msgid="8722939570307386071">"Memproses permintaan menyimpan ke cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"Smpn ke tmbolok"</string>
     <string name="crop_action" msgid="3427470284074377001">"Pangkas"</string>
     <string name="set_as" msgid="3636764710790507868">"Setel sebagai"</string>
@@ -90,24 +96,22 @@
     <string name="untagged" msgid="7281481064509590402">"Tidak di-tag"</string>
     <string name="no_location" msgid="4043624857489331676">"Tidak ada lokasi"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Beberapa lokasi tidak dapat diidentifikasi karena masalah jaringan."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Gagal mengunduh foto dalam album ini. Coba lagi nanti."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Gagal mengunduh daftar album. Coba lagi nanti."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Tidak dapat mengunduh foto di album ini. Coba lagi nanti."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Hanya gambar"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Hanya video"</string>
-    <string name="show_all" msgid="4780647751652596980">"Gambar dan video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Gambar &amp; video"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galeri Foto"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Tidak ada foto."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Gambar yang dipangkas disimpan ke Unduhan."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Gambar yang dipangkas tidak disimpan."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Gambar yang dipotong disimpan ke <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Tidak ada album yang tersedia."</string>
     <string name="empty_album" msgid="4542880442593595494">"O gambar/video tersedia."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Pos"</string>
-    <string name="make_available_offline" msgid="5157950985488297112">"Jadikan agar tersedia luring"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Jadikan agar tersedia offline"</string>
     <string name="sync_picasa_albums" msgid="8522572542111169872">"Segarkan"</string>
     <string name="done" msgid="217672440064436595">"Selesai"</string>
     <string name="sequence_in_set" msgid="7235465319919457488">"%1$d dari %2$d item:"</string>
     <string name="title" msgid="7622928349908052569">"Judul"</string>
-    <string name="description" msgid="3016729318096557520">"Uraian"</string>
+    <string name="description" msgid="3016729318096557520">"Deskripsi"</string>
     <string name="time" msgid="1367953006052876956">"Waktu"</string>
     <string name="location" msgid="3432705876921618314">"Lokasi"</string>
     <string name="path" msgid="4725740395885105824">"Jalur"</string>
@@ -116,7 +120,7 @@
     <string name="orientation" msgid="4958327983165245513">"Orientasi"</string>
     <string name="duration" msgid="8160058911218541616">"Durasi"</string>
     <string name="mimetype" msgid="8024168704337990470">"Jenis MIME"</string>
-    <string name="file_size" msgid="8486169301588318915">"Ukuran berkas"</string>
+    <string name="file_size" msgid="8486169301588318915">"Ukuran file"</string>
     <string name="maker" msgid="7921835498034236197">"Pembuat"</string>
     <string name="model" msgid="8240207064064337366">"Model"</string>
     <string name="flash" msgid="2816779031261147723">"Lampu Kilat"</string>
@@ -131,10 +135,10 @@
     <string name="flash_on" msgid="7891556231891837284">"Lampu kilat aktif"</string>
     <string name="flash_off" msgid="1445443413822680010">"Tanpa lampu kilat"</string>
   <plurals name="make_albums_available_offline">
-    <item quantity="one" msgid="2171596356101611086">"Menjadikan album tersedia secara luring."</item>
-    <item quantity="other" msgid="4948604338155959389">"Menjadikan album tersedia secara luring."</item>
+    <item quantity="one" msgid="2171596356101611086">"Menjadikan album tersedia secara offline."</item>
+    <item quantity="other" msgid="4948604338155959389">"Menjadikan album tersedia secara offline."</item>
   </plurals>
-    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Item ini tersimpan secara lokal dan tersedia secara luring."</string>
+    <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Item ini tersimpan secara lokal dan tersedia secara offline."</string>
     <string name="set_label_all_albums" msgid="4581863582996336783">"Semua album"</string>
     <string name="set_label_local_albums" msgid="6698133661656266702">"Album lokal"</string>
     <string name="set_label_mtp_devices" msgid="1283513183744896368">"Perangkat MTP"</string>
@@ -153,7 +157,7 @@
     <string name="widget_type_shuffle" msgid="8594622705019763768">"Kocok semua gambar"</string>
     <string name="widget_type_photo" msgid="6267065337367795355">"Pilih gambar"</string>
     <string name="widget_type" msgid="1364653978966343448">"Pilih gambar"</string>
-    <string name="slideshow_dream_name" msgid="6915963319933437083">"Rangkai salindia"</string>
+    <string name="slideshow_dream_name" msgid="6915963319933437083">"Rangkai slide"</string>
     <string name="albums" msgid="7320787705180057947">"Album"</string>
     <string name="times" msgid="2023033894889499219">"Waktu"</string>
     <string name="locations" msgid="6649297994083130305">"Lokasi"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Kelompokkan menurut"</string>
     <string name="settings" msgid="1534847740615665736">"Setelan"</string>
     <string name="add_account" msgid="4271217504968243974">"Tambah akun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Unduhan"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Diimpor"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Tangkapan Layar"</string>
+    <string name="help" msgid="7368960711153618354">"Bantuan"</string>
 </resources>
diff --git a/res/values-it/photoeditor_strings.xml b/res/values-it/photoeditor_strings.xml
index 49044ef..1e42e79 100644
--- a/res/values-it/photoeditor_strings.xml
+++ b/res/values-it/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Impossibile caricare la foto"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Impossibile salvare la foto"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto salvate nell\'album <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Modificata"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Salvare la foto modificata?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Impossibile caricare la foto"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Impossibile salvare la foto modificata"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Foto modificata salvata in <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Ignorare le modifiche non salvate?"</string>
     <string name="yes" msgid="5402582493291792293">"Sì"</string>
-    <string name="no" msgid="5595408018304861875">"No"</string>
     <string name="save" msgid="5516670392524294967">"SALVA"</string>
     <string name="autofix" msgid="1663414996270538748">"Correzione autom."</string>
     <string name="crop" msgid="7598378507763334041">"Ritaglia"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Trascina gli indicatori per ritagliare la foto"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Disegna sulla foto per scarabocchiarla"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Trascina la foto per girarla"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Tocca per rimuovere gli occhi rossi"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Tocca gli occhi rossi per rimuoverli"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Trascina la foto per ruotarla"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Trascina la foto per raddrizzarla"</string>
 </resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 321bf67..88d57ab 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Riprendi riproduzione"</string>
     <string name="loading" msgid="7038208555304563571">"Caricamento..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Impossibile caricare"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Impossibile caricare l\'immagine"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Nessuna miniatura"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Ricomincia"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Ritaglia"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Tocca un viso per iniziare."</string>
     <string name="saving_image" msgid="7270334453636349407">"Salvataggio foto..."</string>
     <string name="save_error" msgid="6857408774183654970">"Impossibile salvare immagine ritagliata."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Seleziona album"</string>
     <string name="select_group" msgid="6744208543323307114">"Seleziona gruppo"</string>
     <string name="set_image" msgid="2331476809308010401">"Imposta foto come"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Imposta sfondo"</string>
     <string name="wallpaper" msgid="140165383777262070">"Impostazione sfondo..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Sfondo"</string>
     <string name="delete" msgid="2839695998251824487">"Elimina"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Elimina"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Eliminare elemento selezionato?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Eliminare elementi selezionati?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Conferma"</string>
     <string name="cancel" msgid="3637516880917356226">"Annulla"</string>
     <string name="share" msgid="3619042788254195341">"Condividi"</string>
     <string name="select_all" msgid="3403283025220282175">"Seleziona tutti"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Ruota a destra"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Impossibile trovare l\'elemento."</string>
     <string name="edit" msgid="1502273844748580847">"Modifica"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Nessuna appl. disponibile per completare azione."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Elaborazione richieste memorizzazione cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"Memorizzazione..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Ritaglia"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Senza tag"</string>
     <string name="no_location" msgid="4043624857489331676">"Nessun luogo"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Impossibile identificare alcune località a causa di problemi di rete."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Download delle foto in questo album non riuscito. Riprova più tardi."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Download dell\'elenco di album non riuscito. Riprova più tardi."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Download delle foto in questo album non riuscito. Riprova più tardi."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Solo immagini"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Solo video"</string>
-    <string name="show_all" msgid="4780647751652596980">"Immagini e video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Immagini e video"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galleria fotografica"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Nessuna foto."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Immagine ritagliata salvata in Download."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"L\'immagine ritagliata non è stata salvata."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Immagine ritagliata salvata in <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nessun album disponibile."</string>
     <string name="empty_album" msgid="4542880442593595494">"Nessun video/immagine disponibile."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Post"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Raggruppa per"</string>
     <string name="settings" msgid="1534847740615665736">"Impostazioni"</string>
     <string name="add_account" msgid="4271217504968243974">"Aggiungi account"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotocamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Download"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importate"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshot"</string>
+    <string name="help" msgid="7368960711153618354">"Guida"</string>
 </resources>
diff --git a/res/values-iw/photoeditor_strings.xml b/res/values-iw/photoeditor_strings.xml
index 1cc55b3..bde3689 100644
--- a/res/values-iw/photoeditor_strings.xml
+++ b/res/values-iw/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"לא ניתן לטעון את התמונה"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"לא ניתן לשמור את התמונה"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"התמונה נשמרה באלבום <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"תמונות ערוכות"</string>
-    <string name="save_photo" msgid="3125109368779997862">"לשמור את התמונה הערוכה?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"לא ניתן היה לטעון את התמונה"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"לא ניתן היה לשמור את התמונה שנערכה"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"התמונה שנערכה נשמרה ב-<xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"למחוק שינויים שלא נשמרו?"</string>
     <string name="yes" msgid="5402582493291792293">"כן"</string>
-    <string name="no" msgid="5595408018304861875">"לא"</string>
     <string name="save" msgid="5516670392524294967">"שמור"</string>
     <string name="autofix" msgid="1663414996270538748">"תיקון אוטומטי"</string>
     <string name="crop" msgid="7598378507763334041">"חתוך"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"גרור סמנים כדי לחתוך תמונה"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"גרור על גבי התמונה כדי לצייר עליה"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"גרור את התמונה כדי להפוך אותה"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"הקש כדי להסיר עיניים אדומות"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"הקש על עיניים אדומות כדי להסיר אותן"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"גרור את התמונה כדי לסובב אותה"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"גרור את התמונה כדי ליישר אותה"</string>
 </resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 76e7133..c62aad4 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"המשך את ההפעלה"</string>
     <string name="loading" msgid="7038208555304563571">"טוען..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"לא ניתן לטעון"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"לא היתה אפשרות לטעון את התמונה"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"ללא תמונה ממוזערת"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"התחל מחדש"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"חתוך"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"אישור"</string>
+    <string name="ok" msgid="5296833083983263293">"אישור"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"גע בפנים כלשהם כדי להתחיל."</string>
     <string name="saving_image" msgid="7270334453636349407">"שומר תמונה..."</string>
     <string name="save_error" msgid="6857408774183654970">"לא ניתן לשמור את התמונה החתוכה."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"בחר אלבום"</string>
     <string name="select_group" msgid="6744208543323307114">"בחר קבוצה"</string>
     <string name="set_image" msgid="2331476809308010401">"הגדר תמונה בתור"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"הגדר טפט"</string>
     <string name="wallpaper" msgid="140165383777262070">"מגדיר טפט..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"טפט"</string>
     <string name="delete" msgid="2839695998251824487">"מחק"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"מחק"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"האם למחוק את הפריט שנבחר?"</item>
+    <item quantity="other" msgid="5874316486520635333">"האם למחוק את הפריטים שנבחרו?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"אשר"</string>
     <string name="cancel" msgid="3637516880917356226">"ביטול"</string>
     <string name="share" msgid="3619042788254195341">"שיתוף"</string>
     <string name="select_all" msgid="3403283025220282175">"בחר הכול"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"סובב ימינה"</string>
     <string name="no_such_item" msgid="5315144556325243400">"הפריט לא נמצא."</string>
     <string name="edit" msgid="1502273844748580847">"ערוך"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"אין יישומים זמינים להשלמת הפעולה."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"מעבד בקשות העברה למטמון"</string>
     <string name="caching_label" msgid="4521059045896269095">"מעביר למטמון..."</string>
     <string name="crop_action" msgid="3427470284074377001">"חתוך"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"ללא תג"</string>
     <string name="no_location" msgid="4043624857489331676">"ללא מיקום"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"לא ניתן לזהות מיקומים מסוימים בשל בעיות ברשת."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"הורדת התמונות לאלבום זה נכשלה. נסה שוב מאוחר יותר."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"כשל בהורדת רשימת האלבומים. נסה שוב מאוחר יותר."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"לא ניתן להוריד את התמונות באלבום זה. נסה שוב מאוחר יותר."</string>
     <string name="show_images_only" msgid="7263218480867672653">"תמונות בלבד"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"סרטונים בלבד"</string>
-    <string name="show_all" msgid="4780647751652596980">"תמונות וסרטונים"</string>
+    <string name="show_all" msgid="6963292714584735149">"תמונות וסרטונים"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"גלריית תמונות"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"אין תמונות."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"התמונה החתוכה נשמרה ב\'הורדות\'."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"התמונה החתוכה לא נשמרה."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"התמונה שנחתכה נשמרה ב-<xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"אין אלבומים זמינים."</string>
     <string name="empty_album" msgid="4542880442593595494">"O תמונות/סרטוני וידאו זמינים."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"פוסטים"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"קבץ לפי"</string>
     <string name="settings" msgid="1534847740615665736">"הגדרות"</string>
     <string name="add_account" msgid="4271217504968243974">"הוסף חשבון"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"מצלמה"</string>
+    <string name="folder_download" msgid="7186215137642323932">"הורד"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"מיובאות"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"צילום מסך"</string>
+    <string name="help" msgid="7368960711153618354">"עזרה"</string>
 </resources>
diff --git a/res/values-ja/photoeditor_strings.xml b/res/values-ja/photoeditor_strings.xml
index 7686611..5fab603 100644
--- a/res/values-ja/photoeditor_strings.xml
+++ b/res/values-ja/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"フォトスタジオ"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"画像を読み込めません"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"画像を保存できません"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"写真をアルバム「<xliff:g id="ALBUM_NAME">%s</xliff:g>」に保存しました"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"編集済み"</string>
-    <string name="save_photo" msgid="3125109368779997862">"編集した写真を保存しますか?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"画像を読み込めませんでした"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"編集した画像を保存できませんでした"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"編集した画像を<xliff:g id="FOLDER_NAME">%s</xliff:g>に保存しました"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"保存されていない変更を破棄しますか?"</string>
     <string name="yes" msgid="5402582493291792293">"はい"</string>
-    <string name="no" msgid="5595408018304861875">"いいえ"</string>
     <string name="save" msgid="5516670392524294967">"保存"</string>
     <string name="autofix" msgid="1663414996270538748">"自動修正"</string>
     <string name="crop" msgid="7598378507763334041">"トリミング"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"写真をトリミングするにはマーカーをドラッグ"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"画像の上に書き込みます"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"画像をめくるにはドラッグします"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"赤目を補正するにはタップします"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"赤目を補正するには赤目部分をタップします"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"画像を回転させるにはドラッグします"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"画像をまっすぐにするにはドラッグします"</string>
 </resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 460fe6d..8cef890 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"再生を再開"</string>
     <string name="loading" msgid="7038208555304563571">"読み込み中..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"読み込めませんでした"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"画像を読み込めませんでした"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"サムネイルなし"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"最初から再生"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"トリミング"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"始めるには顔をタップします。"</string>
     <string name="saving_image" msgid="7270334453636349407">"写真を保存中…"</string>
     <string name="save_error" msgid="6857408774183654970">"トリミングした画像を保存できません。"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"アルバムを選択"</string>
     <string name="select_group" msgid="6744208543323307114">"グループの選択"</string>
     <string name="set_image" msgid="2331476809308010401">"登録"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"壁紙を設定"</string>
     <string name="wallpaper" msgid="140165383777262070">"壁紙を設定しています..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁紙"</string>
     <string name="delete" msgid="2839695998251824487">"削除"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"削除"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"アイテムを削除しますか?"</item>
+    <item quantity="other" msgid="5874316486520635333">"アイテムを削除しますか?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"確認"</string>
     <string name="cancel" msgid="3637516880917356226">"キャンセル"</string>
     <string name="share" msgid="3619042788254195341">"共有"</string>
     <string name="select_all" msgid="3403283025220282175">"すべて選択"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"右に回転"</string>
     <string name="no_such_item" msgid="5315144556325243400">"アイテムが見つかりませんでした。"</string>
     <string name="edit" msgid="1502273844748580847">"編集"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"この操作に使用できるアプリがありません。"</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"キャッシュリクエストを処理しています"</string>
     <string name="caching_label" msgid="4521059045896269095">"キャッシュ中..."</string>
     <string name="crop_action" msgid="3427470284074377001">"トリミング"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"タグなし"</string>
     <string name="no_location" msgid="4043624857489331676">"位置情報なし"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"ネットワークの問題により一部の位置情報を特定できませんでした。"</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"このアルバムの画像をダウンロードできませんでした。しばらくしてからもう一度お試しください。"</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"アルバムのリストをダウンロードできませんでした。しばらくしてからもう一度お試しください。"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"このアルバムの画像をダウンロードできませんでした。しばらくしてからもう一度お試しください。"</string>
     <string name="show_images_only" msgid="7263218480867672653">"画像のみ"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"動画のみ"</string>
-    <string name="show_all" msgid="4780647751652596980">"画像と動画"</string>
+    <string name="show_all" msgid="6963292714584735149">"画像と動画"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"フォトギャラリー"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"画像がありません。"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"トリミングした画像を[ダウンロード]に保存しました。"</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"トリミングした画像は保存されていません。"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"トリミングした画像を<xliff:g id="FOLDER_NAME">%s</xliff:g>に保存しました。"</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"使用できるアルバムはありません。"</string>
     <string name="empty_album" msgid="4542880442593595494">"使用できる画像/動画: 0件"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"投稿"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"グループ化"</string>
     <string name="settings" msgid="1534847740615665736">"設定"</string>
     <string name="add_account" msgid="4271217504968243974">"アカウントを追加"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"カメラ"</string>
+    <string name="folder_download" msgid="7186215137642323932">"ダウンロード"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"インポート済み"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"スクリーンショット"</string>
+    <string name="help" msgid="7368960711153618354">"ヘルプ"</string>
 </resources>
diff --git a/res/values-ko/photoeditor_strings.xml b/res/values-ko/photoeditor_strings.xml
index cede76b..3f89a37 100644
--- a/res/values-ko/photoeditor_strings.xml
+++ b/res/values-ko/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"사진 스튜디오"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"사진을 로드할 수 없습니다."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"사진을 저장할 수 없습니다."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"<xliff:g id="ALBUM_NAME">%s</xliff:g> 앨범에 사진을 저장했습니다."</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"수정된 사진"</string>
-    <string name="save_photo" msgid="3125109368779997862">"수정한 사진을 저장하시겠습니까?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"사진을 로드할 수 없습니다."</string>
+    <string name="saving_failure" msgid="8229491575433743974">"수정한 사진을 저장할 수 없습니다."</string>
+    <string name="photo_saved" msgid="6163006724627682202">"수정한 사진이 <xliff:g id="FOLDER_NAME">%s</xliff:g>에 저장되었습니다."</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"저장되지 않은 변경사항을 삭제하시겠습니까?"</string>
     <string name="yes" msgid="5402582493291792293">"예"</string>
-    <string name="no" msgid="5595408018304861875">"아니요"</string>
     <string name="save" msgid="5516670392524294967">"저장"</string>
     <string name="autofix" msgid="1663414996270538748">"자동 보정"</string>
     <string name="crop" msgid="7598378507763334041">"자르기"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"아이콘을 드래그하여 사진 자르기"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"사진에 그림을 그려 낙서하기"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"사진을 드래그하여 방향 바꾸기"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"터치하여 적목 현상 제거"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"적목현상을 없애려면 눈을 탭하세요."</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"드래그하여 사진 회전"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"사진을 드래그하여 반듯하게 하기"</string>
 </resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 1f8f45b..4ac2b37 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"이어서 보기"</string>
     <string name="loading" msgid="7038208555304563571">"로드 중..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"로드하지 못했습니다."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"이미지를 로드할 수 없습니다."</string>
     <string name="no_thumbnail" msgid="284723185546429750">"미리보기 이미지 없음"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"처음부터 보기"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"자르기"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"확인"</string>
+    <string name="ok" msgid="5296833083983263293">"확인"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"시작하려면 얼굴을 터치하세요."</string>
     <string name="saving_image" msgid="7270334453636349407">"사진 저장 중..."</string>
     <string name="save_error" msgid="6857408774183654970">"잘린 이미지를 저장하지 못했습니다."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"앨범 선택"</string>
     <string name="select_group" msgid="6744208543323307114">"그룹 선택"</string>
     <string name="set_image" msgid="2331476809308010401">"사진을 다음으로 설정"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"배경화면 설정"</string>
     <string name="wallpaper" msgid="140165383777262070">"배경화면을 설정하는 중..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"배경화면"</string>
     <string name="delete" msgid="2839695998251824487">"삭제"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"삭제"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"선택한 항목을 삭제할까요?"</item>
+    <item quantity="other" msgid="5874316486520635333">"선택한 항목을 삭제할까요?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"확인"</string>
     <string name="cancel" msgid="3637516880917356226">"취소"</string>
     <string name="share" msgid="3619042788254195341">"공유"</string>
     <string name="select_all" msgid="3403283025220282175">"모두 선택"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"오른쪽으로 회전"</string>
     <string name="no_such_item" msgid="5315144556325243400">"항목을 찾을 수 없습니다."</string>
     <string name="edit" msgid="1502273844748580847">"수정"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"작업을 완료할 수 있는 앱이 없습니다."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"캐시 요청을 처리하는 중"</string>
     <string name="caching_label" msgid="4521059045896269095">"캐시하는 중..."</string>
     <string name="crop_action" msgid="3427470284074377001">"자르기"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"태그 지정 안함"</string>
     <string name="no_location" msgid="4043624857489331676">"위치 없음"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"네트워크 문제로 인해 일부 위치를 식별하지 못했습니다."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"앨범 사진을 다운로드하지 못했습니다. 나중에 다시 시도해 주세요."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"앨범 목록을 다운로드하지 못했습니다. 나중에 다시 시도해 주세요."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"앨범 사진을 다운로드하지 못했습니다. 나중에 다시 시도해 주세요."</string>
     <string name="show_images_only" msgid="7263218480867672653">"이미지"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"동영상"</string>
-    <string name="show_all" msgid="4780647751652596980">"이미지 및 동영상"</string>
+    <string name="show_all" msgid="6963292714584735149">"이미지 및 동영상"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"사진 갤러리"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"사진이 없습니다."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"잘린 이미지가 다운로드에 저장되었습니다."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"잘린 이미지가 저장되지 않았습니다."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"잘린 이미지가 <xliff:g id="FOLDER_NAME">%s</xliff:g>에 저장되었습니다."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"사용할 수 있는 앨범이 없습니다."</string>
     <string name="empty_album" msgid="4542880442593595494">"사용할 수 있는 이미지/동영상이 없습니다."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"소식"</string>
@@ -155,11 +159,16 @@
     <string name="widget_type" msgid="1364653978966343448">"이미지 선택"</string>
     <string name="slideshow_dream_name" msgid="6915963319933437083">"슬라이드쇼"</string>
     <string name="albums" msgid="7320787705180057947">"앨범"</string>
-    <string name="times" msgid="2023033894889499219">"횟수"</string>
+    <string name="times" msgid="2023033894889499219">"시간"</string>
     <string name="locations" msgid="6649297994083130305">"위치"</string>
     <string name="people" msgid="4114003823747292747">"사용자"</string>
     <string name="tags" msgid="5539648765482935955">"태그"</string>
     <string name="group_by" msgid="4308299657902209357">"그룹화 기준"</string>
     <string name="settings" msgid="1534847740615665736">"설정"</string>
     <string name="add_account" msgid="4271217504968243974">"계정 추가"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"카메라"</string>
+    <string name="folder_download" msgid="7186215137642323932">"다운로드"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"가져옴"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"스크린샷"</string>
+    <string name="help" msgid="7368960711153618354">"도움말"</string>
 </resources>
diff --git a/res/values-lt/photoeditor_strings.xml b/res/values-lt/photoeditor_strings.xml
index a2b5fed..c9f76c4 100644
--- a/res/values-lt/photoeditor_strings.xml
+++ b/res/values-lt/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Negalima įkelti nuotraukos"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Negalima išsaugoti nuotraukos"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Nuotrauka išsaugota albume „<xliff:g id="ALBUM_NAME">%s</xliff:g>“"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Redaguota"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Išsaugoti redaguotą nuotrauką?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Nepavyko įkelti nuotraukos"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Nepavyko išsaugoti redaguotos nuotraukos"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Redaguota nuotrauka išsaugota aplanke <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Atmesti neišsaugotus pakeitimus?"</string>
     <string name="yes" msgid="5402582493291792293">"Taip"</string>
-    <string name="no" msgid="5595408018304861875">"Ne"</string>
     <string name="save" msgid="5516670392524294967">"IŠSAUGOT"</string>
     <string name="autofix" msgid="1663414996270538748">"Automat. nustat."</string>
     <string name="crop" msgid="7598378507763334041">"Apkarpyti"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Jei norite apkarpyti nuotr., vilkite žymeklius"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Pieškite ant nuotraukos, jei norite taikyti logotipų puošybą"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Vilkite nuotrauką, kad ją apverstumėte"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Palieskite, kad pašalintumėte raudonas akis"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Paliesk. raud. akis, kad pašal. jų efek."</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Vilkite nuotrauką, kad ją apsuktumėte"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Vilkite nuotrauką, kad ją ištiesintumėte"</string>
 </resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 1a2b9fd..95f1a8f 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Atnaujinti grojimą"</string>
     <string name="loading" msgid="7038208555304563571">"Įkeliama…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nepavyko įkelti"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nepavyko įkelti vaizdo"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Nėra miniatiūros"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Pradėti iš naujo"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Apkarpyti"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Gerai"</string>
+    <string name="ok" msgid="5296833083983263293">"Gerai"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Jei norite pradėti, palieskite veidą."</string>
     <string name="saving_image" msgid="7270334453636349407">"Išsaugomas paveikslėlis..."</string>
     <string name="save_error" msgid="6857408774183654970">"Nepavyko išsaugoti apkarpyto vaizdo."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Pasirinkti albumą"</string>
     <string name="select_group" msgid="6744208543323307114">"Pasirinkti grupę"</string>
     <string name="set_image" msgid="2331476809308010401">"Nustatyti paveikslėlį kaip"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nustat. darbal. foną"</string>
     <string name="wallpaper" msgid="140165383777262070">"Nustatomas darbalaukio fonas…"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Darbalaukio fonas"</string>
     <string name="delete" msgid="2839695998251824487">"Ištrinti"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Ištrinti"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ištrinti pasirinktą elementą?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Ištrinti pasirink. elementus?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Patvirtinti"</string>
     <string name="cancel" msgid="3637516880917356226">"Atšaukti"</string>
     <string name="share" msgid="3619042788254195341">"Bendrinti"</string>
     <string name="select_all" msgid="3403283025220282175">"Pasirinkti viską"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Sukti į dešinę"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Nepavyko surasti elemento."</string>
     <string name="edit" msgid="1502273844748580847">"Redaguoti"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Nėra pasiekiamos programos veiksmui užbaigti."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Talpinimo užklausų apdorojimas"</string>
     <string name="caching_label" msgid="4521059045896269095">"Talpinama…"</string>
     <string name="crop_action" msgid="3427470284074377001">"Apkarpyti"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Nepažymėta"</string>
     <string name="no_location" msgid="4043624857489331676">"Nėra vietos"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Kai kurių vietų nepavyko nustatyti dėl tinklo problemų."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Atsiunčiant šio albumo nuotraukas įvyko klaida. Bandykite dar kartą vėliau."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Atsiunčiant albumų sąrašą įvyko klaida. Bandykite dar kartą vėliau."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Šio albumo nuotraukų atsisiųsti nepavyko. Bandykite dar kartą vėliau."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Tik vaizdai"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Tik vaizdo įrašai"</string>
-    <string name="show_all" msgid="4780647751652596980">"Vaizdai ir vaizdo įrašai"</string>
+    <string name="show_all" msgid="6963292714584735149">"Vaizdai ir vaizdo įrašai"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Nuotraukų galerija"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Nėra nuotraukų."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Apkarpytas vaizdas išsaug. skilt. „Atsisiuntimai“."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Apkarpytas vaizdas neišsaugotas."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Apkarpytas vaizdas išsaugotas aplanke <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nėra pasiekiamų albumų."</string>
     <string name="empty_album" msgid="4542880442593595494">"Nepasiekiami jokie vaizdai / vaizdo įrašai."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Įrašai"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupuoti pagal"</string>
     <string name="settings" msgid="1534847740615665736">"Nustatymai"</string>
     <string name="add_account" msgid="4271217504968243974">"Pridėti paskyrą"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparatas"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Atsisiųsti"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importuota"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekrano kopija"</string>
+    <string name="help" msgid="7368960711153618354">"Pagalba"</string>
 </resources>
diff --git a/res/values-lv/photoeditor_strings.xml b/res/values-lv/photoeditor_strings.xml
index f1af66e..444f2c9 100644
--- a/res/values-lv/photoeditor_strings.xml
+++ b/res/values-lv/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Fotostudija"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotoattēlu nevar ielādēt."</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotoattēlu nevar saglabāt."</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotoattēls ir saglabāts albumā <xliff:g id="ALBUM_NAME">%s</xliff:g>."</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Rediģēti"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Vai saglabāt rediģēto fotoattēlu?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotoattēlu nevarēja ielādēt."</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Rediģēto fotoattēlu nevarēja saglabāt."</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Rediģētais attēls ir saglabāts mapē <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Vai atmest nesaglabātās izmaiņas?"</string>
     <string name="yes" msgid="5402582493291792293">"Jā"</string>
-    <string name="no" msgid="5595408018304861875">"Nē"</string>
     <string name="save" msgid="5516670392524294967">"SAGLABĀT"</string>
     <string name="autofix" msgid="1663414996270538748">"Automātiska labošana"</string>
     <string name="crop" msgid="7598378507763334041">"Apgriešana"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Velciet marķierus, lai apgrieztu fotoattēlu."</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Velciet uz fotoattēla, lai kricelētu."</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Velciet fotoattēlu, lai to apgrieztu."</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Pieskarieties, lai noņemtu sarkano acu efektu."</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Piesk. sark. acīm, lai noņ. šo efektu."</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Velciet fotoattēlu, lai to pagrieztu."</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Velciet fotoattēlu, lai to iztaisnotu."</string>
 </resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 2da2540..c3e47b4 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Atsākt atskaņošanu"</string>
     <string name="loading" msgid="7038208555304563571">"Notiek ielāde…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nevarēja ielādēt"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nevarēja ielādēt attēlu."</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Nav sīktēla."</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Sākt vēlreiz"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Apgriezt"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Labi"</string>
+    <string name="ok" msgid="5296833083983263293">"Labi"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Pieskarieties sejai, lai sāktu."</string>
     <string name="saving_image" msgid="7270334453636349407">"Notiek attēla saglabāšana..."</string>
     <string name="save_error" msgid="6857408774183654970">"Nevarēja saglabāt izgriezto attēlu."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Albuma atlase"</string>
     <string name="select_group" msgid="6744208543323307114">"Atlasiet grupu"</string>
     <string name="set_image" msgid="2331476809308010401">"Iestatīt attēlu kā:"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Iestatīt fona tapeti"</string>
     <string name="wallpaper" msgid="140165383777262070">"Notiek fona tapetes iestatīšana..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fona tapete"</string>
     <string name="delete" msgid="2839695998251824487">"Dzēst"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Dzēst"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vai dzēst atlasīto vienumu?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vai dzēst atlasītos vienumus?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Apstiprināt"</string>
     <string name="cancel" msgid="3637516880917356226">"Atcelt"</string>
     <string name="share" msgid="3619042788254195341">"Dalies"</string>
     <string name="select_all" msgid="3403283025220282175">"Atlasīt visu"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Pagriezt pa labi"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Nevarēja atrast vienumu."</string>
     <string name="edit" msgid="1502273844748580847">"Rediģēt"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Nav nevienas lietotnes šīs darbības veikšanai."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Tiek apstrādāti pieprasījumi rakstīšanai kešatmiņā"</string>
     <string name="caching_label" msgid="4521059045896269095">"Glabā kešatm..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Apgriezt"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Bez atzīmēm"</string>
     <string name="no_location" msgid="4043624857489331676">"Nav vietas inform."</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Tīkla problēmu dēļ nevarēja noteikt dažas atrašanās vietas."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Neizdevās lejupielādēt šī albuma fotoattēlus. Lūdzu, vēlāk mēģiniet vēlreiz."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Neizdevās lejupielādēt albumu sarakstu. Lūdzu, vēlāk mēģiniet vēlreiz."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Nevarēja lejupielādēt šī albuma fotoattēlus. Vēlāk mēģiniet vēlreiz."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Tikai attēli"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Tikai videoklipi"</string>
-    <string name="show_all" msgid="4780647751652596980">"Attēli un videoklipi"</string>
+    <string name="show_all" msgid="6963292714584735149">"Attēli un videoklipi"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerija"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Nav fotoattēlu."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Izgrieztais attēls ir saglabāts mapē Lejupielādes."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Izgrieztais attēls netika saglabāts."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Apgrieztais attēls ir saglabāts mapē <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nav pieejams neviens albums."</string>
     <string name="empty_album" msgid="4542880442593595494">"Nav pieejams neviens attēls/videoklips."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Ziņas"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupēt pēc:"</string>
     <string name="settings" msgid="1534847740615665736">"Iestatījumi"</string>
     <string name="add_account" msgid="4271217504968243974">"Konta pievienošana"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Lejupielādētie"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importētie"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekrānuzņēmums"</string>
+    <string name="help" msgid="7368960711153618354">"Palīdzība"</string>
 </resources>
diff --git a/res/values-ms/photoeditor_strings.xml b/res/values-ms/photoeditor_strings.xml
index ac4a130..c7be0f8 100644
--- a/res/values-ms/photoeditor_strings.xml
+++ b/res/values-ms/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Studio Foto"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Foto tidak boleh dimuatkan"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Foto tidak boleh disimpan"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto disimpan ke album <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Diedit"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Simpan foto yang diedit?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Tidak dapat memuatkan foto"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Tidak dapat menyimpan foto yang diedit"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Foto diedit disimpan ke <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Buang perubahan yang belum disimpan?"</string>
     <string name="yes" msgid="5402582493291792293">"Ya"</string>
-    <string name="no" msgid="5595408018304861875">"Tidak"</string>
     <string name="save" msgid="5516670392524294967">"SIMPAN"</string>
     <string name="autofix" msgid="1663414996270538748">"Auto-baiki"</string>
     <string name="crop" msgid="7598378507763334041">"Pangkas"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Seret penanda untuk memangkas foto"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Lukis pada foto untuk mencoret"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Seret foto untuk menterbalikkannya"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Ketik untuk mengalih keluar mata merah"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Ketik pada mata merah untuk mengalih keluar"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Seret foto untuk memutarnya"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Seret foto untuk meluruskannya"</string>
 </resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index eead8b1..8349896 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Sambung semula proses main"</string>
     <string name="loading" msgid="7038208555304563571">"Memuatkan..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Tidak dapat memuatkan"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Tidak dapat memuatkan imej"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Tiada lakaran kenit"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Mainkan semula dari mula"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Pangkas"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Sentuh muka untuk bermula."</string>
     <string name="saving_image" msgid="7270334453636349407">"Menyimpan gambar..."</string>
     <string name="save_error" msgid="6857408774183654970">"Tidak dapat menyimpan imej yang dipangkas."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Pilih album"</string>
     <string name="select_group" msgid="6744208543323307114">"Pilih kumpulan"</string>
     <string name="set_image" msgid="2331476809308010401">"Tetapkan gambar sebagai"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Tetapkan kertas dinding"</string>
     <string name="wallpaper" msgid="140165383777262070">"Menetapkan kertas dinding…"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Kertas dinding"</string>
     <string name="delete" msgid="2839695998251824487">"Padam"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Padam"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Padam item yang dipilih?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Padam item yang dipilih?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Sahkan"</string>
     <string name="cancel" msgid="3637516880917356226">"Batal"</string>
     <string name="share" msgid="3619042788254195341">"Kongsi"</string>
     <string name="select_all" msgid="3403283025220282175">"Pilih semua"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Putar ke kanan"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Tidak dapat mencari item."</string>
     <string name="edit" msgid="1502273844748580847">"Edit"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Tiada aplikasi tersedia untuk melengkapkan tindakan."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Memproses permintaan cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"Mengcache..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Pangkas"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Tidak ditanda namakan"</string>
     <string name="no_location" msgid="4043624857489331676">"Tiada lokasi"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Beberapa lokasi tidak dapat dikenal pasti kerana masalah rangkaian."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Gagal untuk memuat turun foto dalam album ini. Sila cuba lagi nanti."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Gagal untuk memuat turun senarai album. Sila cuba lagi nanti."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Tidak dapat memuat turun foto dalam album ini. Cuba lagi nanti."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Imej sahaja"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Video sahaja"</string>
-    <string name="show_all" msgid="4780647751652596980">"Imej dan video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imej &amp; video"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galeri Foto"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Tiada foto."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Imej yang dipangkas disimpan ke Muat Turun."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Imej yang dipangkas tidak disimpan."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Imej yang dipangkas disimpan ke <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Tiada album tersedia."</string>
     <string name="empty_album" msgid="4542880442593595494">"O imej/video yang tersedia."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Siaran"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Kumpulkan mengikut"</string>
     <string name="settings" msgid="1534847740615665736">"Tetapan"</string>
     <string name="add_account" msgid="4271217504968243974">"Tambah akaun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Muat Turun"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Diimport"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Tangkapan skrin"</string>
+    <string name="help" msgid="7368960711153618354">"Bantuan"</string>
 </resources>
diff --git a/res/values-nb/photoeditor_strings.xml b/res/values-nb/photoeditor_strings.xml
index 03f7c12..425cd77 100644
--- a/res/values-nb/photoeditor_strings.xml
+++ b/res/values-nb/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Bildet kan ikke lastes inn"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Bildet kan ikke lagres"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto lagret i albumet <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Redigert"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Vil du lagre det redigerte bildet?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Kunne ikke laste inn bildet"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Kunne ikke lagre det redigerte bildet"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Det redigerte bildet ble lagret i <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Vil du forkaste endringer som ikke er lagret?"</string>
     <string name="yes" msgid="5402582493291792293">"Ja"</string>
-    <string name="no" msgid="5595408018304861875">"Nei"</string>
     <string name="save" msgid="5516670392524294967">"LAGRE"</string>
     <string name="autofix" msgid="1663414996270538748">"Autokorrektur"</string>
     <string name="crop" msgid="7598378507763334041">"Beskjær"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Dra markører for å beskjære bildet"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Tegn på bildet for å drodle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Dra bildet for å snu det"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Trykk for å fjerne røde øyne"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Trykk på røde øyne for å fjerne dem"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Dra bildet for å rotere det"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Dra bildet for å rette på det"</string>
 </resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index d8a708a..37874fe 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsett avspilling"</string>
     <string name="loading" msgid="7038208555304563571">"Laster inn ..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Kunne ikke laste"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kunne ikke laste inn bildet"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniatyrbilder"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Begynn på nytt"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Beskjæring"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Berør et ansikt for å begynne."</string>
     <string name="saving_image" msgid="7270334453636349407">"Lagrer bilde ..."</string>
     <string name="save_error" msgid="6857408774183654970">"Kan ikke lagre det beskårede bildet."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Velg album"</string>
     <string name="select_group" msgid="6744208543323307114">"Valg av gruppe"</string>
     <string name="set_image" msgid="2331476809308010401">"Angi bilde som"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Angi bakgrunnsbilde"</string>
     <string name="wallpaper" msgid="140165383777262070">"Angir bakgrunn …"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrunnsbilde"</string>
     <string name="delete" msgid="2839695998251824487">"Slett"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Slett"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Vil du slette det valgte medieelementet?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Vil du slette de valgte medieelementene?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bekreft"</string>
     <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
     <string name="share" msgid="3619042788254195341">"Del"</string>
     <string name="select_all" msgid="3403283025220282175">"Velg alle"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Roter mot høyre"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Finner ikke elementet."</string>
     <string name="edit" msgid="1502273844748580847">"Rediger"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Ingen app er tilgjengelig for å fullføre handlingen."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Behandler forespørsler om bufring"</string>
     <string name="caching_label" msgid="4521059045896269095">"Henter …"</string>
     <string name="crop_action" msgid="3427470284074377001">"Beskjær"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Uten etikett"</string>
     <string name="no_location" msgid="4043624857489331676">"Ingen posisjon"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Noen steder kunne ikke identifiseres på grunn av nettverksproblemer."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Kunne ikke laste ned bildene i dette albumet. Prøv på nytt senere."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Kunne ikke laste ned listen over albumer. Prøv på nytt senere."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Kunne ikke laste ned bildene i dette albumet. Prøv på nytt senere."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Kun bilder"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Kun videoer"</string>
-    <string name="show_all" msgid="4780647751652596980">"Bilder og videoer"</string>
+    <string name="show_all" msgid="6963292714584735149">"Bilder og videoer"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalleri"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Ingen bilder."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Beskåret bilde lagret i Nedlastinger."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Beskåret bilde ble ikke lagret."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Det beskårede bildet er lagret i <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Ingen album tilgjengelig."</string>
     <string name="empty_album" msgid="4542880442593595494">"0 bilder/videoer tilgjengelig."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Innlegg"</string>
@@ -155,11 +159,16 @@
     <string name="widget_type" msgid="1364653978966343448">"Valg av bilder"</string>
     <string name="slideshow_dream_name" msgid="6915963319933437083">"Lysbildefremvisning"</string>
     <string name="albums" msgid="7320787705180057947">"Album"</string>
-    <string name="times" msgid="2023033894889499219">"Ganger"</string>
+    <string name="times" msgid="2023033894889499219">"Tidspunkt"</string>
     <string name="locations" msgid="6649297994083130305">"Steder"</string>
     <string name="people" msgid="4114003823747292747">"Folk"</string>
     <string name="tags" msgid="5539648765482935955">"Etiketter"</string>
     <string name="group_by" msgid="4308299657902209357">"Gruppér etter"</string>
     <string name="settings" msgid="1534847740615665736">"Innstillinger"</string>
     <string name="add_account" msgid="4271217504968243974">"Legg til konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Nedlasting"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importert"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skjermdump"</string>
+    <string name="help" msgid="7368960711153618354">"Brukerstøtte"</string>
 </resources>
diff --git a/res/values-nl/photoeditor_strings.xml b/res/values-nl/photoeditor_strings.xml
index 7a0f374..e4418fc 100644
--- a/res/values-nl/photoeditor_strings.xml
+++ b/res/values-nl/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Foto kan niet worden geladen"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Foto kan niet worden opgeslagen"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto opgeslagen in album <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Bewerkt"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Bewerkte foto opslaan?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Kan de foto niet laden"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Kan de bewerkte foto niet opslaan"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Bewerkte foto is opgeslagen in <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Niet-opgeslagen wijzigingen annuleren?"</string>
     <string name="yes" msgid="5402582493291792293">"Ja"</string>
-    <string name="no" msgid="5595408018304861875">"Nee"</string>
     <string name="save" msgid="5516670392524294967">"OPSLAAN"</string>
     <string name="autofix" msgid="1663414996270538748">"Auto-fix"</string>
     <string name="crop" msgid="7598378507763334041">"Bijsnijden"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Sleep de markeringen om de foto bij te snijden"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Teken op de foto om hierop te krabbelen"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Sleep de foto om deze te spiegelen"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Tik om rode ogen te verwijderen"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Tik op rode ogen om ze te verwijderen"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Sleep de foto om deze te draaien"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Sleep de foto om deze recht te zetten"</string>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 9e43d85..f28c90d 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Afspelen hervatten"</string>
     <string name="loading" msgid="7038208555304563571">"Laden..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Kan niet laden"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Kan de afbeelding niet laden"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Geen miniatuur"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Opnieuw starten"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Bijsnijden"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Raak een gezicht aan om te beginnen."</string>
     <string name="saving_image" msgid="7270334453636349407">"Foto opslaan..."</string>
     <string name="save_error" msgid="6857408774183654970">"Kan bijgesneden afbeelding niet opslaan."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Album selecteren"</string>
     <string name="select_group" msgid="6744208543323307114">"Groep selecteren"</string>
     <string name="set_image" msgid="2331476809308010401">"Foto instellen als"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Achtergrond inst."</string>
     <string name="wallpaper" msgid="140165383777262070">"Achtergrond instellen..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Achtergrond"</string>
     <string name="delete" msgid="2839695998251824487">"Verwijderen"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Verwijderen"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Geselecteerd item verwijderen?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Geselect. items verwijderen?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bevestigen"</string>
     <string name="cancel" msgid="3637516880917356226">"Annuleren"</string>
     <string name="share" msgid="3619042788254195341">"Delen"</string>
     <string name="select_all" msgid="3403283025220282175">"Alles selecteren"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Rechtsom draaien"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Kan item niet vinden."</string>
     <string name="edit" msgid="1502273844748580847">"Bewerken"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Geen app beschikbaar om de actie te voltooien."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Cacheverzoeken verwerken"</string>
     <string name="caching_label" msgid="4521059045896269095">"In cache opslaan..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Bijsnijden"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Geen tags"</string>
     <string name="no_location" msgid="4043624857489331676">"Geen locatie"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Sommige locaties kunnen wegens problemen met het netwerk niet worden geïdentificeerd."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Het downloaden van de foto\'s in dit album is mislukt. Probeer het later opnieuw."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Het downloaden van de lijst met albums is mislukt. Probeer het later opnieuw."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Kan de foto\'s in dit album niet downloaden. Probeer het later opnieuw."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Alleen afbeeldingen"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Alleen video\'s"</string>
-    <string name="show_all" msgid="4780647751652596980">"Afbeeldingen en video\'s"</string>
+    <string name="show_all" msgid="6963292714584735149">"Afbeeldingen en video\'s"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerij"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Geen foto\'s."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Bijgesneden afbeelding opgeslagen in Downloads."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Bijgesneden afbeelding is niet opgeslagen."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Bijgesneden afbeelding is opgeslagen in <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Geen albums beschikbaar."</string>
     <string name="empty_album" msgid="4542880442593595494">"0 afbeeldingen/video\'s beschikbaar."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Berichten"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Groeperen op"</string>
     <string name="settings" msgid="1534847740615665736">"Instellingen"</string>
     <string name="add_account" msgid="4271217504968243974">"Account toevoegen"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Camera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Downloaden"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Geïmporteerd"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Schermafbeelding"</string>
+    <string name="help" msgid="7368960711153618354">"Help"</string>
 </resources>
diff --git a/res/values-pl/photoeditor_strings.xml b/res/values-pl/photoeditor_strings.xml
index e48c333..628cccb 100644
--- a/res/values-pl/photoeditor_strings.xml
+++ b/res/values-pl/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Studio fotograficzne"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Nie można wczytać zdjęcia"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Nie można zapisać zdjęcia"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Zdjęcie zapisano w albumie <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Edytowane"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Czy zapisać edytowane zdjęcie?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Nie można wczytać zdjęcia"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Nie można zapisać edytowanego zdjęcia"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Edytowane zdjęcie zapisano w <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Odrzucić niezapisane zmiany?"</string>
     <string name="yes" msgid="5402582493291792293">"Tak"</string>
-    <string name="no" msgid="5595408018304861875">"Nie"</string>
     <string name="save" msgid="5516670392524294967">"ZAPISZ"</string>
     <string name="autofix" msgid="1663414996270538748">"Autopoprawka"</string>
     <string name="crop" msgid="7598378507763334041">"Przytnij"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Przeciągnij znaczniki, aby przyciąć zdjęcie"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Rysuj po zdjęciu, aby dodać bazgroły."</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Przeciągnij zdjęcie, aby je odwrócić."</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Dotknij, aby usunąć efekt czerwonych oczu."</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Dotknij czerwonych oczu, aby je usunąć"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Przeciągnij zdjęcie, aby je obrócić."</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Przeciągnij zdjęcie, aby je wyprostować."</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 82f4310..5b6ca53 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Wznów odtwarzanie"</string>
     <string name="loading" msgid="7038208555304563571">"Wczytywanie…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nie można wczytać"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nie można wczytać zdjęcia"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Brak miniatury"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Rozpocznij"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Przytnij"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Dotknij twarzy, aby rozpocząć."</string>
     <string name="saving_image" msgid="7270334453636349407">"Zapisywanie zdjęcia…"</string>
     <string name="save_error" msgid="6857408774183654970">"Nie można zapisać przyciętego obrazu."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Wybierz album"</string>
     <string name="select_group" msgid="6744208543323307114">"Wybierz grupę"</string>
     <string name="set_image" msgid="2331476809308010401">"Ustaw zdjęcie jako"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Ustaw tapetę"</string>
     <string name="wallpaper" msgid="140165383777262070">"Ustawianie tapety…"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
     <string name="delete" msgid="2839695998251824487">"Usuń"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Usuń"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Usunąć wybrany element?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Usunąć wybrane elementy?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potwierdź"</string>
     <string name="cancel" msgid="3637516880917356226">"Anuluj"</string>
     <string name="share" msgid="3619042788254195341">"Udostępnij"</string>
     <string name="select_all" msgid="3403283025220282175">"Zaznacz wszystkie"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Obróć w prawo"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Nie można znaleźć elementu."</string>
     <string name="edit" msgid="1502273844748580847">"Edytuj"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Brak dostępnej aplikacji do ukończenia operacji"</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Przetwarzanie żądań dotyczących buforowania"</string>
     <string name="caching_label" msgid="4521059045896269095">"Buforowanie…"</string>
     <string name="crop_action" msgid="3427470284074377001">"Przytnij"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Nieoznaczone tagami"</string>
     <string name="no_location" msgid="4043624857489331676">"Brak lokalizacji"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Niektórych lokalizacji nie można zidentyfikować z powodu problemów z siecią."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Nie udało się pobrać zdjęć z tego albumu. Spróbuj ponownie później."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Nie udało się pobrać listy albumów. Spróbuj ponownie później."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Nie można pobrać zdjęć z tego albumu. Spróbuj ponownie później."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Tylko obrazy"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Tylko filmy"</string>
-    <string name="show_all" msgid="4780647751652596980">"Obrazy i filmy"</string>
+    <string name="show_all" msgid="6963292714584735149">"Obrazy i filmy"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galeria zdjęć"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Brak zdjęć"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Przycięty obraz zapisano w Pobranych plikach."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Przycięty obraz nie został zapisany."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Przycięty obraz zapisano w <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Brak dostępnych albumów"</string>
     <string name="empty_album" msgid="4542880442593595494">"O dostępnych obrazów/filmów"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Posty"</string>
@@ -139,7 +143,7 @@
     <string name="set_label_local_albums" msgid="6698133661656266702">"Albumy lokalne"</string>
     <string name="set_label_mtp_devices" msgid="1283513183744896368">"Urządzenia MTP"</string>
     <string name="set_label_picasa_albums" msgid="5356258354953935895">"Albumy Picasa"</string>
-    <string name="free_space_format" msgid="8766337315709161215">"Wolne: <xliff:g id="BYTES">%s</xliff:g>"</string>
+    <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> wolne"</string>
     <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> lub mniej"</string>
     <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> lub więcej"</string>
     <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> do <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupuj według"</string>
     <string name="settings" msgid="1534847740615665736">"Ustawienia"</string>
     <string name="add_account" msgid="4271217504968243974">"Dodaj konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Aparat"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Pobrane"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Zaimportowane"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Zrzuty ekranu"</string>
+    <string name="help" msgid="7368960711153618354">"Pomoc"</string>
 </resources>
diff --git a/res/values-pt-rPT/photoeditor_strings.xml b/res/values-pt-rPT/photoeditor_strings.xml
index f0f6e00..13575a8 100644
--- a/res/values-pt-rPT/photoeditor_strings.xml
+++ b/res/values-pt-rPT/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Não é possível carregar a fotografia"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Não é possível guardar a fotografia"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotografia guardada no álbum <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Editado"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Guardar a fotografia editada?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Não foi possível carregar a fotografia"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Não foi possível guardar a foto editada."</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Fotografia editada guardada em <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Rejeitar alterações não guardadas?"</string>
     <string name="yes" msgid="5402582493291792293">"Sim"</string>
-    <string name="no" msgid="5595408018304861875">"Não"</string>
     <string name="save" msgid="5516670392524294967">"GUARDAR"</string>
     <string name="autofix" msgid="1663414996270538748">"Correção Autom."</string>
     <string name="crop" msgid="7598378507763334041">"Recortar"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Arraste os marcadores para recortar a fotografia"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Desenhe na fotografia para rabiscar"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Arraste a fotografia para virá-la"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Toque para remover o efeito de olhos vermelhos"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Toque nos olhos vermelhos p/ os remover"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Arraste a fotografia para rodá-la"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Arraste a fotografia para endireitá-la"</string>
 </resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index d0b54fe..fa0288a 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string>
     <string name="loading" msgid="7038208555304563571">"A carregar..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Não foi possível carregar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Não foi possível carregar a imagem"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Sem miniatura"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Recomeçar"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Recortar"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Toque num rosto para começar."</string>
     <string name="saving_image" msgid="7270334453636349407">"A guardar imagem..."</string>
     <string name="save_error" msgid="6857408774183654970">"Impossível guardar imagem recortada."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Selecionar álbum"</string>
     <string name="select_group" msgid="6744208543323307114">"Selecionar grupo"</string>
     <string name="set_image" msgid="2331476809308010401">"Definir imagem como"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Definir imagem fundo"</string>
     <string name="wallpaper" msgid="140165383777262070">"A definir imagem de fundo…"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Imagem de fundo"</string>
     <string name="delete" msgid="2839695998251824487">"Eliminar"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Eliminar"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Eliminar item selecionado?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Eliminar itens selecionados?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Partilhar"</string>
     <string name="select_all" msgid="3403283025220282175">"Selecionar tudo"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Rodar para a direita"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Não foi possível encontrar item."</string>
     <string name="edit" msgid="1502273844748580847">"Editar"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Nenhuma aplicação disponível para concluir a ação."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"A processar pedidos de colocação em cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"A col. cache..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Recortar"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Sem etiqueta"</string>
     <string name="no_location" msgid="4043624857489331676">"Sem localização"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Não foi possível identificar algumas localizações devido a problemas de rede."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Falha ao transferir as fotografias deste álbum. Tente novamente mais tarde."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Falha ao transferir a lista de álbuns. Tente novamente mais tarde."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Não foi possível transferir as fotografias deste álbum. Tente novamente mais tarde."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Apenas imagens"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Apenas vídeos"</string>
-    <string name="show_all" msgid="4780647751652596980">"Imagens e vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imagens e vídeos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotografias"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Não existem fotografias."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Imagem recortada guardada em Transferências."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"A imagem recortada não foi guardada."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Imagem recortada guardada em <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nenhum álbum disponível."</string>
     <string name="empty_album" msgid="4542880442593595494">"0 imagens/vídeos disponíveis."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Mensagens"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
     <string name="settings" msgid="1534847740615665736">"Definições"</string>
     <string name="add_account" msgid="4271217504968243974">"Adicionar conta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Câmara"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Transferidas"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importada"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captura de ecrã"</string>
+    <string name="help" msgid="7368960711153618354">"Ajuda"</string>
 </resources>
diff --git a/res/values-pt/photoeditor_strings.xml b/res/values-pt/photoeditor_strings.xml
index 664c9a5..8c8a28b 100644
--- a/res/values-pt/photoeditor_strings.xml
+++ b/res/values-pt/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Não foi possível carregar a foto"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Não é possível salvar a foto"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Foto salva no álbum <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Editado"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Salvar foto editada?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Não foi possível carregar a foto"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Não foi possível salvar a foto editada"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"A foto editada foi salva em <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Descartar as alterações não salvas?"</string>
     <string name="yes" msgid="5402582493291792293">"Sim"</string>
-    <string name="no" msgid="5595408018304861875">"Não"</string>
     <string name="save" msgid="5516670392524294967">"SALVAR"</string>
     <string name="autofix" msgid="1663414996270538748">"Correção autom."</string>
     <string name="crop" msgid="7598378507763334041">"Cortar"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Arraste os marcadores para cortar a foto"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Desenhe na foto para rabiscar"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Arraste a foto para invertê-la"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Toque para remover olhos vermelhos"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Toque nos olhos vermelhos para removê-los"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Arraste a foto para girá-la"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Arraste a foto para endireitá-la"</string>
 </resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index a5fce6c..b7a0140 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string>
     <string name="loading" msgid="7038208555304563571">"Carregando..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Não foi possível carregar"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Não foi possível carregar a imagem"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Sem miniatura"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Reiniciar"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Cortar"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Toque em um rosto para começar."</string>
     <string name="saving_image" msgid="7270334453636349407">"Salvando imagem…"</string>
     <string name="save_error" msgid="6857408774183654970">"Não é possível salvar a imagem cortada."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Selecionar álbum"</string>
     <string name="select_group" msgid="6744208543323307114">"Selecionar grupo"</string>
     <string name="set_image" msgid="2331476809308010401">"Definir imagem como"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Definir plano de fundo"</string>
     <string name="wallpaper" msgid="140165383777262070">"Definindo plano de fundo..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Plano de fundo"</string>
     <string name="delete" msgid="2839695998251824487">"Excluir"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Excluir"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Excluir o item selecionado?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Excluir os itens selecionados?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmar"</string>
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Compartilhar"</string>
     <string name="select_all" msgid="3403283025220282175">"Selecionar tudo"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Girar para a direita"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Não foi possível encontrar o item."</string>
     <string name="edit" msgid="1502273844748580847">"Editar"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Não há aplicativo disponível para concluir a ação."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Processando solicitações de armazenamento em cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"Cache..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Cortar"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Sem tags"</string>
     <string name="no_location" msgid="4043624857489331676">"Nenhum local"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Alguns locais não foram identificados por problemas na rede."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Falha ao fazer download das fotos neste álbum. Tente novamente mais tarde."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Falha ao fazer download da lista de álbuns. Tente novamente mais tarde."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Não foi possível fazer download das fotos neste álbum. Tente novamente mais tarde."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Apenas imagens"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Somente vídeos"</string>
-    <string name="show_all" msgid="4780647751652596980">"Imagens e vídeos"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imagens e vídeos"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotos"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Nenhuma foto."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"A imagem cortada foi salva em Downloads."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"A imagem cortada não foi salva."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"A imagem cortada foi salva em <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nenhum álbum disponível."</string>
     <string name="empty_album" msgid="4542880442593595494">"0 imagens/vídeos disponíveis."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Postagens"</string>
@@ -155,11 +159,16 @@
     <string name="widget_type" msgid="1364653978966343448">"Escolher imagens"</string>
     <string name="slideshow_dream_name" msgid="6915963319933437083">"Apresent. de slides"</string>
     <string name="albums" msgid="7320787705180057947">"Álbuns"</string>
-    <string name="times" msgid="2023033894889499219">"Vezes"</string>
+    <string name="times" msgid="2023033894889499219">"Data e hora"</string>
     <string name="locations" msgid="6649297994083130305">"Locais"</string>
     <string name="people" msgid="4114003823747292747">"Pessoas"</string>
     <string name="tags" msgid="5539648765482935955">"Etiquetas"</string>
     <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
     <string name="settings" msgid="1534847740615665736">"Configurações"</string>
     <string name="add_account" msgid="4271217504968243974">"Adicionar conta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Câmera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Downloads"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importado"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Capturas de tela"</string>
+    <string name="help" msgid="7368960711153618354">"Ajuda"</string>
 </resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
index 4cf5c1f..ffb6ef7 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -34,10 +34,14 @@
     <skip />
     <!-- no translation found for fail_to_load (8394392853646664505) -->
     <skip />
+    <!-- no translation found for fail_to_load_image (6155388718549782425) -->
+    <skip />
     <!-- no translation found for no_thumbnail (284723185546429750) -->
     <skip />
     <string name="resume_playing_restart" msgid="5471008499835769292">"Cumenzar"</string>
-    <!-- no translation found for crop_save_text (4972430481677741184) -->
+    <!-- no translation found for crop_save_text (152200178986698300) -->
+    <skip />
+    <!-- no translation found for ok (5296833083983263293) -->
     <skip />
     <!-- no translation found for multiface_crop_help (2554690102655855657) -->
     <skip />
@@ -56,11 +60,15 @@
     <!-- no translation found for select_group (6744208543323307114) -->
     <skip />
     <string name="set_image" msgid="2331476809308010401">"Definir il maletg sco"</string>
+    <!-- no translation found for set_wallpaper (8491121226190175017) -->
+    <skip />
     <!-- no translation found for wallpaper (140165383777262070) -->
     <skip />
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Culissa"</string>
     <string name="delete" msgid="2839695998251824487">"Stizzar"</string>
-    <!-- no translation found for confirm_delete (7358770022173660511) -->
+    <!-- no translation found for delete_selection:one (6453379735401083732) -->
+    <!-- no translation found for delete_selection:other (5874316486520635333) -->
+    <!-- no translation found for confirm (8646870096527848520) -->
     <skip />
     <string name="cancel" msgid="3637516880917356226">"Interrumper"</string>
     <string name="share" msgid="3619042788254195341">"Cundivider"</string>
@@ -92,8 +100,6 @@
     <skip />
     <!-- no translation found for edit (1502273844748580847) -->
     <skip />
-    <!-- no translation found for activity_not_found (5619154886080878023) -->
-    <skip />
     <!-- no translation found for process_caching_requests (8722939570307386071) -->
     <skip />
     <!-- no translation found for caching_label (4521059045896269095) -->
@@ -121,23 +127,19 @@
     <skip />
     <!-- no translation found for no_connectivity (7164037617297293668) -->
     <skip />
-    <!-- no translation found for sync_album_error (2218733298953719785) -->
-    <skip />
-    <!-- no translation found for sync_album_set_error (9016732535181154028) -->
+    <!-- no translation found for sync_album_error (1020688062900977530) -->
     <skip />
     <!-- no translation found for show_images_only (7263218480867672653) -->
     <skip />
     <!-- no translation found for show_videos_only (3850394623678871697) -->
     <skip />
-    <!-- no translation found for show_all (4780647751652596980) -->
+    <!-- no translation found for show_all (6963292714584735149) -->
     <skip />
     <!-- no translation found for appwidget_title (6410561146863700411) -->
     <skip />
     <!-- no translation found for appwidget_empty_text (1228925628357366957) -->
     <skip />
-    <!-- no translation found for crop_saved (1062612625032731770) -->
-    <skip />
-    <!-- no translation found for crop_not_saved (3400773981839556) -->
+    <!-- no translation found for crop_saved (1595985909779105158) -->
     <skip />
     <!-- no translation found for no_albums_alert (4111744447491690896) -->
     <skip />
@@ -256,4 +258,14 @@
     <string name="settings" msgid="1534847740615665736">"Parameters"</string>
     <!-- no translation found for add_account (4271217504968243974) -->
     <skip />
+    <!-- no translation found for folder_camera (4714658994809533480) -->
+    <skip />
+    <!-- no translation found for folder_download (7186215137642323932) -->
+    <skip />
+    <!-- no translation found for folder_imported (2773581395524747099) -->
+    <skip />
+    <!-- no translation found for folder_screenshot (7200396565864213450) -->
+    <skip />
+    <!-- no translation found for help (7368960711153618354) -->
+    <skip />
 </resources>
diff --git a/res/values-ro/photoeditor_strings.xml b/res/values-ro/photoeditor_strings.xml
index f4c23ec..3967449 100644
--- a/res/values-ro/photoeditor_strings.xml
+++ b/res/values-ro/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Studio foto"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotografia nu poate fi încărcată"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotografia nu poate fi salvată"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotografie salvată în albumul <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Editate"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Salvaţi fotografia editată?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotografia nu a putut fi încărcată"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Fotografia editată nu a putut fi salvată"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Fotografia editată a fost salvată în <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Renunţaţi la modificările nesalvate?"</string>
     <string name="yes" msgid="5402582493291792293">"Da"</string>
-    <string name="no" msgid="5595408018304861875">"Nu"</string>
     <string name="save" msgid="5516670392524294967">"SALVAŢI"</string>
     <string name="autofix" msgid="1663414996270538748">"Remediere auto."</string>
     <string name="crop" msgid="7598378507763334041">"Decupaţi"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Trageţi marcatorii pentru a decupa fotografia"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Desenaţi pe fotografie pentru a crea doodle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Trageţi o fotografie pentru a o răsturna"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Apăsaţi pentru a elimina ochii roşii"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Apăsaţi pe ochii roşii pentru eliminare"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Trageţi fotografia pentru a o roti"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Trageţi de fotografie pentru a o îndrepta"</string>
 </resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 3caf4bc..d10bf6f 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Reluaţi redarea"</string>
     <string name="loading" msgid="7038208555304563571">"Se încarcă..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nu s-au putut încărca"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Nu s-a putut încărca imaginea"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Nu există o miniatură"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Începeţi din nou"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Decupaţi"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Atingeţi o faţă pentru a începe."</string>
     <string name="saving_image" msgid="7270334453636349407">"Se salvează fotografia..."</string>
     <string name="save_error" msgid="6857408774183654970">"Imaginea decupată nu s-a putut salva."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Selectaţi un album"</string>
     <string name="select_group" msgid="6744208543323307114">"Selectaţi un grup"</string>
     <string name="set_image" msgid="2331476809308010401">"Setaţi fotografia ca"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Setaţi imag. fundal"</string>
     <string name="wallpaper" msgid="140165383777262070">"Se setează imaginea de fundal..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Imagine de fundal"</string>
     <string name="delete" msgid="2839695998251824487">"Ştergeţi"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Ştergeţi"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ştergeţi articolul selectat?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Ştergeţi articolele selectate?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Confirmaţi"</string>
     <string name="cancel" msgid="3637516880917356226">"Anulaţi"</string>
     <string name="share" msgid="3619042788254195341">"Distribuiţi"</string>
     <string name="select_all" msgid="3403283025220282175">"Selectaţi-le pe toate"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Rotiţi spre dreapta"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Elementul nu a putut fi găsit."</string>
     <string name="edit" msgid="1502273844748580847">"Editaţi"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Nu există aplic. dispon. pt. a finaliza acţiunea."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Se proces. solicit. de stocare în memoria cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"Mem. cache..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Decupaţi"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Neetichetate"</string>
     <string name="no_location" msgid="4043624857489331676">"Fără locaţie"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Unele locaţii nu au putut fi identificate din cauza unor probleme de reţea."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Descărcarea fotografiilor din acest album a eşuat. Încercaţi din nou mai târziu."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Descărcarea listei de albume a eşuat. Încercaţi din nou mai târziu."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Descărcarea fotografiilor din acest album a eşuat. Încercaţi din nou mai târziu."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Numai imagini"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Numai videoclipuri"</string>
-    <string name="show_all" msgid="4780647751652596980">"Imagini şi videoclipuri"</string>
+    <string name="show_all" msgid="6963292714584735149">"Imagini şi videoclipuri"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Galerie foto"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Nicio fotografie."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Imaginea decupată a fost salvată la Descărcări."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Imaginea decupată nu a fost salvată."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Imaginea decupată a fost salvată în <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nu există albume disponibile."</string>
     <string name="empty_album" msgid="4542880442593595494">"Nu există imagini/videoclipuri disponibile."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Postări"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupaţi după"</string>
     <string name="settings" msgid="1534847740615665736">"Setări"</string>
     <string name="add_account" msgid="4271217504968243974">"Adăugaţi un cont"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Cameră foto"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Descărcate"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importate"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Captură de ecran"</string>
+    <string name="help" msgid="7368960711153618354">"Ajutor"</string>
 </resources>
diff --git a/res/values-ru/photoeditor_strings.xml b/res/values-ru/photoeditor_strings.xml
index 312ae43..af7f91f 100644
--- a/res/values-ru/photoeditor_strings.xml
+++ b/res/values-ru/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Фотостудия"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Не удалось загрузить фото"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Не удалось сохранить фото"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Фотография сохранена в альбоме \"<xliff:g id="ALBUM_NAME">%s</xliff:g>\""</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Измененные"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Сохранить изменения?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Не удалось загрузить фотографию"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Не удалось сохранить изменения"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Измененная фотография сохранена в папке <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Отменить несохраненные изменения?"</string>
     <string name="yes" msgid="5402582493291792293">"Да"</string>
-    <string name="no" msgid="5595408018304861875">"Нет"</string>
     <string name="save" msgid="5516670392524294967">"ГОТОВО"</string>
     <string name="autofix" msgid="1663414996270538748">"Авто"</string>
     <string name="crop" msgid="7598378507763334041">"Кадрировать"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Чтобы кадрировать фото, перетащите маркеры"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Рисуйте на фото пальцем"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Чтобы отразить фото, перетащите его край"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Нажмите для устранения эффекта красных глаз"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Нажмите на красные глаза, чтобы убрать их"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Чтобы повернуть фото, перетащите его край"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Чтобы выровнять фото, перетащите его"</string>
 </resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 0413826..a9a7800 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Продолжить воспроизведение"</string>
     <string name="loading" msgid="7038208555304563571">"Идет загрузка…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Не загружено"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Не удалось загрузить изображение"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Нет уменьшенного изображения"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Начать с начала"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Кадрировать"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ОК"</string>
+    <string name="ok" msgid="5296833083983263293">"ОК"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Нажмите на лицо, чтобы начать."</string>
     <string name="saving_image" msgid="7270334453636349407">"Сохранение картинки..."</string>
     <string name="save_error" msgid="6857408774183654970">"Изображение не сохранено."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Выбрать альбомы"</string>
     <string name="select_group" msgid="6744208543323307114">"Выбрать группы"</string>
     <string name="set_image" msgid="2331476809308010401">"Установить картинку как"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Установить обои"</string>
     <string name="wallpaper" msgid="140165383777262070">"Установка обоев..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Обои"</string>
     <string name="delete" msgid="2839695998251824487">"Удалить"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Удалить"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Удалить выбранный файл?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Удалить выбранные файлы?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Подтвердить"</string>
     <string name="cancel" msgid="3637516880917356226">"Отмена"</string>
     <string name="share" msgid="3619042788254195341">"Отправить"</string>
     <string name="select_all" msgid="3403283025220282175">"Выбрать все"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Повернуть вправо"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Элемент не найден."</string>
     <string name="edit" msgid="1502273844748580847">"Изменить"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Действие не поддерживается ни в одном приложении."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Обработка запросов на кэширование"</string>
     <string name="caching_label" msgid="4521059045896269095">"Кэширование..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Кадрировать"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Без тегов"</string>
     <string name="no_location" msgid="4043624857489331676">"Место не указано"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Не удалось найти все места из-за проблем с подключением."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Не удалось загрузить фотографии. Повторите попытку позже."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Не удалось загрузить альбомы. Повторите попытку позже."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Не удалось загрузить фото. Повторите попытку позже."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Только изображения"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Только видео"</string>
-    <string name="show_all" msgid="4780647751652596980">"Изображения и видео"</string>
+    <string name="show_all" msgid="6963292714584735149">"Изображения и видео"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерея"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Нет фотографий"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Изображение сохранено в папке \"Загрузки\"."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Изображение не сохранено."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Обрезанное изображение сохранено в папке <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Нет доступных альбомов."</string>
     <string name="empty_album" msgid="4542880442593595494">"Нет фото/видео"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Записи"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Сгруппировать"</string>
     <string name="settings" msgid="1534847740615665736">"Настройки"</string>
     <string name="add_account" msgid="4271217504968243974">"Добавить аккаунт"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Загруженные"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Импортированные"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Скриншоты"</string>
+    <string name="help" msgid="7368960711153618354">"Справка"</string>
 </resources>
diff --git a/res/values-sk/photoeditor_strings.xml b/res/values-sk/photoeditor_strings.xml
index c5c2684..2b563f4 100644
--- a/res/values-sk/photoeditor_strings.xml
+++ b/res/values-sk/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotografiu sa nepodarilo načítať"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotografiu sa nepodarilo uložiť"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotografia bola uložená do albumu <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Upravené"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Chcete upravenú fotografiu uložiť?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotografiu sa nepodarilo načítať"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Upravenú fotografiu sa nepodarilo uložiť"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Uprav. fotografia bola ulož. do prieč. <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Zahodiť neuložené zmeny?"</string>
     <string name="yes" msgid="5402582493291792293">"Áno"</string>
-    <string name="no" msgid="5595408018304861875">"Nie"</string>
     <string name="save" msgid="5516670392524294967">"ULOŽIŤ"</string>
     <string name="autofix" msgid="1663414996270538748">"Automat. oprava"</string>
     <string name="crop" msgid="7598378507763334041">"Orezať"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Potiahnutím značiek orežete fotografiu"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Na fotografii môžete kresliť prstom"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Potiahnutím prstom fotografiu prevrátite"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Klepnutím odstráňte efekt červených očí"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Červené oči odstránite klepnutím na ne"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Potiahnutím prstom fotografiu otočíte"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Potiahnutím prstom fotografiu vyrovnáte"</string>
 </resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index c25ea40..bd914b9 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Obnoviť prehrávanie"</string>
     <string name="loading" msgid="7038208555304563571">"Prebieha načítavanie…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Nepodarilo sa načítať"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Obrázok sa nepodarilo načítať"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Žiadne miniatúry"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Začať odznova"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Orezať"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Začnite dotknutím sa tváre."</string>
     <string name="saving_image" msgid="7270334453636349407">"Prebieha ukladanie fotografie..."</string>
     <string name="save_error" msgid="6857408774183654970">"Orezaný obrázok sa nepodarilo uložiť."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Vybrať album"</string>
     <string name="select_group" msgid="6744208543323307114">"Vybrať skupinu"</string>
     <string name="set_image" msgid="2331476809308010401">"Fotografia bude použitá ako"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nastaviť tapetu"</string>
     <string name="wallpaper" msgid="140165383777262070">"Prebieha nastavenie tapety..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
     <string name="delete" msgid="2839695998251824487">"Odstrániť"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Odstrániť"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Odstrániť vybratú položku?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Odstrániť vybraté položky?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potvrdiť"</string>
     <string name="cancel" msgid="3637516880917356226">"Zrušiť"</string>
     <string name="share" msgid="3619042788254195341">"Zdieľať"</string>
     <string name="select_all" msgid="3403283025220282175">"Vybrať všetko"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Otočiť doprava"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Položku sa nepodarilo nájsť."</string>
     <string name="edit" msgid="1502273844748580847">"Upraviť"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Na dokončenie akcie chýba aplikácia."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Spracovávanie žiadostí o ulož. do vyrovnáv. pamäte"</string>
     <string name="caching_label" msgid="4521059045896269095">"Do pamäte..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Orezať"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Neoznačené"</string>
     <string name="no_location" msgid="4043624857489331676">"Bez údajov o polohe"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Niektoré umiestnenia nebolo možné identifikovať kvôli problémom so sieťou."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Fotografie z tohto albumu sa nepodarilo prevziať. Skúste to znova neskôr."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Zoznam albumov sa nepodarilo prevziať. Skúste to znova neskôr."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Fotografie z tohto albumu sa nepodarilo prevziať. Skúste to znova neskôr."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Iba obrázky"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Iba videá"</string>
-    <string name="show_all" msgid="4780647751652596980">"Obrázky a videá"</string>
+    <string name="show_all" msgid="6963292714584735149">"Obrázky a videá"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogaléria"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Žiadne fotografie."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Orez. obrázok bol uložený do Prevzatých súborov."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Orezaný obrázok nebol uložený."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Orezaný obrázok bol uložený do priečinka <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Nie sú k dispozícii žiadne albumy."</string>
     <string name="empty_album" msgid="4542880442593595494">"Počet obrázkov / videí k dispozícii: 0"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Príspevky"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Zoskupiť podľa"</string>
     <string name="settings" msgid="1534847740615665736">"Nastavenia"</string>
     <string name="add_account" msgid="4271217504968243974">"Pridať účet"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparát"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Preberanie"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importované"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Snímka obrazovky"</string>
+    <string name="help" msgid="7368960711153618354">"Pomocník"</string>
 </resources>
diff --git a/res/values-sl/photoeditor_strings.xml b/res/values-sl/photoeditor_strings.xml
index 802cb07..10a6e82 100644
--- a/res/values-sl/photoeditor_strings.xml
+++ b/res/values-sl/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Slike ni mogoče prenesti"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotografije ni mogoče shraniti"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotografija je bila shranjena v album <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Urejeno"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Želite shraniti urejene fotografije?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotografije ni mogoče naložiti"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Urejene fotografije ni mogoče shraniti"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Urejena fotografija je shranjena v mapi <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Želite zavreči neshranjene spremembe?"</string>
     <string name="yes" msgid="5402582493291792293">"Da"</string>
-    <string name="no" msgid="5595408018304861875">"Ne"</string>
     <string name="save" msgid="5516670392524294967">"SHRANI"</string>
     <string name="autofix" msgid="1663414996270538748">"Samod. poprav."</string>
     <string name="crop" msgid="7598378507763334041">"Obrezovanje"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Povlecite oznake, da obrežete fotografijo"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Povlecite po fotografiji za prostoročno risanje"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Povlecite fotografijo, da jo obrnete"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Tapnite, da odstranite rdeče oči"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Rdeče oči odstranite z dotikom"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Povlecite fotografijo, da jo zavrtite"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Povlecite fotografijo, da jo poravnate"</string>
 </resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 39c7266..fb20120 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Nadaljuj predvajanje"</string>
     <string name="loading" msgid="7038208555304563571">"Prenos …"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Ni bilo mogoče naložiti"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Slike ni bilo mogoče naložiti"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Ni sličice"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Začni znova"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Obreži"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"V redu"</string>
+    <string name="ok" msgid="5296833083983263293">"V redu"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Dotaknite se obraza, če želite začeti."</string>
     <string name="saving_image" msgid="7270334453636349407">"Shranjevanje slike ..."</string>
     <string name="save_error" msgid="6857408774183654970">"Obrezane slike ni bilo mogoče shraniti."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Izberite album"</string>
     <string name="select_group" msgid="6744208543323307114">"Izberite skupino"</string>
     <string name="set_image" msgid="2331476809308010401">"Nastavi sliko kot"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Nastavi ozadje"</string>
     <string name="wallpaper" msgid="140165383777262070">"Nastavljanje slike za ozadje ..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Slika za ozadje"</string>
     <string name="delete" msgid="2839695998251824487">"Izbriši"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Izbriši"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Želite izbrisati ta element?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Želite izbrisati te elemente?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Potrdi"</string>
     <string name="cancel" msgid="3637516880917356226">"Prekliči"</string>
     <string name="share" msgid="3619042788254195341">"Skupna raba"</string>
     <string name="select_all" msgid="3403283025220282175">"Izberi vse"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Zasukaj desno"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Ni mogoče najti elementa."</string>
     <string name="edit" msgid="1502273844748580847">"Urejanje"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Ni programa, ki bi dokončal dejanje."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Obdelava zahtev za predpomnjenje"</string>
     <string name="caching_label" msgid="4521059045896269095">"Predpomnjenje ..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Obrezovanje"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string>
     <string name="no_location" msgid="4043624857489331676">"Ni lokacije"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Nekaterih ​​lokacij ni bilo mogoče določiti zaradi težav v omrežju."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Napaka pri prenosu slik v tem albumu. Poskusite pozneje."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Napaka pri prenosu seznama albumov. Poskusite pozneje."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Slik v tem albumu ni mogoče prenesti. Poskusite znova pozneje."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Samo slike"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Samo videoposnetki"</string>
-    <string name="show_all" msgid="4780647751652596980">"Slike in videoposnetki"</string>
+    <string name="show_all" msgid="6963292714584735149">"Slike in videoposnetki"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerija"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Ni fotografij."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Obrezana slika je shranjena v prenose."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Obrezana slika ni bila shranjena."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Obrezana slika je shranjena v mapi <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Ni albumov."</string>
     <string name="empty_album" msgid="4542880442593595494">"Na voljo 0 slik oz. videoposnetkov."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Objave"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Razvrsti po"</string>
     <string name="settings" msgid="1534847740615665736">"Nastavitve"</string>
     <string name="add_account" msgid="4271217504968243974">"Dodaj račun"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Fotoaparat"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Prenosi"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Uvoženo"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Posnetek zaslona"</string>
+    <string name="help" msgid="7368960711153618354">"Pomoč"</string>
 </resources>
diff --git a/res/values-sr/photoeditor_strings.xml b/res/values-sr/photoeditor_strings.xml
index a4a9ab5..6ac3576 100644
--- a/res/values-sr/photoeditor_strings.xml
+++ b/res/values-sr/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Фото-студио"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Фотографија не може да се учита"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Фотографија не може да се сачува"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Фотографија је сачувана у албум <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Измењено"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Желите ли да сачувате измењену фотографију?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Није могуће учитати слику"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Није могуће сачувати измењену слику"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Измењена слика је сачувана у <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Желите да одбаците несачуване промене?"</string>
     <string name="yes" msgid="5402582493291792293">"Да"</string>
-    <string name="no" msgid="5595408018304861875">"Не"</string>
     <string name="save" msgid="5516670392524294967">"САЧУВАЈ"</string>
     <string name="autofix" msgid="1663414996270538748">"Аутоподешавање"</string>
     <string name="crop" msgid="7598378507763334041">"Опсецање"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Превуците означиваче да бисте опсекли фотографију"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Превуците слику на дудл логотип"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Превуците фотографију да бисте је окренули"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Додирните да бисте уклонили ефекат црвених очију"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Додирните црвене очи да бисте их поправили"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Превуците фотографију да бисте је ротирали"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Превуците фотографију да бисте је исправили"</string>
 </resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 341096a..ad14c08 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Настави репродукцију"</string>
     <string name="loading" msgid="7038208555304563571">"Учитавање…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Учитавање није могуће"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Није могуће учитати слику"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Нема сличице"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Започни поново"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Опсеци"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Потврди"</string>
+    <string name="ok" msgid="5296833083983263293">"Потврди"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Додирните неко лице за почетак."</string>
     <string name="saving_image" msgid="7270334453636349407">"Чување слике…"</string>
     <string name="save_error" msgid="6857408774183654970">"Није могуће сачувати опсечену слику."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Избор албума"</string>
     <string name="select_group" msgid="6744208543323307114">"Избор групе"</string>
     <string name="set_image" msgid="2331476809308010401">"Постављање слике као"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Подешавање позадине"</string>
     <string name="wallpaper" msgid="140165383777262070">"Подешавање позадине..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Позадина"</string>
     <string name="delete" msgid="2839695998251824487">"Избриши"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Избриши"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Бришете изабрану ставку?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Бришете изабране ставке?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Потврди"</string>
     <string name="cancel" msgid="3637516880917356226">"Откажи"</string>
     <string name="share" msgid="3619042788254195341">"Дели"</string>
     <string name="select_all" msgid="3403283025220282175">"Изабери све"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Ротирај удесно"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Није могуће пронаћи ставку."</string>
     <string name="edit" msgid="1502273844748580847">"Измени"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Нема доступних апликација за довршавање радње."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Обрада захтева за кеширање"</string>
     <string name="caching_label" msgid="4521059045896269095">"Кеширање..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Опсеци"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Није означено"</string>
     <string name="no_location" msgid="4043624857489331676">"Без локације"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Није могуће идентификовати неке локације због проблема са мрежом."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Преузимање фотографија из овог албума није успело. Покушајте поново касније."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Преузимање листе албума није успело. Покушајте поново касније."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Није могуће преузети фотографије у овом албуму. Покушајте поново касније."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Само слике"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Само видео снимци"</string>
-    <string name="show_all" msgid="4780647751652596980">"Слике и видео снимци"</string>
+    <string name="show_all" msgid="6963292714584735149">"Слике и видео снимци"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Фото-галерија"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Нема фотографија."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Опсечена слика је сачувана у Преузимањима."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Опсечена слика није сачувана."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Опсечена слика је сачувана у директоријуму <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Ниједан албум није доступан."</string>
     <string name="empty_album" msgid="4542880442593595494">"Доступно је 0 слика/видео снимака."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Постови"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Групиши према"</string>
     <string name="settings" msgid="1534847740615665736">"Подешавања"</string>
     <string name="add_account" msgid="4271217504968243974">"Додавање налога"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Камера"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Преузето"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Увезено"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Снимак екрана"</string>
+    <string name="help" msgid="7368960711153618354">"Помоћ"</string>
 </resources>
diff --git a/res/values-sv/photoeditor_strings.xml b/res/values-sv/photoeditor_strings.xml
index bf82cf5..e1d9a00 100644
--- a/res/values-sv/photoeditor_strings.xml
+++ b/res/values-sv/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Fotostudio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotot kan inte läsas in"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotot kan inte sparas"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotot sparades i albumet <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Redigerade"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Spara redigerat foto?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Det gick inte att läsa in bilden"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Det gick inte att spara redigerad bild"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Den redigerade bilden sparas i <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Vill du ignorera ändringar som inte har sparats?"</string>
     <string name="yes" msgid="5402582493291792293">"Ja"</string>
-    <string name="no" msgid="5595408018304861875">"Nej"</string>
     <string name="save" msgid="5516670392524294967">"SPARA"</string>
     <string name="autofix" msgid="1663414996270538748">"Åtgärda automatiskt"</string>
     <string name="crop" msgid="7598378507763334041">"Beskär"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Dra markörerna om du vill beskära bilden"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Rita på bilden om du vill"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Tryck och dra bilden om du vill vända den"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Knacka lätt om du vill ta bort röda ögon"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Knacka lätt på röda ögon för att ta bort"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Tryck och dra bilden om du vill rotera den"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Tryck och dra bilden om du vill räta upp den"</string>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 59f8a8f..c3a0efa 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsätt spela upp"</string>
     <string name="loading" msgid="7038208555304563571">"Läser in..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Kunde inte läsas in"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Det gick inte att läsa in bilden"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniatyrbild"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Börja om"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Beskär"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Tryck på ett ansikte när du vill börja."</string>
     <string name="saving_image" msgid="7270334453636349407">"Sparar bild…"</string>
     <string name="save_error" msgid="6857408774183654970">"Det gick inte att spara den beskurna bilden."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Välj ett album"</string>
     <string name="select_group" msgid="6744208543323307114">"Välj en grupp"</string>
     <string name="set_image" msgid="2331476809308010401">"Använd bild som"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Välj bakgrund"</string>
     <string name="wallpaper" msgid="140165383777262070">"Bakgrund ställs in ..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrund"</string>
     <string name="delete" msgid="2839695998251824487">"Ta bort"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Ta bort"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Ta bort markerat objekt?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Radera markerade objekt?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Bekräfta"</string>
     <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
     <string name="share" msgid="3619042788254195341">"Dela"</string>
     <string name="select_all" msgid="3403283025220282175">"Markera alla"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Rotera åt höger"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Det gick inte att hitta objektet."</string>
     <string name="edit" msgid="1502273844748580847">"Redigera"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Det finns ingen app för att slutföra åtgärden."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Begäran om cachelagring bearbetas"</string>
     <string name="caching_label" msgid="4521059045896269095">"Cachelagrar ..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Beskär"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Saknar etikett"</string>
     <string name="no_location" msgid="4043624857489331676">"Ingen plats"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Det gick inte att identifiera vissa platser på grund av nätverksproblem."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Det gick inte att hämta fotona i albumet. Försök igen senare."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Det gick inte att hämta albumlistan. Försök igen senare."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Det gick inte att hämta bilderna i albumet. Försök igen senare."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Endast bilder"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Endast video"</string>
-    <string name="show_all" msgid="4780647751652596980">"Bilder och videor"</string>
+    <string name="show_all" msgid="6963292714584735149">"Bilder och videor"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotogalleri"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Inga foton."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Den beskurna bilden sparades i Hämtningar."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Den beskurna bilden sparades inte."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Den beskurna bilden har sparats i <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Det finns inga album."</string>
     <string name="empty_album" msgid="4542880442593595494">"Det finns inga bilder/videor."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Inlägg"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Ordna efter"</string>
     <string name="settings" msgid="1534847740615665736">"Inställningar"</string>
     <string name="add_account" msgid="4271217504968243974">"Lägg till konto"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Hämtat"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Importerat"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Skärmbild"</string>
+    <string name="help" msgid="7368960711153618354">"Hjälp"</string>
 </resources>
diff --git a/res/values-sw/photoeditor_strings.xml b/res/values-sw/photoeditor_strings.xml
index c48e0d6..d8d1b82 100644
--- a/res/values-sw/photoeditor_strings.xml
+++ b/res/values-sw/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Studio ya Picha"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Picha haiwezi kkupakizwa"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Picha haiwezi kuhifadhiwa"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Picha imehifadhiwa kwenye albamu <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Imehaririwa"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Hifadhi picha zilizohaririwa"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Haikuweza kupakia picha"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Haikuweza kuhifadhi picha iliyohaririwa"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Picha iliyohaririwa imehifadhiwa kwa <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Tupa mabadiliko yasiyohifadhiwa?"</string>
     <string name="yes" msgid="5402582493291792293">"Ndio"</string>
-    <string name="no" msgid="5595408018304861875">"Hakuna"</string>
     <string name="save" msgid="5516670392524294967">"HIFADHI"</string>
     <string name="autofix" msgid="1663414996270538748">"Weka kiotomatiki"</string>
     <string name="crop" msgid="7598378507763334041">"Kata"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Kokota viweka alama ili kupuna picha"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Chora kwenye picha ku doodle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Kokota picha ili kuiflipu"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Gonja ili kuondoa macho mekundu"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Gonga kwenye macho mekundu ili kuyaondoa"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"kokota picha ili kuizungusha"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Kokota picha ili kuizungusha"</string>
 </resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 6058d5c..772ce63 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Endelea kucheza"</string>
     <string name="loading" msgid="7038208555304563571">"Inapakia…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Imeshindwa kupakia."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Haikuweza kupakia picha"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Hakuna kijipicha"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Anza tena"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Pogoa"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Sawa"</string>
+    <string name="ok" msgid="5296833083983263293">"Sawa"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Gusa sura ili kuanza."</string>
     <string name="saving_image" msgid="7270334453636349407">"Inahifadhi picha…"</string>
     <string name="save_error" msgid="6857408774183654970">"Haiwezi kuhifadhi picha iliyopogolewa."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Chagua albamu"</string>
     <string name="select_group" msgid="6744208543323307114">"Chagua kikundi"</string>
     <string name="set_image" msgid="2331476809308010401">"Weka picha kama"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Weka mandhari"</string>
     <string name="wallpaper" msgid="140165383777262070">"Inaweka karatasi ya ukuta..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Mandhari"</string>
     <string name="delete" msgid="2839695998251824487">"Futa"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Futa"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Futa kipengee kilichoteuliwa?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Futa vipengee vilivyoteuliwa?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Thibitisha"</string>
     <string name="cancel" msgid="3637516880917356226">"Ghairi"</string>
     <string name="share" msgid="3619042788254195341">"Shiriki"</string>
     <string name="select_all" msgid="3403283025220282175">"Chagua zote"</string>
@@ -78,10 +85,9 @@
     <string name="rotate_right" msgid="6776325835923384839">"Zungusha kulia"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Hakuweza kupata kipengee."</string>
     <string name="edit" msgid="1502273844748580847">"Hariri"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Hakuna prog inayopatikana ili kukamilisha tendo hilo."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Maombi ya kuakibisha michakato"</string>
     <string name="caching_label" msgid="4521059045896269095">"Inaakibisha..."</string>
-    <string name="crop_action" msgid="3427470284074377001">"Puna"</string>
+    <string name="crop_action" msgid="3427470284074377001">"Punguza"</string>
     <string name="set_as" msgid="3636764710790507868">"Weka kama"</string>
     <string name="video_err" msgid="7003051631792271009">"Video haiwezi kuchezwa."</string>
     <string name="group_by_location" msgid="316641628989023253">"Kwa mahali"</string>
@@ -93,15 +99,13 @@
     <string name="untagged" msgid="7281481064509590402">"Ondoa lebo"</string>
     <string name="no_location" msgid="4043624857489331676">"Hakuna Mahali"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Baadhi ya maeneo haikuweza kutambuliwa kutokana na matatizo ya mtandao."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Imeshindwa kupakua picha katika albamu hii. Tafadhali Jaribu tena baadaye."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Imeshindwa kupakua orodha ya albamu. Tafadhali Jaribu tena baadaye."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Haikuwezi kupakua picha zilizo kwenye albamu hii. Jaribu tena baadaye."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Picha tu"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Video tu"</string>
-    <string name="show_all" msgid="4780647751652596980">"Picha na video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Picha &amp; video"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Matunzio ya picha"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Hakuna picha."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Picha iliyopunguzwa imehifadhiwa kwa Vipakuzi."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Picha iliyopunguzwa haijahifadhiwa"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Picha iliyopunguzwa imehifadhiwa kwa <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Hakuna albamu zinazopatikana."</string>
     <string name="empty_album" msgid="4542880442593595494">"picha / video zilizopo 0."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Zilizowekwa"</string>
@@ -165,4 +169,9 @@
     <string name="group_by" msgid="4308299657902209357">"Panga kwa kikundi na"</string>
     <string name="settings" msgid="1534847740615665736">"Mipangilio"</string>
     <string name="add_account" msgid="4271217504968243974">"Ongeza akaunti"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Pakua"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Zilizoletwa"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Picha kiwamba"</string>
+    <string name="help" msgid="7368960711153618354">"Msaada"</string>
 </resources>
diff --git a/res/values-sw320dp/photoeditor_dimens.xml b/res/values-sw320dp/photoeditor_dimens.xml
index ff76e25..a7d6a24 100755
--- a/res/values-sw320dp/photoeditor_dimens.xml
+++ b/res/values-sw320dp/photoeditor_dimens.xml
@@ -27,9 +27,11 @@
     <dimen name="action_bar_icon_padding_right">5dp</dimen>
     <dimen name="action_button_padding_horizontal">13dp</dimen>
     <dimen name="effects_menu_container_width">320dp</dimen>
-    <dimen name="effect_tool_panel_padding">10dp</dimen>
-    <dimen name="seekbar_width">290dp</dimen>
-    <dimen name="seekbar_height">27dp</dimen>
-    <dimen name="seekbar_margin_bottom">3dp</dimen>
+    <dimen name="effect_tool_panel_padding_top">3dp</dimen>
+    <dimen name="effect_tool_panel_padding_bottom">8dp</dimen>
+    <dimen name="seekbar_width">350dp</dimen>
+    <dimen name="seekbar_height">43dp</dimen>
+    <dimen name="seekbar_padding_horizontal">30dp</dimen>
+    <dimen name="seekbar_padding_vertical">8dp</dimen>
     <dimen name="crop_indicator_size">35dp</dimen>
 </resources>
diff --git a/res/values-sw600dp/photoeditor_dimens.xml b/res/values-sw600dp/photoeditor_dimens.xml
index 29d7f53..b861150 100755
--- a/res/values-sw600dp/photoeditor_dimens.xml
+++ b/res/values-sw600dp/photoeditor_dimens.xml
@@ -29,9 +29,11 @@
     <dimen name="action_button_padding_vertical">8dp</dimen>
     <dimen name="action_button_padding_horizontal">22dp</dimen>
     <dimen name="effects_menu_container_width">400dp</dimen>
-    <dimen name="effect_tool_panel_padding">13dp</dimen>
-    <dimen name="seekbar_width">560dp</dimen>
-    <dimen name="seekbar_height">35dp</dimen>
-    <dimen name="seekbar_margin_bottom">4dp</dimen>
+    <dimen name="effect_tool_panel_padding_top">4dp</dimen>
+    <dimen name="effect_tool_panel_padding_bottom">11dp</dimen>
+    <dimen name="seekbar_width">640dp</dimen>
+    <dimen name="seekbar_height">57dp</dimen>
+    <dimen name="seekbar_padding_horizontal">40dp</dimen>
+    <dimen name="seekbar_padding_vertical">11dp</dimen>
     <dimen name="crop_indicator_size">43dp</dimen>
 </resources>
diff --git a/res/values-sw800dp/photoeditor_dimens.xml b/res/values-sw800dp/photoeditor_dimens.xml
index e869acf..804a8ca 100755
--- a/res/values-sw800dp/photoeditor_dimens.xml
+++ b/res/values-sw800dp/photoeditor_dimens.xml
@@ -29,9 +29,11 @@
     <dimen name="action_button_padding_vertical">8dp</dimen>
     <dimen name="action_button_padding_horizontal">28dp</dimen>
     <dimen name="effects_menu_container_width">400dp</dimen>
-    <dimen name="effect_tool_panel_padding">15dp</dimen>
-    <dimen name="seekbar_width">560dp</dimen>
-    <dimen name="seekbar_height">35dp</dimen>
-    <dimen name="seekbar_margin_bottom">5dp</dimen>
+    <dimen name="effect_tool_panel_padding_top">5dp</dimen>
+    <dimen name="effect_tool_panel_padding_bottom">13dp</dimen>
+    <dimen name="seekbar_width">640dp</dimen>
+    <dimen name="seekbar_height">61dp</dimen>
+    <dimen name="seekbar_padding_horizontal">40dp</dimen>
+    <dimen name="seekbar_padding_vertical">13dp</dimen>
     <dimen name="crop_indicator_size">48dp</dimen>
 </resources>
diff --git a/res/values-th/photoeditor_strings.xml b/res/values-th/photoeditor_strings.xml
index d39658a..7f819e8 100644
--- a/res/values-th/photoeditor_strings.xml
+++ b/res/values-th/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"สตูดิโอถ่ายภาพ"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"ไม่สามารถโหลดภาพถ่าย"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"ไม่สามารถบันทึกภาพถ่าย"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"ภาพได้ัรับการบันทึกในอัลบั้ม <xliff:g id="ALBUM_NAME">%s</xliff:g> แล้ว"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"แก้ไขแล้ว"</string>
-    <string name="save_photo" msgid="3125109368779997862">"บันทึกภาพที่แก้ไขหรือไม่"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"ไม่สามารถโหลดรูปภาพ"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"ไม่สามารถบันทึกรูปภาพที่แก้ไข"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"บันทึกภาพที่แก้ไขไว้ที่ <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"ยกเลิกการเปลี่ยนแปลงที่ยังไม่ได้บันทึก"</string>
     <string name="yes" msgid="5402582493291792293">"ใช่"</string>
-    <string name="no" msgid="5595408018304861875">"ไม่"</string>
     <string name="save" msgid="5516670392524294967">"บันทึก"</string>
     <string name="autofix" msgid="1663414996270538748">"แก้ไขอัตโนมัติ"</string>
     <string name="crop" msgid="7598378507763334041">"ตัด"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"ลากเครื่องหมายเพื่อครอบตัดภาพ"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"วาดบนภาพเพื่อ doodle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"ลากภาพเพื่อพลิก"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"แตะเพื่อลบตาแดง"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"แตะที่ตาแดงเพื่อนำออก"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"ลากภาพเพื่อหมุน"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"ลากภาพเพื่อยืดให้ตรง"</string>
 </resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 8ee83a4..2095b9b 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"เล่นต่อ"</string>
     <string name="loading" msgid="7038208555304563571">"กำลังโหลด…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"ไม่สามารถโหลด"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"ไม่สามารถโหลดภาพ"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"ไม่มีภาพขนาดย่อ"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"เริ่มต้นใหม่"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"ครอบตัด"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"ตกลง"</string>
+    <string name="ok" msgid="5296833083983263293">"ตกลง"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"แตะใบหน้าเพื่อเริ่มต้น"</string>
     <string name="saving_image" msgid="7270334453636349407">"กำลังบันทึกภาพ..."</string>
     <string name="save_error" msgid="6857408774183654970">"ไม่สามารถบันทึกภาพที่ตัดได้"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"เลือกอัลบั้ม"</string>
     <string name="select_group" msgid="6744208543323307114">"เลือกกลุ่ม"</string>
     <string name="set_image" msgid="2331476809308010401">"ตั้งค่าภาพเป็น"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"ตั้งค่าวอลเปเปอร์"</string>
     <string name="wallpaper" msgid="140165383777262070">"กำลังตั้งค่าวอลเปเปอร์..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"วอลเปเปอร์"</string>
     <string name="delete" msgid="2839695998251824487">"ลบ"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"ลบ"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"ลบรายการที่เลือกหรือไม่"</item>
+    <item quantity="other" msgid="5874316486520635333">"ลบรายการที่เลือกหรือไม่"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"ยืนยัน"</string>
     <string name="cancel" msgid="3637516880917356226">"ยกเลิก"</string>
     <string name="share" msgid="3619042788254195341">"แบ่งปัน"</string>
     <string name="select_all" msgid="3403283025220282175">"เลือกทั้งหมด"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"หมุนไปทางขวา"</string>
     <string name="no_such_item" msgid="5315144556325243400">"ไม่พบรายการ"</string>
     <string name="edit" msgid="1502273844748580847">"แก้ไข"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"ไม่มีแอปพลิเคชันสำหรับดำเนินการให้เสร็จสิ้น"</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"กำลังประมวลผลคำขอให้แคช"</string>
     <string name="caching_label" msgid="4521059045896269095">"กำลังแคช..."</string>
     <string name="crop_action" msgid="3427470284074377001">"ตัด"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"ยกเลิกการติดแท็ก"</string>
     <string name="no_location" msgid="4043624857489331676">"ไม่มีสถานที่"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"ไม่สามารถระบุบางตำแหน่งได้เนื่องจากปัญหาเกี่ยวกับเครือข่าย"</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"ไม่สามารถดาวน์โหลดรูปภาพในอัลบั้มนี้ โปรดลองอีกครั้งในภายหลัง"</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"ไม่สามารถดาวน์โหลดรายชื่ออัลบั้ม โปรดลองอีกครั้งในภายหลัง"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"ไม่สามารถดาวน์โหลดรูปภาพในอัลบั้มนี้ โปรดลองอีกครั้งในภายหลัง"</string>
     <string name="show_images_only" msgid="7263218480867672653">"เฉพาะภาพ"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"เฉพาะวิดีโอ"</string>
-    <string name="show_all" msgid="4780647751652596980">"ภาพและวิดีโอ"</string>
+    <string name="show_all" msgid="6963292714584735149">"ภาพและวิดีโอ"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"แกลเลอรีรูปภาพ"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"ไม่มีรูปภาพ"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"ภาพที่ตัดจะได้รับการบันทึกไว้ในโฟลเดอร์ดาวน์โหลด"</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"ภาพที่ตัดไม่ได้รับการบันทึก"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"บันทึกภาพที่ครอบตัดไว้ที่ <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"ไม่มีอัลบั้ม"</string>
     <string name="empty_album" msgid="4542880442593595494">"O ภาพ/วิดีโอ"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"โพสต์"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"จัดกลุ่มตาม"</string>
     <string name="settings" msgid="1534847740615665736">"การตั้งค่า"</string>
     <string name="add_account" msgid="4271217504968243974">"เพิ่มบัญชี"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"กล้องถ่ายรูป"</string>
+    <string name="folder_download" msgid="7186215137642323932">"ดาวน์โหลด"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"นำเข้าแล้ว"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"ภาพหน้าจอ"</string>
+    <string name="help" msgid="7368960711153618354">"ความช่วยเหลือ"</string>
 </resources>
diff --git a/res/values-tl/photoeditor_strings.xml b/res/values-tl/photoeditor_strings.xml
index fa547b5..4675685 100644
--- a/res/values-tl/photoeditor_strings.xml
+++ b/res/values-tl/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Hindi ma-load ang larawan"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Hindi ma-save ang larawan"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Na-save ang larawan sa album na <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Na-edit"</string>
-    <string name="save_photo" msgid="3125109368779997862">"I-save ang na-edit na larawan?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Hindi mai-load ang larawan"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Hindi mai-save ang na-edit na larawan"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Nai-save ang na-edit na larawan sa <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"I-discard ang di naka-save na pagbabago?"</string>
     <string name="yes" msgid="5402582493291792293">"Oo"</string>
-    <string name="no" msgid="5595408018304861875">"Hindi"</string>
     <string name="save" msgid="5516670392524294967">"I-SAVE"</string>
     <string name="autofix" msgid="1663414996270538748">"Auto-fix"</string>
     <string name="crop" msgid="7598378507763334041">"Pag-crop"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"I-drag ang mga marker upang i-crop ang larawan"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Gumuhit sa larawan upang mag-doodle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"I-drag ang larawan upang i-flip ito"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Tapikin upang alisin ang mga pulang mata"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Pindutin ang mga red eye upang alisin ang mga ito"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"I-drag ang larawan upang i-rotate ito"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"I-drag ang larawan upang iunat ito"</string>
 </resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index f52540b..d376a02 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Ipagpatuloy ang pag-play"</string>
     <string name="loading" msgid="7038208555304563571">"Naglo-load…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Hindi mai-load"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Hindi ma-load ang larawan"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Walang thumbnail"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Magsimula na"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"I-crop"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Pumindot ng mukha upang magsimula."</string>
     <string name="saving_image" msgid="7270334453636349407">"Nagse-save ng larawan..."</string>
     <string name="save_error" msgid="6857408774183654970">"Hindi mai-save ang na-crop na larawan."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Pumili ng album"</string>
     <string name="select_group" msgid="6744208543323307114">"Pumili ng pangkat"</string>
     <string name="set_image" msgid="2331476809308010401">"Itakda ang larawan bilang"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Itakda ang wallpaper"</string>
     <string name="wallpaper" msgid="140165383777262070">"Itinatakda ang wallpaper…"</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string>
     <string name="delete" msgid="2839695998251824487">"Tanggalin"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Tanggalin"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Tanggalin ang napiling item?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Tanggalin mga napiling item?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Kumpirmahin"</string>
     <string name="cancel" msgid="3637516880917356226">"Kanselahin"</string>
     <string name="share" msgid="3619042788254195341">"Ibahagi"</string>
     <string name="select_all" msgid="3403283025220282175">"Piliin lahat"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"I-rotate pakanan"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Hindi mahanap ang item."</string>
     <string name="edit" msgid="1502273844748580847">"I-edit"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Walang app available para makumpleto ang pagkilos."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Pinoproseso ang mga kahilingan sa pag-cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"Nagka-cache…"</string>
     <string name="crop_action" msgid="3427470284074377001">"I-crop"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Hindi naka-tag"</string>
     <string name="no_location" msgid="4043624857489331676">"Walang lokasyon"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Hindi matukoy ang ilang mga lokasyon dahil sa mga problema sa network."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Nabigong i-download ang mga larawan sa album na ito. Pakisubukang muli sa ibang pagkakataon."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Nabigong i-download ang listahan ng mga album. Pakisubukang muli sa ibang pagkakataon."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Hindi ma-download ang mga larawan sa album na ito. Pakisubukang muli sa ibang pagkakataon."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Mga larawan lamang"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Mga video lamang"</string>
-    <string name="show_all" msgid="4780647751652596980">"Mga larawan at mga video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Mga larawan &amp; video"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Photo Gallery"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Walang mga larawan."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Nai-save ang na-crop na larawan sa Mga Download."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Hindi nai-save ang na-crop na larawan."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Nai-save ang na-crop na larawan sa <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Walang available na mga album."</string>
     <string name="empty_album" msgid="4542880442593595494">"O na larawan/video ang available."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Mga Post"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Ipangkat ayon sa"</string>
     <string name="settings" msgid="1534847740615665736">"Mga Setting"</string>
     <string name="add_account" msgid="4271217504968243974">"Magdagdag ng account"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Camera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"I-download"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Na-import"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Screenshot"</string>
+    <string name="help" msgid="7368960711153618354">"Tulong"</string>
 </resources>
diff --git a/res/values-tr/photoeditor_strings.xml b/res/values-tr/photoeditor_strings.xml
index 35b9ece..e3dac66 100644
--- a/res/values-tr/photoeditor_strings.xml
+++ b/res/values-tr/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Fotoğraf Stüdyosu"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Fotoğraf yüklenemiyor"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Fotoğraf kaydedilemiyor"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Fotoğraf <xliff:g id="ALBUM_NAME">%s</xliff:g> albümüne kaydedildi"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Düzenlendi"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Düzenlenmiş fotoğraf kaydedilsin mi?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Fotoğraf yüklenemedi"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Düzenlenen fotoğraf kaydedilemedi"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Düzenlenen fotoğraf <xliff:g id="FOLDER_NAME">%s</xliff:g> içine kaydedildi"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Kaydedilmeyen değişiklikler silinsin mi?"</string>
     <string name="yes" msgid="5402582493291792293">"Evet"</string>
-    <string name="no" msgid="5595408018304861875">"Hayır"</string>
     <string name="save" msgid="5516670392524294967">"KAYDET"</string>
     <string name="autofix" msgid="1663414996270538748">"Otomatik düzelt"</string>
     <string name="crop" msgid="7598378507763334041">"Kırp"</string>
@@ -48,14 +46,14 @@
     <string name="sepia" msgid="7978093531824705601">"Sepya Tonu"</string>
     <string name="shadow" msgid="8235188588101973090">"Gölgeler"</string>
     <string name="sharpen" msgid="8449662378104403230">"Keskinleştir"</string>
-    <string name="straighten" msgid="5217801513491493491">"Düzelt"</string>
+    <string name="straighten" msgid="5217801513491493491">"Düzleştir"</string>
     <string name="temperature" msgid="1607987938521534517">"Sıcaklık"</string>
     <string name="tint" msgid="154435943863418434">"Tonlama"</string>
     <string name="vignette" msgid="7648125924662648282">"Vinyet"</string>
     <string name="crop_tooltip" msgid="8794037869706891710">"Fotoğrafı kırpmak içni işaretçileri sürükleyin"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Doodle için fotoğrafın üzerinde çizin"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Çevirmek için fotoğrafı sürükleyin"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Kırmızı gözleri kaldırmak için hafifçe vurun"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Kırmızı gözleri hafifçe vurarak düzeltin"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Döndürmek için fotoğrafı sürükleyin"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Düzleştirmek için fotoğrafı sürükleyin"</string>
 </resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index da4d8ff..190dc46 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Yürütmeyi sürdür"</string>
     <string name="loading" msgid="7038208555304563571">"Yükleniyor…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Yüklenemedi"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Resim yüklenemedi"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Küçük resim yok"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Başlat"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Kırp"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Tamam"</string>
+    <string name="ok" msgid="5296833083983263293">"Tamam"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Başlamak için bir yüze dokunun."</string>
     <string name="saving_image" msgid="7270334453636349407">"Resim kaydediliyor..."</string>
     <string name="save_error" msgid="6857408774183654970">"Kırpılmış resim kaydedilemedi."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Albüm seçin"</string>
     <string name="select_group" msgid="6744208543323307114">"Grup seçin"</string>
     <string name="set_image" msgid="2331476809308010401">"Resmi şu şekilde ayarla:"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Duvar kağıdını ayarla"</string>
     <string name="wallpaper" msgid="140165383777262070">"Duvar kağıdı ayarlanıyor..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Duvar Kağıdı"</string>
     <string name="delete" msgid="2839695998251824487">"Sil"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Sil"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Seçilen öğe silinsin mi?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Seçilen öğeler silinsin mi?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Onayla"</string>
     <string name="cancel" msgid="3637516880917356226">"İptal"</string>
     <string name="share" msgid="3619042788254195341">"Paylaş"</string>
     <string name="select_all" msgid="3403283025220282175">"Tümünü seç"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Sağa döndür"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Öğe bulunamadı."</string>
     <string name="edit" msgid="1502273844748580847">"Düzenle"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"İşlemi tamamlayacak hiç uygulama yok."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Önbelleğe alma istekleri işleniyor"</string>
     <string name="caching_label" msgid="4521059045896269095">"Önbelleğe alınıyor..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Kırp"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Etiketlenmemiş"</string>
     <string name="no_location" msgid="4043624857489331676">"Konum bilgisi yok"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Ağ sorunları nedeniyle bazı konumlar tanımlanamadı."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Bu albümdeki fotoğraflar indirilemedi. Lütfen daha sonra tekrar deneyin."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Albüm listesi indirilemedi. Lütfen daha sonra tekrar deneyin."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Bu albümdeki fotoğraflar indirilemedi. Daha sonra tekrar deneyin."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Yalnızca resimler"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Yalnızca videolar"</string>
-    <string name="show_all" msgid="4780647751652596980">"Resimler ve videolar"</string>
+    <string name="show_all" msgid="6963292714584735149">"Resimler ve videolar"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Fotoğraf Galerisi"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Fotoğraf yok."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Kırpılmış resim İndirilenler klasörüne kaydedildi."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Kırpılmış resim kaydedilmedi."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Kırpılan resim <xliff:g id="FOLDER_NAME">%s</xliff:g> klasörüne kaydedildi."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Kullanılabilir albüm yok."</string>
     <string name="empty_album" msgid="4542880442593595494">"Kullanılabilir resim/video yok."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Yayınlar"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Grupla:"</string>
     <string name="settings" msgid="1534847740615665736">"Ayarlar"</string>
     <string name="add_account" msgid="4271217504968243974">"Hesap ekle"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Kamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"İndir"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"İçe aktarıldı"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ekran görüntüsü"</string>
+    <string name="help" msgid="7368960711153618354">"Yardım"</string>
 </resources>
diff --git a/res/values-uk/photoeditor_strings.xml b/res/values-uk/photoeditor_strings.xml
index 76bb7ce..67707d5 100644
--- a/res/values-uk/photoeditor_strings.xml
+++ b/res/values-uk/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Photo Studio"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Фото неможливо завантажити"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Фото неможливо зберегти"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Фото збережено в альбом \"<xliff:g id="ALBUM_NAME">%s</xliff:g>\""</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Редаговано"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Зберегти відредаговану фотографію?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Не вдалося завантажити фотографію"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Не вдалося зберегти відредаговане фото"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Відредаговане фото збережено в папці <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Відхилити незбережені зміни?"</string>
     <string name="yes" msgid="5402582493291792293">"Так"</string>
-    <string name="no" msgid="5595408018304861875">"Ні"</string>
     <string name="save" msgid="5516670392524294967">"ЗБЕРЕГТИ"</string>
     <string name="autofix" msgid="1663414996270538748">"Автовиправлення"</string>
     <string name="crop" msgid="7598378507763334041">"Обрізка"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Перетягніть маркери, щоб обрізати фото"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Малюйте на фото, щоб створити doodle"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Потягніть за фото, щоб перевернути його"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Натисніть, щоб усунути ефект червоних очей"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Торкн. червоних очей, щоб усунути ефект"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Потягніть за фото, щоб обертати його"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Перетягніть фото, щоб вирівняти його"</string>
 </resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index de76648..1626d46 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Віднов. відтвор."</string>
     <string name="loading" msgid="7038208555304563571">"Завантаж…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Не вдалося завантажити"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Не вдалося завантажити зображення"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Немає ескізу"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Почати знову"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Обрізати"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"ОК"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Торкніться обличчя, щоб почати."</string>
     <string name="saving_image" msgid="7270334453636349407">"Зберіг-ня фото…"</string>
     <string name="save_error" msgid="6857408774183654970">"Не вдалося зберегти обрізане зображення."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Виберіть альбом"</string>
     <string name="select_group" msgid="6744208543323307114">"Виберіть групу"</string>
     <string name="set_image" msgid="2331476809308010401">"Устан. фото як"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Фонове зображення"</string>
     <string name="wallpaper" msgid="140165383777262070">"Встановлення фонового малюнка..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Фоновий мал."</string>
     <string name="delete" msgid="2839695998251824487">"Видалити"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Видалити"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Видалити вибраний елемент?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Видалити вибрані елементи?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Підтвердити"</string>
     <string name="cancel" msgid="3637516880917356226">"Скасувати"</string>
     <string name="share" msgid="3619042788254195341">"Надісл."</string>
     <string name="select_all" msgid="3403283025220282175">"Вибрати всі"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Поверн. вправо"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Не вдалося знайти елемент."</string>
     <string name="edit" msgid="1502273844748580847">"Редагувати"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Немає доступної програми, щоб виконати цю дію."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Виконується обробка запитів кешування"</string>
     <string name="caching_label" msgid="4521059045896269095">"Кешування..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Обрізати"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Без тегів"</string>
     <string name="no_location" msgid="4043624857489331676">"Без місцезнаходження"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Не вдалося визначити деякі місцезнаходження через проблеми з мережею."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Не вдалося завантажити фото цього альбому. Повторіть спробу пізніше."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Не вдалося завантажити список альбомів. Повторіть спробу пізніше."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Не вдалося завантажити фото цього альбому. Повторіть спробу пізніше."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Лише зображення"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Лише відео"</string>
-    <string name="show_all" msgid="4780647751652596980">"Зображення та відео"</string>
+    <string name="show_all" msgid="6963292714584735149">"Зображення й відео"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерея"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Фотографій немає."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Обрізане зображення збережено в папці завантажень."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Обрізане зображення не збережено."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Обрізане зображення збережено в папці <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Немає доступних альбомів."</string>
     <string name="empty_album" msgid="4542880442593595494">"Немає доступних зображень або відео."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Публікації"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Групувати за"</string>
     <string name="settings" msgid="1534847740615665736">"Налаштування"</string>
     <string name="add_account" msgid="4271217504968243974">"Додати обліковий запис"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"З камери"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Звантаження"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Імпортовані"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Знімки екрана"</string>
+    <string name="help" msgid="7368960711153618354">"Довідка"</string>
 </resources>
diff --git a/res/values-vi/photoeditor_strings.xml b/res/values-vi/photoeditor_strings.xml
index c3f8281..82779c3 100644
--- a/res/values-vi/photoeditor_strings.xml
+++ b/res/values-vi/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Studio ảnh"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Không thể tải ảnh"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Không thể lưu ảnh"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Ảnh đã được lưu vào album <xliff:g id="ALBUM_NAME">%s</xliff:g>"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Đã chỉnh sửa"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Lưu ảnh đã chỉnh sửa?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Không thể tải ảnh"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Không thể lưu ảnh đã chỉnh sửa"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Đã lưu ảnh chỉnh sửa vào <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Hủy các thay đổi chưa lưu?"</string>
     <string name="yes" msgid="5402582493291792293">"Có"</string>
-    <string name="no" msgid="5595408018304861875">"Không"</string>
     <string name="save" msgid="5516670392524294967">"LƯU"</string>
     <string name="autofix" msgid="1663414996270538748">"T.động sửa"</string>
     <string name="crop" msgid="7598378507763334041">"Cắt"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Kéo các điểm đánh dấu để cắt ảnh"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Kéo trên ảnh để vẽ ngoạch ngoạc"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Kéo ảnh để lật"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Chạm để loại bỏ mắt đỏ"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Chạm vào mắt đỏ để xóa hiệu ứng mắt đỏ"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Kéo ảnh để xoay"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Kéo ảnh để sắp thẳng"</string>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 1cbf0bd..72457c6 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Tiếp tục phát"</string>
     <string name="loading" msgid="7038208555304563571">"Đang tải…"</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Không thể tải"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Không thể tải hình ảnh"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Không có hình thu nhỏ"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Bắt đầu lại"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Cắt"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"OK"</string>
+    <string name="ok" msgid="5296833083983263293">"OK"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Chạm vào một khuôn  mặt để bắt đầu."</string>
     <string name="saving_image" msgid="7270334453636349407">"Đang lưu ảnh…"</string>
     <string name="save_error" msgid="6857408774183654970">"Không thể lưu hình ảnh được cắt."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Chọn album"</string>
     <string name="select_group" msgid="6744208543323307114">"Chọn nhóm"</string>
     <string name="set_image" msgid="2331476809308010401">"Đặt ảnh làm"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Đặt hình nền"</string>
     <string name="wallpaper" msgid="140165383777262070">"Đang đặt hình nền..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Hình nền"</string>
     <string name="delete" msgid="2839695998251824487">"Xóa"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Xóa"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Xóa mục đã chọn?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Xóa các mục đã chọn?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Xác nhận"</string>
     <string name="cancel" msgid="3637516880917356226">"Hủy"</string>
     <string name="share" msgid="3619042788254195341">"Chia sẻ"</string>
     <string name="select_all" msgid="3403283025220282175">"Chọn tất cả"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Xoay phải"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Không thể tìm thấy mục."</string>
     <string name="edit" msgid="1502273844748580847">"Chỉnh sửa"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Không có ứng dụng nào để hoàn tất tác vụ."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Đang xử lý yêu cầu lưu vào bộ nhớ cache"</string>
     <string name="caching_label" msgid="4521059045896269095">"Lưu cache..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Cắt"</string>
@@ -90,19 +96,17 @@
     <string name="untagged" msgid="7281481064509590402">"Không được gắn thẻ"</string>
     <string name="no_location" msgid="4043624857489331676">"Không có vị trí nào"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Không thể xác định một số vị trí do sự cố mạng."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Không thể tải xuống các ảnh trong album này. Vui lòng thử lại sau."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Không thể tải xuống danh sách album. Vui lòng thử lại sau."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Không thể tải xuống ảnh trong album này. Hãy thử lại sau."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Chỉ hình ảnh"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Chỉ video"</string>
-    <string name="show_all" msgid="4780647751652596980">"Hình ảnh và video"</string>
+    <string name="show_all" msgid="6963292714584735149">"Hình ảnh &amp; video"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Thư viện ảnh"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Không có ảnh."</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Đã lưu hình ảnh được cắt vào Tải xuống."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Hình ảnh được cắt chưa được lưu."</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Đã lưu hình ảnh được cắt vào <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Không có album nào."</string>
     <string name="empty_album" msgid="4542880442593595494">"O có hình ảnh/video nào."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Bài đăng"</string>
-    <string name="make_available_offline" msgid="5157950985488297112">"Làm cho sẵn có khi ngoại tuyến"</string>
+    <string name="make_available_offline" msgid="5157950985488297112">"Cho phép ngoại tuyến"</string>
     <string name="sync_picasa_albums" msgid="8522572542111169872">"Làm mới"</string>
     <string name="done" msgid="217672440064436595">"Xong"</string>
     <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d mục:"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Nhóm theo"</string>
     <string name="settings" msgid="1534847740615665736">"Cài đặt"</string>
     <string name="add_account" msgid="4271217504968243974">"Thêm tài khoản"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Máy ảnh"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Tải xuống"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Đã nhập"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Ảnh chụp màn hình"</string>
+    <string name="help" msgid="7368960711153618354">"Trợ giúp"</string>
 </resources>
diff --git a/res/values-xlarge/styles.xml b/res/values-xlarge/styles.xml
index b71e039..0bf4d56 100644
--- a/res/values-xlarge/styles.xml
+++ b/res/values-xlarge/styles.xml
@@ -17,7 +17,7 @@
     <style name="Theme.Gallery" parent="android:Theme.Holo">
         <item name="android:displayOptions"></item>
         <item name="android:actionBarStyle">@style/Holo.ActionBar</item>
-        <item name="android:windowBackground">@null</item>
+        <item name="android:windowBackground">@android:color/black</item>
         <item name="android:colorBackground">@null</item>
         <item name="android:colorBackgroundCacheHint">@null</item>
     </style>
diff --git a/res/values-zh-rCN/photoeditor_strings.xml b/res/values-zh-rCN/photoeditor_strings.xml
index 09ab11a..123e390 100644
--- a/res/values-zh-rCN/photoeditor_strings.xml
+++ b/res/values-zh-rCN/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"照相馆"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"无法加载照片"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"无法保存照片"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"照片已保存到“<xliff:g id="ALBUM_NAME">%s</xliff:g>”相册"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"已修改"</string>
-    <string name="save_photo" msgid="3125109368779997862">"要保存已修改的照片吗?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"无法加载该照片"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"无法保存经过编辑的照片"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"经过编辑的照片已保存至“<xliff:g id="FOLDER_NAME">%s</xliff:g>”"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"要舍弃未保存的更改吗?"</string>
     <string name="yes" msgid="5402582493291792293">"是"</string>
-    <string name="no" msgid="5595408018304861875">"否"</string>
     <string name="save" msgid="5516670392524294967">"保存"</string>
     <string name="autofix" msgid="1663414996270538748">"自动修正"</string>
     <string name="crop" msgid="7598378507763334041">"修剪"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"拖动标记以裁剪照片"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"在照片上绘制即可涂鸦"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"拖动照片可将其翻转"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"点按可消除红眼"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"点按即可消除红眼"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"拖动照片可将其旋转"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"拖动照片可将其拉直"</string>
 </resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index a58e1fe..ba3e913 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"继续播放"</string>
     <string name="loading" msgid="7038208555304563571">"正在载入..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"无法加载"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"无法加载此图片"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"无缩略图"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"重新开始"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"裁剪"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"确定"</string>
+    <string name="ok" msgid="5296833083983263293">"确定"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"触摸一张脸开始裁剪。"</string>
     <string name="saving_image" msgid="7270334453636349407">"正在保存照片..."</string>
     <string name="save_error" msgid="6857408774183654970">"无法保存经过裁剪的图片。"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"选择相册"</string>
     <string name="select_group" msgid="6744208543323307114">"选择群组"</string>
     <string name="set_image" msgid="2331476809308010401">"将照片设置为"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"设置壁纸"</string>
     <string name="wallpaper" msgid="140165383777262070">"正在设置壁纸..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁纸"</string>
     <string name="delete" msgid="2839695998251824487">"删除"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"删除"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"要删除所选内容吗?"</item>
+    <item quantity="other" msgid="5874316486520635333">"要删除所选内容吗?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"确认"</string>
     <string name="cancel" msgid="3637516880917356226">"取消"</string>
     <string name="share" msgid="3619042788254195341">"分享"</string>
     <string name="select_all" msgid="3403283025220282175">"全选"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"向右旋转"</string>
     <string name="no_such_item" msgid="5315144556325243400">"找不到指定的项。"</string>
     <string name="edit" msgid="1502273844748580847">"编辑"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"没有可完成该操作的应用程序。"</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"正在处理缓存请求"</string>
     <string name="caching_label" msgid="4521059045896269095">"正在缓存..."</string>
     <string name="crop_action" msgid="3427470284074377001">"修剪"</string>
@@ -90,17 +96,15 @@
     <string name="untagged" msgid="7281481064509590402">"未加标签"</string>
     <string name="no_location" msgid="4043624857489331676">"无位置信息"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"出现网络问题,系统无法识别某些位置。"</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"无法下载此相册中的照片,请稍后重试。"</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"无法下载相册列表,请稍后重试。"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"无法下载此相册中的照片,请稍后重试。"</string>
     <string name="show_images_only" msgid="7263218480867672653">"仅限图片"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"仅限视频"</string>
-    <string name="show_all" msgid="4780647751652596980">"图片和视频"</string>
-    <string name="appwidget_title" msgid="6410561146863700411">"照片库"</string>
-    <string name="appwidget_empty_text" msgid="1228925628357366957">"无照片。"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"经过裁剪的图片已保存至“下载内容”文件夹。"</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"经过裁剪的图片尚未保存。"</string>
+    <string name="show_all" msgid="6963292714584735149">"图片和视频"</string>
+    <string name="appwidget_title" msgid="6410561146863700411">"照片"</string>
+    <string name="appwidget_empty_text" msgid="1228925628357366957">"没有照片。"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"经过裁剪的图片已保存至“<xliff:g id="FOLDER_NAME">%s</xliff:g>”。"</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"没有可用的相册。"</string>
-    <string name="empty_album" msgid="4542880442593595494">"没有可用的图片/视频。"</string>
+    <string name="empty_album" msgid="4542880442593595494">"没有图片/视频。"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"帖子"</string>
     <string name="make_available_offline" msgid="5157950985488297112">"允许离线状态下使用"</string>
     <string name="sync_picasa_albums" msgid="8522572542111169872">"刷新"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"分组依据"</string>
     <string name="settings" msgid="1534847740615665736">"设置"</string>
     <string name="add_account" msgid="4271217504968243974">"添加帐户"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"相机"</string>
+    <string name="folder_download" msgid="7186215137642323932">"下载"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"已导入"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"屏幕截图"</string>
+    <string name="help" msgid="7368960711153618354">"帮助"</string>
 </resources>
diff --git a/res/values-zh-rTW/photoeditor_strings.xml b/res/values-zh-rTW/photoeditor_strings.xml
index b2e8fc6..21b7246 100644
--- a/res/values-zh-rTW/photoeditor_strings.xml
+++ b/res/values-zh-rTW/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"相片工作室"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"無法載入相片"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"無法儲存相片"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"相片已儲存至 <xliff:g id="ALBUM_NAME">%s</xliff:g> 相簿中"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"已編輯"</string>
-    <string name="save_photo" msgid="3125109368779997862">"要儲存已編輯的相片嗎?"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"無法載入相片"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"無法儲存編輯完成的相片"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"編輯完成的相片已儲存至「<xliff:g id="FOLDER_NAME">%s</xliff:g>」"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"要捨棄尚未儲存的變更?"</string>
     <string name="yes" msgid="5402582493291792293">"是"</string>
-    <string name="no" msgid="5595408018304861875">"否"</string>
     <string name="save" msgid="5516670392524294967">"儲存"</string>
     <string name="autofix" msgid="1663414996270538748">"自動修正"</string>
     <string name="crop" msgid="7598378507763334041">"裁剪"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"拖曳標記即可裁剪相片"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"在相片上畫圖即可塗鴉"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"拖曳照片即可翻轉"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"輕按即可消除紅眼"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"輕按紅眼即可予以移除"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"拖曳照片即可旋轉"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"拖曳照片即可拉正"</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 1a4592f..ac7de4f 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"繼續播放"</string>
     <string name="loading" msgid="7038208555304563571">"載入中..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"無法載入"</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"無法載入圖片"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"無縮圖"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"重新開始"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"裁剪"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"確定"</string>
+    <string name="ok" msgid="5296833083983263293">"確定"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"輕觸臉孔即可開始。"</string>
     <string name="saving_image" msgid="7270334453636349407">"正在儲存相片..."</string>
     <string name="save_error" msgid="6857408774183654970">"無法儲存裁剪的圖片。"</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"選取相簿"</string>
     <string name="select_group" msgid="6744208543323307114">"選取群組"</string>
     <string name="set_image" msgid="2331476809308010401">"將相片設為"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"設定桌布"</string>
     <string name="wallpaper" msgid="140165383777262070">"正在設定桌布..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"桌布"</string>
     <string name="delete" msgid="2839695998251824487">"刪除"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"刪除"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"刪除選取的項目?"</item>
+    <item quantity="other" msgid="5874316486520635333">"刪除選取的項目?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"確認"</string>
     <string name="cancel" msgid="3637516880917356226">"取消"</string>
     <string name="share" msgid="3619042788254195341">"分享"</string>
     <string name="select_all" msgid="3403283025220282175">"全選"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"向右旋轉"</string>
     <string name="no_such_item" msgid="5315144556325243400">"找不到項目。"</string>
     <string name="edit" msgid="1502273844748580847">"編輯"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"沒有可完成這個動作的應用程式。"</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"正在處理快取要求"</string>
     <string name="caching_label" msgid="4521059045896269095">"快取中…"</string>
     <string name="crop_action" msgid="3427470284074377001">"裁剪"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"無標記"</string>
     <string name="no_location" msgid="4043624857489331676">"無位置資訊"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"網路發生問題,因此無法辨識部分位置。"</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"無法下載這本相簿中的相片,請稍後再試。"</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"無法下載相簿清單,請稍後再試。"</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"無法下載這個相簿中的相片,請稍後再試。"</string>
     <string name="show_images_only" msgid="7263218480867672653">"僅顯示圖片"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"僅顯示影片"</string>
-    <string name="show_all" msgid="4780647751652596980">"圖片和影片"</string>
+    <string name="show_all" msgid="6963292714584735149">"圖片和影片"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"相片庫"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"沒有任何相片。"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"裁剪的圖片已儲存至「下載」資料夾。"</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"未儲存裁剪的圖片。"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"裁剪的圖片已儲存至「<xliff:g id="FOLDER_NAME">%s</xliff:g>」。"</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"沒有相簿。"</string>
     <string name="empty_album" msgid="4542880442593595494">"O 個可用的圖片/影片。"</string>
     <string name="picasa_posts" msgid="1497721615718760613">"訊息中的相片"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"分組依據"</string>
     <string name="settings" msgid="1534847740615665736">"設定"</string>
     <string name="add_account" msgid="4271217504968243974">"新增帳戶"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"相機"</string>
+    <string name="folder_download" msgid="7186215137642323932">"下載"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"匯入"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"螢幕擷取畫面"</string>
+    <string name="help" msgid="7368960711153618354">"說明"</string>
 </resources>
diff --git a/res/values-zu/photoeditor_strings.xml b/res/values-zu/photoeditor_strings.xml
index 98f599f..fad43bf 100644
--- a/res/values-zu/photoeditor_strings.xml
+++ b/res/values-zu/photoeditor_strings.xml
@@ -17,13 +17,11 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="photoeditor_name" msgid="4738587346771969697">"Endawo Yezithombe"</string>
-    <string name="loading_failure" msgid="5990741970574295950">"Isithombe asikwazi ukulayishwa"</string>
-    <string name="saving_failure" msgid="8596600582621364077">"Isithombe asikwazi ukulondolozwa"</string>
-    <string name="photo_saved" msgid="3494363661299484657">"Isithombe silondolozwe kwi <xliff:g id="ALBUM_NAME">%s</xliff:g> albhamu"</string>
-    <string name="edited_photo_bucket_name" msgid="3777746536831799890">"Kuhleliwe"</string>
-    <string name="save_photo" msgid="3125109368779997862">"Londoloza isithombe esihleliwe"</string>
+    <string name="loading_failure" msgid="7890360277163832130">"Ayikwazanga ukulayisha isithombe"</string>
+    <string name="saving_failure" msgid="8229491575433743974">"Ayikwazanga ukulondoloza isithombe esilungisiwe"</string>
+    <string name="photo_saved" msgid="6163006724627682202">"Isithombe esilungisiwe silondolozwe e <xliff:g id="FOLDER_NAME">%s</xliff:g>"</string>
+    <string name="discard_unsaved_photo" msgid="2353274490633681946">"Lahla izinguquko ezingagciniwe?"</string>
     <string name="yes" msgid="5402582493291792293">"Yebo"</string>
-    <string name="no" msgid="5595408018304861875">"Cha"</string>
     <string name="save" msgid="5516670392524294967">"Londoloza"</string>
     <string name="autofix" msgid="1663414996270538748">"Insiza Yokulungisa Okuzenzekelayo"</string>
     <string name="crop" msgid="7598378507763334041">"Nqampuna"</string>
@@ -55,7 +53,7 @@
     <string name="crop_tooltip" msgid="8794037869706891710">"Hudula insiza yokubeka uphawu ukuze usike ongakudingi esithombeni"</string>
     <string name="doodle_tooltip" msgid="2902117272374362915">"Dweba esithombeni ukuze kube nemidwebo eshintshayo"</string>
     <string name="flip_tooltip" msgid="2700943256714731737">"Hudula isithombe ukuze usiphendule"</string>
-    <string name="redeye_tooltip" msgid="9112774042113471358">"Thepha ukuze ususu iso elibomvu"</string>
+    <string name="redeye_tooltip" msgid="4183695928490874014">"Thinta emehlweni abomvu ukuze uwasuse"</string>
     <string name="rotate_tooltip" msgid="7008602969130734229">"Hudula isithombe ukuze usijikeleze"</string>
     <string name="straighten_tooltip" msgid="4846317027139212339">"Hudula isithombe ukuze usiqondise"</string>
 </resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 4e3a513..7ebfa3a 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -29,9 +29,11 @@
     <string name="resume_playing_resume" msgid="3847915469173852416">"Qalisa ukudlala"</string>
     <string name="loading" msgid="7038208555304563571">"Iyalayisha..."</string>
     <string name="fail_to_load" msgid="8394392853646664505">"Ayikwazanga ukulayisha."</string>
+    <string name="fail_to_load_image" msgid="6155388718549782425">"Ayikwazanga ukulayisha umfanekiso"</string>
     <string name="no_thumbnail" msgid="284723185546429750">"Asikho isithoombe esincane"</string>
     <string name="resume_playing_restart" msgid="5471008499835769292">"Qala phansi"</string>
-    <string name="crop_save_text" msgid="4972430481677741184">"Khropha"</string>
+    <string name="crop_save_text" msgid="152200178986698300">"Kulungile"</string>
+    <string name="ok" msgid="5296833083983263293">"KULUNGILE"</string>
     <string name="multiface_crop_help" msgid="2554690102655855657">"Cindezela ubuso ukuze uqale."</string>
     <string name="saving_image" msgid="7270334453636349407">"Ilondoloza isithombe..."</string>
     <string name="save_error" msgid="6857408774183654970">"Yehlulekile ukulondoloza umfanekiso onqampuniwe."</string>
@@ -42,10 +44,15 @@
     <string name="select_album" msgid="1557063764849434077">"Khetha i-albhamu"</string>
     <string name="select_group" msgid="6744208543323307114">"Khetha iqembu"</string>
     <string name="set_image" msgid="2331476809308010401">"Hlela isithombe njenge"</string>
+    <string name="set_wallpaper" msgid="8491121226190175017">"Setha isithombe sangemuva"</string>
     <string name="wallpaper" msgid="140165383777262070">"Isetha iphephadonga..."</string>
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Iphephadonga"</string>
     <string name="delete" msgid="2839695998251824487">"Susa"</string>
-    <string name="confirm_delete" msgid="7358770022173660511">"Susa"</string>
+  <plurals name="delete_selection">
+    <item quantity="one" msgid="6453379735401083732">"Sula into ekhethiwe?"</item>
+    <item quantity="other" msgid="5874316486520635333">"Sula izinto ezikhethiwe?"</item>
+  </plurals>
+    <string name="confirm" msgid="8646870096527848520">"Qinisekisa"</string>
     <string name="cancel" msgid="3637516880917356226">"Khansela"</string>
     <string name="share" msgid="3619042788254195341">"Yabelana"</string>
     <string name="select_all" msgid="3403283025220282175">"Khetha konke"</string>
@@ -75,7 +82,6 @@
     <string name="rotate_right" msgid="6776325835923384839">"Phendukisela kwesokudla"</string>
     <string name="no_such_item" msgid="5315144556325243400">"Yehlulekile ukuthola into yakho."</string>
     <string name="edit" msgid="1502273844748580847">"Hlela"</string>
-    <string name="activity_not_found" msgid="5619154886080878023">"Azikho izinhlelo zokusebenza ezitholakalayo ukuze kuqedelwe okwenziwayo."</string>
     <string name="process_caching_requests" msgid="8722939570307386071">"Ilungisa izicelo zokufaka kwinqolobane"</string>
     <string name="caching_label" msgid="4521059045896269095">"Ukulondoloza isikhashana..."</string>
     <string name="crop_action" msgid="3427470284074377001">"Khropha"</string>
@@ -90,15 +96,13 @@
     <string name="untagged" msgid="7281481064509590402">"Akunasilengiso"</string>
     <string name="no_location" msgid="4043624857489331676">"Ayikho indawo"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Ezinye izindawo azikwazanga ukubonakala ngenxa yezinkinga zokuxhumeka kuhleloxhumano"</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Ayikwazanga ukulayisha ngokungenisa izithombe kule alibhamu. Zama futhi kamuva."</string>
-    <string name="sync_album_set_error" msgid="9016732535181154028">"Ihlulekile ukungenisa ngokulayisha uhlu lwama-alibhumu. Sicela uzame kabusha."</string>
+    <string name="sync_album_error" msgid="1020688062900977530">"Ayikwazanga ukulanda izithombe kule albhamu. Zama ngemva kwesikhashana."</string>
     <string name="show_images_only" msgid="7263218480867672653">"Izithombe kuphela"</string>
     <string name="show_videos_only" msgid="3850394623678871697">"Amavidiyo kuphela"</string>
-    <string name="show_all" msgid="4780647751652596980">"Izithombe namavidiyo"</string>
+    <string name="show_all" msgid="6963292714584735149">"Izithombe namavidiyo"</string>
     <string name="appwidget_title" msgid="6410561146863700411">"Igalari Yesithombe"</string>
     <string name="appwidget_empty_text" msgid="1228925628357366957">"Azikho izithombe"</string>
-    <string name="crop_saved" msgid="1062612625032731770">"Isithombe esincishisiwe silondolozwe kokulayishwe ngokungenisa."</string>
-    <string name="crop_not_saved" msgid="3400773981839556">"Umfanekiso onqampuliwe awulondoloziwe"</string>
+    <string name="crop_saved" msgid="1595985909779105158">"Umfanekiso onqampuniwe ulondolozelwe e <xliff:g id="FOLDER_NAME">%s</xliff:g>."</string>
     <string name="no_albums_alert" msgid="4111744447491690896">"Awekho ama-albhamu atholalakalayo."</string>
     <string name="empty_album" msgid="4542880442593595494">"O izithombe/amavidiyo okutholakalayo."</string>
     <string name="picasa_posts" msgid="1497721615718760613">"Okuposiwe"</string>
@@ -162,4 +166,9 @@
     <string name="group_by" msgid="4308299657902209357">"Qoqa nge-"</string>
     <string name="settings" msgid="1534847740615665736">"Izilungiselelo"</string>
     <string name="add_account" msgid="4271217504968243974">"Yengeza i-akhawunti"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Ikhamera"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Laysha"</string>
+    <string name="folder_imported" msgid="2773581395524747099">"Okulandiwe"</string>
+    <string name="folder_screenshot" msgid="7200396565864213450">"Isithombe-skrini"</string>
+    <string name="help" msgid="7368960711153618354">"Usizo"</string>
 </resources>
diff --git a/res/values/dimensions.xml b/res/values/dimensions.xml
index 39b1b73..92e6713 100644
--- a/res/values/dimensions.xml
+++ b/res/values/dimensions.xml
@@ -41,16 +41,6 @@
     <dimen name="cache_pin_size">24dp</dimen>
     <dimen name="cache_pin_margin">8dp</dimen>
 
-    <!-- configuration for film strip in photo page -->
-    <dimen name="filmstrip_top_margin">12dp</dimen>
-    <dimen name="filmstrip_mid_margin">0dp</dimen>
-    <dimen name="filmstrip_bottom_margin">2dp</dimen>
-    <dimen name="filmstrip_thumb_size">48dp</dimen>
-    <dimen name="filmstrip_content_size">56dp</dimen>
-    <dimen name="filmstrip_grip_size">10dp</dimen>
-    <dimen name="filmstrip_bar_size">10dp</dimen>
-    <dimen name="filmstrip_grip_width">96dp</dimen>
-
     <!-- for manage cache bar -->
     <dimen name="manage_cache_bottom_height">48dp</dimen>
 </resources>
diff --git a/res/values/photoeditor_strings.xml b/res/values/photoeditor_strings.xml
index b282a45..f53e5a6 100644
--- a/res/values/photoeditor_strings.xml
+++ b/res/values/photoeditor_strings.xml
@@ -19,26 +19,20 @@
     <string name="photoeditor_name">Photo Studio</string>
 
     <!-- Toast shown when selected photo could not be loaded. [CHAR LIMIT=40] -->
-    <string name="loading_failure">Photo cannot be loaded</string>
+    <string name="loading_failure">Couldn\'t load the photo</string>
 
-    <!-- Toast shown when edited photo could not be saved. [CHAR LIMIT=40] -->
-    <string name="saving_failure">Photo cannot be saved</string>
+    <!-- Toast shown when edited photo could not be saved. [CHAR LIMIT=48] -->
+    <string name="saving_failure">Couldn\'t save edited photo</string>
 
-    <!-- Toast shown when edited photo is successfully saved under %s album [CHAR LIMIT=40] -->
-    <string name="photo_saved">Photo saved to <xliff:g id="album_name">%s</xliff:g> album</string>
+    <!-- Toast shown when edited photo is successfully saved under %s folder [CHAR LIMIT=40] -->
+    <string name="photo_saved">Edited photo saved to <xliff:g id="folder_name">%s</xliff:g></string>
 
-    <!-- Album name where edited photo will be saved in the storage. [CHAR LIMIT=12] -->
-    <string name="edited_photo_bucket_name">Edited</string>
-
-    <!-- Dialog message prompted when the user is abandoning unsaved changes. [CHAR LIMIT=40] -->
-    <string name="save_photo">Save edited photo?</string>
+    <!-- Dialog message prompted to confirm the user is abandoning unsaved changes. [CHAR LIMIT=40] -->
+    <string name="discard_unsaved_photo">Discard unsaved changes?</string>
 
     <!-- Dialog yes button for the user to accept text presented in a dialog. [CHAR LIMIT=12] -->
     <string name="yes">Yes</string>
 
-    <!-- Dialog no button for the user to decline text presented in a dialog. [CHAR LIMIT=12] -->
-    <string name="no">No</string>
-
     <!-- Text button in the action bar for the user to save edited photo. [CHAR LIMIT=8] -->
     <string name="save">SAVE</string>
 
@@ -133,7 +127,7 @@
     <string name="flip_tooltip">Drag photo to flip it</string>
 
     <!-- Tool-tip toast shown when the user chooses to remove red eyes. [CHAR LIMIT=40] -->
-    <string name="redeye_tooltip">Tap to remove red eyes</string>
+    <string name="redeye_tooltip">Tap on red eyes to remove them</string>
 
     <!-- Tool-tip toast shown when the user chooses to rotate photo. [CHAR LIMIT=40] -->
     <string name="rotate_tooltip">Drag photo to rotate it</string>
diff --git a/res/values/photoeditor_styles.xml b/res/values/photoeditor_styles.xml
index 7bd7fd0..c02296d 100644
--- a/res/values/photoeditor_styles.xml
+++ b/res/values/photoeditor_styles.xml
@@ -54,6 +54,20 @@
         <item name="android:layout_alignParentBottom">true</item>
         <item name="android:orientation">vertical</item>
     </style>
+    <style name="EffectLabelInToolPanel" parent="@style/EffectLabel">
+        <item name="android:layout_marginTop">0dp</item>
+    </style>
+    <style name="SeekBar">
+        <item name="android:layout_width">@dimen/seekbar_width</item>
+        <item name="android:layout_height">@dimen/seekbar_height</item>
+        <item name="android:paddingTop">@dimen/seekbar_padding_vertical</item>
+        <item name="android:paddingBottom">@dimen/seekbar_padding_vertical</item>
+        <item name="android:paddingLeft">@dimen/seekbar_padding_horizontal</item>
+        <item name="android:paddingRight">@dimen/seekbar_padding_horizontal</item>
+        <item name="android:minHeight">@dimen/seekbar_height</item>
+        <item name="android:maxHeight">@dimen/seekbar_height</item>
+        <item name="android:background">@android:color/transparent</item>
+    </style>
     <style name="ActionBarInner">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">?android:attr/actionBarSize</item>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index af62883..529480c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -48,6 +48,10 @@
          [CHAR LIMIT=50]-->
     <string name="fail_to_load">Couldn\'t load</string>
 
+    <!-- Used in a toast message when an image fails to be loaded
+         [CHAR LIMIT=50]-->
+    <string name="fail_to_load_image">Couldn\'t load the image</string>
+
     <!-- Displayed in place of the picture when we fail to get the thumbnail of it.
          [CHAR LIMIT=50]-->
     <string name="no_thumbnail">No thumbnail</string>
@@ -57,7 +61,10 @@
 
     <!-- Title of a menu item to indicate performing the image crop operation
          [CHAR LIMIT=20] -->
-    <string name="crop_save_text">Crop</string>
+    <string name="crop_save_text">OK</string>
+    <!-- Title of a menu item to indicate performing the image crop operation
+         [CHAR LIMIT=20] -->
+    <string name="ok">OK</string>
     <!-- Button indicating that the cropped image should be reverted back to the original -->
     <!-- Hint that appears when cropping an image with more than one face -->
     <string name="multiface_crop_help">Touch a face to begin.</string>
@@ -84,13 +91,20 @@
     <!-- Displayed in the title of the dialog for things to do with a picture
              that is to be "set as" (e.g. set as contact photo or set as wallpaper) -->
     <string name="set_image">Set picture as</string>
+    <!-- Activity title for cropping picture and setting it as wallpaper [CHAR LIMIT=20] -->
+    <string name="set_wallpaper">Set wallpaper</string>
     <!-- Toast/alert after saving wallpaper -->
     <string name="wallpaper">Setting wallpaper\u2026</string>
     <string name="camera_setas_wallpaper">Wallpaper</string>
 
     <!-- Details dialog "OK" button. Dismisses dialog. -->
     <string name="delete">Delete</string>
-    <string name="confirm_delete">Delete</string>
+    <!-- String Delete the selected media item(s) [CHAR LIMIT=50] -->
+    <plurals name="delete_selection">
+        <item quantity="one">Delete selected item?</item>
+        <item quantity="other">Delete selected items?</item>
+    </plurals>
+    <string name="confirm">Confirm</string>
     <string name="cancel">Cancel</string>
     <string name="share">Share</string>
 
@@ -142,10 +156,6 @@
          [CHAR_LIMIT=20]-->
     <string name="edit">Edit</string>
 
-    <!-- String used in a toast message indicating there is no application
-         available to handle a request [CHAR LIMIT=50] -->
-    <string name="activity_not_found">No app is available to complete the action.</string>
-
     <!-- String used as a title of a progress dialog. The user can
          choose to cache some Picasa picture albums on device, so it can
          be viewed offline. This string is shown when the request is being
@@ -200,9 +210,7 @@
     <string name="no_connectivity">Some locations couldn\'t be identified due to network problems.</string>
 
     <!-- This toast message is shown when failed to load the album data. [CHAR LIMIT=NONE] -->
-    <string name="sync_album_error">Failed to download the photos in this album. Please retry later.</string>
-    <!-- This toast message is shown when failed to load the album list data. [CHAR LIMIT=NONE] -->
-    <string name="sync_album_set_error">Failed to download the list of albums. Please retry later.</string>
+    <string name="sync_album_error">Couldn\'t download the photos in this album. Retry later.</string>
 
     <!-- The title of the menu item to let user choose the which portion of
          the media items the user wants to see. When pressed, a submenu will
@@ -216,7 +224,7 @@
     <string name="show_videos_only">Videos only</string>
 
     <!-- Title of a menu item to show all (both images and videos) [CHAR LIMIT=30]-->
-    <string name="show_all">Images and videos</string>
+    <string name="show_all">Images &amp; videos</string>
 
     <!-- Title of the StackView AppWidget -->
     <string name="appwidget_title">Photo Gallery</string>
@@ -225,12 +233,9 @@
     <string name="appwidget_empty_text">No photos.</string>
 
     <!-- Toast message shown when the cropped image has been saved in the
-         download folder [CHAR LIMIT=50]-->
-    <string name="crop_saved">Cropped image saved to Downloads.</string>
-
-    <!-- Toast message shown when the cropped image is not saved
-         [CHAR LIMIT=50]-->
-    <string name="crop_not_saved">Cropped image wasn\'t saved.</string>
+         %s folder (string: folder_download) [CHAR LIMIT=50]-->
+    <string name="crop_saved">
+        Cropped image saved to <xliff:g id="folder_name">%s</xliff:g>.</string>
 
     <!-- Toast message shown when there is no albums available [CHAR LIMIT=50]-->
     <string name="no_albums_alert">No albums available.</string>
@@ -415,4 +420,23 @@
 
     <!-- The title of menu item where user can add a new account -->
     <string name="add_account">Add account</string>
+
+    <!-- The label for the folder contains pictures taken by the camera. [CHAR LIMIT=20]-->
+    <string name="folder_camera">Camera</string>
+
+    <!-- The label for the folder contains downloaded pictures. [CHAR LIMIT=20]-->
+    <string name="folder_download">Download</string>
+
+    <!-- The label for the folder contains pictures that was imported from an
+         external camera. [CHAR LIMIT=20]-->
+    <string name="folder_imported">Imported</string>
+
+    <!-- The label for the folder contains screenshot images. [CHAR LIMIT=20]-->
+    <string name="folder_screenshot">Screenshot</string>
+
+    <!-- The title of the menu item which display online help in browser. [CHAR LIMIT=20]-->
+    <string name="help">Help</string>
+
+    <!-- Web address for gallery help.  DO NOT TRANSLATE -->
+    <string name="help_url_gallery_main" translatable="false"></string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index b26c728..3c32cef 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,14 +17,14 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <style name="Theme.Gallery" parent="android:Theme.Holo">
         <item name="android:displayOptions"></item>
-        <item name="android:windowFullscreen">true</item>
         <item name="android:windowContentOverlay">@null</item>
         <item name="android:actionBarStyle">@style/Holo.ActionBar</item>
-        <item name="android:windowBackground">@null</item>
+        <item name="android:windowBackground">@android:color/black</item>
         <item name="android:colorBackground">@null</item>
         <item name="android:colorBackgroundCacheHint">@null</item>
     </style>
     <style name="Holo.ActionBar" parent="android:Widget.Holo.ActionBar">
+        <item name="android:displayOptions">useLogo|showHome</item>
         <item name="android:background">@drawable/actionbar_translucent</item>
         <item name="android:backgroundStacked">@null</item>
     </style>
diff --git a/src/com/android/gallery3d/anim/Animation.java b/src/com/android/gallery3d/anim/Animation.java
index bd5a6cd..cc117bb 100644
--- a/src/com/android/gallery3d/anim/Animation.java
+++ b/src/com/android/gallery3d/anim/Animation.java
@@ -16,10 +16,10 @@
 
 package com.android.gallery3d.anim;
 
-import com.android.gallery3d.common.Utils;
-
 import android.view.animation.Interpolator;
 
+import com.android.gallery3d.common.Utils;
+
 // Animation calculates a value according to the current input time.
 //
 // 1. First we need to use setDuration(int) to set the duration of the
diff --git a/src/com/android/gallery3d/anim/AnimationSet.java b/src/com/android/gallery3d/anim/AnimationSet.java
deleted file mode 100644
index 773cb43..0000000
--- a/src/com/android/gallery3d/anim/AnimationSet.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.anim;
-
-import com.android.gallery3d.ui.GLCanvas;
-
-import java.util.ArrayList;
-
-public class AnimationSet extends CanvasAnimation {
-
-    private final ArrayList<CanvasAnimation> mAnimations =
-            new ArrayList<CanvasAnimation>();
-    private int mSaveFlags = 0;
-
-
-    public void addAnimation(CanvasAnimation anim) {
-        mAnimations.add(anim);
-        mSaveFlags |= anim.getCanvasSaveFlags();
-    }
-
-    @Override
-    public void apply(GLCanvas canvas) {
-        for (int i = 0, n = mAnimations.size(); i < n; i++) {
-            mAnimations.get(i).apply(canvas);
-        }
-    }
-
-    @Override
-    public int getCanvasSaveFlags() {
-        return mSaveFlags;
-    }
-
-    @Override
-    protected void onCalculate(float progress) {
-        // DO NOTHING
-    }
-
-    @Override
-    public boolean calculate(long currentTimeMillis) {
-        boolean more = false;
-        for (CanvasAnimation anim : mAnimations) {
-            more |= anim.calculate(currentTimeMillis);
-        }
-        return more;
-    }
-
-    @Override
-    public void start() {
-        for (CanvasAnimation anim : mAnimations) {
-            anim.start();
-        }
-    }
-
-    @Override
-    public boolean isActive() {
-        for (CanvasAnimation anim : mAnimations) {
-            if (anim.isActive()) return true;
-        }
-        return false;
-    }
-
-}
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
index 9500fff..144485d 100644
--- a/src/com/android/gallery3d/app/AbstractGalleryActivity.java
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -16,15 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.ImageCacheService;
-import com.android.gallery3d.ui.GLRoot;
-import com.android.gallery3d.ui.GLRootView;
-import com.android.gallery3d.ui.PositionRepository;
-import com.android.gallery3d.util.ThreadPool;
-
-import android.app.ActionBar;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
@@ -36,13 +27,26 @@
 import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.util.ThreadPool;
 
 public class AbstractGalleryActivity extends Activity implements GalleryActivity {
     @SuppressWarnings("unused")
     private static final String TAG = "AbstractGalleryActivity";
     private GLRootView mGLRootView;
     private StateManager mStateManager;
-    private PositionRepository mPositionRepository = new PositionRepository();
+    private GalleryActionBar mActionBar;
+    private OrientationManager mOrientationManager;
+    private TransitionStore mTransitionStore = new TransitionStore();
+    private boolean mDisableToggleStatusBar;
 
     private AlertDialog mAlertDialog = null;
     private BroadcastReceiver mMountReceiver = new BroadcastReceiver() {
@@ -54,6 +58,14 @@
     private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
 
     @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mOrientationManager = new OrientationManager(this);
+        toggleStatusBarByOrientation();
+        getWindow().setBackgroundDrawable(null);
+    }
+
+    @Override
     protected void onSaveInstanceState(Bundle outState) {
         mGLRootView.lockRenderThread();
         try {
@@ -69,16 +81,13 @@
         super.onConfigurationChanged(config);
         mStateManager.onConfigurationChange(config);
         invalidateOptionsMenu();
+        toggleStatusBarByOrientation();
     }
 
     public Context getAndroidContext() {
         return this;
     }
 
-    public ImageCacheService getImageCacheService() {
-        return ((GalleryApp) getApplication()).getImageCacheService();
-    }
-
     public DataManager getDataManager() {
         return ((GalleryApp) getApplication()).getDataManager();
     }
@@ -87,10 +96,6 @@
         return ((GalleryApp) getApplication()).getThreadPool();
     }
 
-    public GalleryApp getGalleryApplication() {
-        return (GalleryApp) getApplication();
-    }
-
     public synchronized StateManager getStateManager() {
         if (mStateManager == null) {
             mStateManager = new StateManager(this);
@@ -102,8 +107,8 @@
         return mGLRootView;
     }
 
-    public PositionRepository getPositionRepository() {
-        return mPositionRepository;
+    public OrientationManager getOrientationManager() {
+        return mOrientationManager;
     }
 
     @Override
@@ -112,11 +117,6 @@
         mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
     }
 
-    public int getActionBarHeight() {
-        ActionBar actionBar = getActionBar();
-        return actionBar != null ? actionBar.getHeight() : 0;
-    }
-
     protected void onStorageReady() {
         if (mAlertDialog != null) {
             mAlertDialog.dismiss();
@@ -173,11 +173,13 @@
             mGLRootView.unlockRenderThread();
         }
         mGLRootView.onResume();
+        mOrientationManager.resume();
     }
 
     @Override
     protected void onPause() {
         super.onPause();
+        mOrientationManager.pause();
         mGLRootView.onPause();
         mGLRootView.lockRenderThread();
         try {
@@ -186,6 +188,20 @@
         } finally {
             mGLRootView.unlockRenderThread();
         }
+        MediaItem.getMicroThumbPool().clear();
+        MediaItem.getThumbPool().clear();
+        MediaItem.getBytesBufferPool().clear();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mGLRootView.lockRenderThread();
+        try {
+            getStateManager().destroy();
+        } finally {
+            mGLRootView.unlockRenderThread();
+        }
     }
 
     @Override
@@ -200,7 +216,54 @@
     }
 
     @Override
+    public void onBackPressed() {
+        // send the back event to the top sub-state
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            getStateManager().onBackPressed();
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    @Override
     public GalleryActionBar getGalleryActionBar() {
-        return null;
+        if (mActionBar == null) {
+            mActionBar = new GalleryActionBar(this);
+        }
+        return mActionBar;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            return getStateManager().itemSelected(item);
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    protected void disableToggleStatusBar() {
+        mDisableToggleStatusBar = true;
+    }
+
+    // Shows status bar in portrait view, hide in landscape view
+    private void toggleStatusBarByOrientation() {
+        if (mDisableToggleStatusBar) return;
+
+        Window win = getWindow();
+        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+            win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        } else {
+            win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        }
+    }
+
+    @Override
+    public TransitionStore getTransitionStore() {
+        return mTransitionStore;
     }
 }
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
index 1563a09..867cda3 100644
--- a/src/com/android/gallery3d/app/ActivityState.java
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -16,15 +16,12 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.ui.GLView;
-
 import android.app.ActionBar;
 import android.app.Activity;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.BatteryManager;
 import android.os.Bundle;
@@ -34,10 +31,13 @@
 import android.view.Window;
 import android.view.WindowManager;
 
+import com.android.gallery3d.ui.GLView;
+
 abstract public class ActivityState {
-    public static final int FLAG_HIDE_ACTION_BAR = 1;
-    public static final int FLAG_HIDE_STATUS_BAR = 2;
-    public static final int FLAG_SCREEN_ON = 3;
+    protected static final int FLAG_HIDE_ACTION_BAR = 1;
+    protected static final int FLAG_HIDE_STATUS_BAR = 2;
+    protected static final int FLAG_SCREEN_ON_WHEN_PLUGGED = 4;
+    protected static final int FLAG_SCREEN_ON_ALWAYS = 8;
 
     private static final int SCREEN_ON_FLAGS = (
               WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
@@ -56,11 +56,11 @@
         public int requestCode;
         public int resultCode = Activity.RESULT_CANCELED;
         public Intent resultData;
-        ResultEntry next;
     }
 
     private boolean mDestroyed = false;
     private boolean mPlugged = false;
+    boolean mIsFinishing = false;
 
     protected ActivityState() {
     }
@@ -109,25 +109,26 @@
 
                 if (plugged != mPlugged) {
                     mPlugged = plugged;
-                    final Window win = ((Activity) mActivity).getWindow();
-                    final WindowManager.LayoutParams params = win.getAttributes();
-                    setScreenOnFlags(params);
-                    win.setAttributes(params);
+                    setScreenOnFlags();
                 }
             }
         }
     };
 
-    void setScreenOnFlags(WindowManager.LayoutParams params) {
-        if (mPlugged && 0 != (mFlags & FLAG_SCREEN_ON)) {
+    void setScreenOnFlags() {
+        final Window win = ((Activity) mActivity).getWindow();
+        final WindowManager.LayoutParams params = win.getAttributes();
+        if ((0 != (mFlags & FLAG_SCREEN_ON_ALWAYS)) ||
+                (mPlugged && 0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED))) {
             params.flags |= SCREEN_ON_FLAGS;
         } else {
             params.flags &= ~SCREEN_ON_FLAGS;
         }
+        win.setAttributes(params);
     }
 
     protected void onPause() {
-        if (0 != (mFlags & FLAG_SCREEN_ON)) {
+        if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
             ((Activity) mActivity).unregisterReceiver(mPowerIntentReceiver);
         }
     }
@@ -143,25 +144,17 @@
                 actionBar.show();
             }
             int stateCount = mActivity.getStateManager().getStateCount();
-            actionBar.setDisplayOptions(
-                    stateCount == 1 ? 0 : ActionBar.DISPLAY_HOME_AS_UP,
-                    ActionBar.DISPLAY_HOME_AS_UP);
-            actionBar.setHomeButtonEnabled(true);
+            mActivity.getGalleryActionBar().setDisplayOptions(stateCount > 1, true);
+            // Default behavior, this can be overridden in ActivityState's onResume.
+            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
         }
 
         activity.invalidateOptionsMenu();
 
-        final Window win = activity.getWindow();
-        final WindowManager.LayoutParams params = win.getAttributes();
+        setScreenOnFlags();
 
-        if ((mFlags & FLAG_HIDE_STATUS_BAR) != 0) {
-            params.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE;
-        } else {
-            params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
-        }
-
-        setScreenOnFlags(params);
-        win.setAttributes(params);
+        boolean lightsOut = ((mFlags & FLAG_HIDE_STATUS_BAR) != 0);
+        mActivity.getGLRoot().setLightsOutMode(lightsOut);
 
         ResultEntry entry = mReceivedResults;
         if (entry != null) {
@@ -169,13 +162,16 @@
             onStateResult(entry.requestCode, entry.resultCode, entry.resultData);
         }
 
-        if (0 != (mFlags & FLAG_SCREEN_ON)) {
+        if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
             // we need to know whether the device is plugged in to do this correctly
             final IntentFilter filter = new IntentFilter();
             filter.addAction(Intent.ACTION_BATTERY_CHANGED);
             activity.registerReceiver(mPowerIntentReceiver, filter);
         }
         onResume();
+
+        // the transition store should be cleared after resume;
+        mActivity.getTransitionStore().clear();
     }
 
     // a subclass of ActivityState should override the method to resume itself
@@ -199,4 +195,8 @@
     boolean isDestroyed() {
         return mDestroyed;
     }
+
+    public boolean isFinishing() {
+        return mIsFinishing;
+    }
 }
diff --git a/src/com/android/gallery3d/app/AlbumDataAdapter.java b/src/com/android/gallery3d/app/AlbumDataLoader.java
similarity index 93%
rename from src/com/android/gallery3d/app/AlbumDataAdapter.java
rename to src/com/android/gallery3d/app/AlbumDataLoader.java
index 6711786..a99cf93 100644
--- a/src/com/android/gallery3d/app/AlbumDataAdapter.java
+++ b/src/com/android/gallery3d/app/AlbumDataLoader.java
@@ -18,6 +18,7 @@
 
 import android.os.Handler;
 import android.os.Message;
+import android.os.Process;
 
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.ContentListener;
@@ -25,7 +26,7 @@
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.ui.AlbumView;
+import com.android.gallery3d.ui.AlbumSlotRenderer;
 import com.android.gallery3d.ui.SynchronizedHandler;
 
 import java.util.ArrayList;
@@ -34,7 +35,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.FutureTask;
 
-public class AlbumDataAdapter implements AlbumView.Model {
+public class AlbumDataLoader {
     @SuppressWarnings("unused")
     private static final String TAG = "AlbumDataAdapter";
     private static final int DATA_CACHE_SIZE = 1000;
@@ -50,6 +51,11 @@
     private final long[] mItemVersion;
     private final long[] mSetVersion;
 
+    public static interface DataListener {
+        public void onContentChanged(int index);
+        public void onSizeChanged(int size);
+    }
+
     private int mActiveStart = 0;
     private int mActiveEnd = 0;
 
@@ -62,13 +68,13 @@
     private final Handler mMainHandler;
     private int mSize = 0;
 
-    private AlbumView.ModelListener mModelListener;
+    private DataListener mDataListener;
     private MySourceListener mSourceListener = new MySourceListener();
     private LoadingListener mLoadingListener;
 
     private ReloadTask mReloadTask;
 
-    public AlbumDataAdapter(GalleryActivity context, MediaSet mediaSet) {
+    public AlbumDataLoader(GalleryActivity context, MediaSet mediaSet) {
         mSource = mediaSet;
 
         mData = new MediaItem[DATA_CACHE_SIZE];
@@ -119,10 +125,6 @@
         return mActiveStart;
     }
 
-    public int getActiveEnd() {
-        return mActiveEnd;
-    }
-
     public boolean isActive(int index) {
         return index >= mActiveStart && index < mActiveEnd;
     }
@@ -192,8 +194,8 @@
         }
     }
 
-    public void setModelListener(AlbumView.ModelListener listener) {
-        mModelListener = listener;
+    public void setDataListener(DataListener listener) {
+        mDataListener = listener;
     }
 
     public void setLoadingListener(LoadingListener listener) {
@@ -261,7 +263,7 @@
             mSourceVersion = info.version;
             if (mSize != info.size) {
                 mSize = info.size;
-                if (mModelListener != null) mModelListener.onSizeChanged(mSize);
+                if (mDataListener != null) mDataListener.onSizeChanged(mSize);
                 if (mContentEnd > mSize) mContentEnd = mSize;
                 if (mActiveEnd > mSize) mActiveEnd = mSize;
             }
@@ -280,8 +282,8 @@
                 if (mItemVersion[index] != itemVersion) {
                     mItemVersion[index] = itemVersion;
                     mData[index] = updateItem;
-                    if (mModelListener != null && i >= mActiveStart && i < mActiveEnd) {
-                        mModelListener.onWindowContentChanged(i);
+                    if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
+                        mDataListener.onContentChanged(i);
                     }
                 }
             }
@@ -318,6 +320,8 @@
 
         @Override
         public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
             boolean updateComplete = false;
             while (mActive) {
                 synchronized (this) {
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index 6fb4143..e773691 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -19,8 +19,11 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
 import android.os.Vibrator;
 import android.provider.MediaStore;
 import android.view.ActionMode;
@@ -40,33 +43,35 @@
 import com.android.gallery3d.data.Path;
 import com.android.gallery3d.ui.ActionModeHandler;
 import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
-import com.android.gallery3d.ui.AlbumView;
+import com.android.gallery3d.ui.AlbumSlotRenderer;
 import com.android.gallery3d.ui.DetailsHelper;
 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.FadeTexture;
 import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.ui.GLView;
-import com.android.gallery3d.ui.GridDrawer;
-import com.android.gallery3d.ui.HighlightDrawer;
-import com.android.gallery3d.ui.PositionProvider;
-import com.android.gallery3d.ui.PositionRepository;
-import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
+import com.android.gallery3d.ui.RelativePosition;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
-import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.GalleryUtils;
-
-import java.util.Random;
+import com.android.gallery3d.util.MediaSetUtils;
 
 public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
         SelectionManager.SelectionListener, MediaSet.SyncListener {
     @SuppressWarnings("unused")
     private static final String TAG = "AlbumPage";
 
+    private static final int MSG_PICK_PHOTO = 1;
+
     public static final String KEY_MEDIA_PATH = "media-path";
+    public static final String KEY_PARENT_MEDIA_PATH = "parent-media-path";
     public static final String KEY_SET_CENTER = "set-center";
     public static final String KEY_AUTO_SELECT_ALL = "auto-select-all";
     public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu";
+    public static final String KEY_RESUME_ANIMATION = "resume_animation";
 
     private static final int REQUEST_SLIDESHOW = 1;
     private static final int REQUEST_PHOTO = 2;
@@ -78,16 +83,15 @@
     private static final float USER_DISTANCE_METER = 0.3f;
 
     private boolean mIsActive = false;
-    private StaticBackground mStaticBackground;
-    private AlbumView mAlbumView;
+    private AlbumSlotRenderer mAlbumView;
     private Path mMediaSetPath;
+    private String mParentMediaSetString;
+    private SlotView mSlotView;
 
-    private AlbumDataAdapter mAlbumDataAdapter;
+    private AlbumDataLoader mAlbumDataAdapter;
 
     protected SelectionManager mSelectionManager;
     private Vibrator mVibrator;
-    private GridDrawer mGridDrawer;
-    private HighlightDrawer mHighlightDrawer;
 
     private boolean mGetContent;
     private boolean mShowClusterMenu;
@@ -100,35 +104,65 @@
     private MediaSet mMediaSet;
     private boolean mShowDetails;
     private float mUserDistance; // in pixel
+    private Handler mHandler;
 
     private Future<Integer> mSyncTask = null;
 
     private int mLoadingBits = 0;
     private boolean mInitialSynced = false;
+    private RelativePosition mOpenCenter = new RelativePosition();
+
+    private PhotoFallbackEffect mResumeEffect;
+    private PhotoFallbackEffect.PositionProvider mPositionProvider =
+            new PhotoFallbackEffect.PositionProvider() {
+        @Override
+        public Rect getPosition(int index) {
+            Rect rect = mSlotView.getSlotRect(index);
+            Rect bounds = mSlotView.bounds();
+            rect.offset(bounds.left - mSlotView.getScrollX(),
+                    bounds.top - mSlotView.getScrollY());
+            return rect;
+        }
+
+        @Override
+        public int getItemIndex(Path path) {
+            int start = mSlotView.getVisibleStart();
+            int end = mSlotView.getVisibleEnd();
+            for (int i = start; i < end; ++i) {
+                MediaItem item = mAlbumDataAdapter.get(i);
+                if (item != null && item.getPath() == path) return i;
+            }
+            return -1;
+        }
+    };
 
     private final GLView mRootPane = new GLView() {
         private final float mMatrix[] = new float[16];
 
         @Override
+        protected void renderBackground(GLCanvas view) {
+            view.clearBuffer();
+        }
+
+        @Override
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
-            mStaticBackground.layout(0, 0, right - left, bottom - top);
 
-            int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+            int slotViewTop = mActivity.getGalleryActionBar().getHeight();
             int slotViewBottom = bottom - top;
             int slotViewRight = right - left;
 
             if (mShowDetails) {
                 mDetailsHelper.layout(left, slotViewTop, right, bottom);
             } else {
-                mAlbumView.setSelectionDrawer(mGridDrawer);
+                mAlbumView.setHighlightItemPath(null);
             }
 
-            mAlbumView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+            // Set the mSlotView as a reference point to the open animation
+            mOpenCenter.setReferencePosition(0, slotViewTop);
+            mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
             GalleryUtils.setViewPointMatrix(mMatrix,
                     (right - left) / 2, (bottom - top) / 2, -mUserDistance);
-            PositionRepository.getInstance(mActivity).setOffset(
-                    0, slotViewTop);
         }
 
         @Override
@@ -136,10 +170,32 @@
             canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
             canvas.multiplyMatrix(mMatrix, 0);
             super.render(canvas);
+
+            if (mResumeEffect != null) {
+                boolean more = mResumeEffect.draw(canvas);
+                if (!more) {
+                    mResumeEffect = null;
+                    mAlbumView.setSlotFilter(null);
+                }
+                // We want to render one more time even when no more effect
+                // required. So that the animated thumbnails could be draw
+                // with declarations in super.render().
+                invalidate();
+            }
             canvas.restore();
         }
     };
 
+    // This are the transitions we want:
+    //
+    // +--------+           +------------+    +-------+    +----------+
+    // | Camera |---------->| Fullscreen |--->| Album |--->| AlbumSet |
+    // |  View  | thumbnail |   Photo    | up | Page  | up |   Page   |
+    // +--------+           +------------+    +-------+    +----------+
+    //     ^                      |               |            ^  |
+    //     |                      |               |            |  |         close
+    //     +----------back--------+               +----back----+  +--back->  app
+    //
     @Override
     protected void onBackPressed() {
         if (mShowDetails) {
@@ -147,54 +203,87 @@
         } else if (mSelectionManager.inSelectionMode()) {
             mSelectionManager.leaveSelectionMode();
         } else {
-            mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+            // TODO: fix this regression
+            // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+            onUpPressed();
+        }
+    }
+
+    private void onUpPressed() {
+        if (mActivity.getStateManager().getStateCount() > 1) {
             super.onBackPressed();
+        } else if (mParentMediaSetString != null) {
+            Bundle data = new Bundle(getData());
+            data.putString(AlbumSetPage.KEY_MEDIA_PATH, mParentMediaSetString);
+            mActivity.getStateManager().switchState(
+                    this, AlbumSetPage.class, data);
         }
     }
 
     private void onDown(int index) {
-        MediaItem item = mAlbumDataAdapter.get(index);
-        Path path = (item == null) ? null : item.getPath();
-        mSelectionManager.setPressedPath(path);
-        mAlbumView.invalidate();
+        mAlbumView.setPressedIndex(index);
     }
 
-    private void onUp() {
-        mSelectionManager.setPressedPath(null);
-        mAlbumView.invalidate();
-    }
-
-    public void onSingleTapUp(int slotIndex) {
-        MediaItem item = mAlbumDataAdapter.get(slotIndex);
-        if (item == null) {
-            Log.w(TAG, "item not ready yet, ignore the click");
-            return;
-        }
-        if (mShowDetails) {
-            mHighlightDrawer.setHighlightItem(item.getPath());
-            mDetailsHelper.reloadDetails(slotIndex);
-        } else if (!mSelectionManager.inSelectionMode()) {
-            if (mGetContent) {
-                onGetContent(item);
-            } else {
-                // Get into the PhotoPage.
-                Bundle data = new Bundle();
-                mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
-                data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
-                data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
-                        mMediaSetPath.toString());
-                data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
-                        item.getPath().toString());
-                mActivity.getStateManager().startStateForResult(
-                        PhotoPage.class, REQUEST_PHOTO, data);
-            }
+    private void onUp(boolean followedByLongPress) {
+        if (followedByLongPress) {
+            // Avoid showing press-up animations for long-press.
+            mAlbumView.setPressedIndex(-1);
         } else {
+            mAlbumView.setPressedUp();
+        }
+    }
+
+    private void onSingleTapUp(int slotIndex) {
+        if (!mIsActive) return;
+
+        if (mSelectionManager.inSelectionMode()) {
+            MediaItem item = mAlbumDataAdapter.get(slotIndex);
+            if (item == null) return; // Item not ready yet, ignore the click
             mSelectionManager.toggle(item.getPath());
             mDetailsSource.findIndex(slotIndex);
-            mAlbumView.invalidate();
+            mSlotView.invalidate();
+        } else {
+            // Show pressed-up animation for the single-tap.
+            mAlbumView.setPressedIndex(slotIndex);
+            mAlbumView.setPressedUp();
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_PHOTO, slotIndex, 0),
+                    FadeTexture.DURATION);
         }
     }
 
+    private void pickPhoto(int slotIndex) {
+        if (!mIsActive) return;
+
+        MediaItem item = mAlbumDataAdapter.get(slotIndex);
+        if (item == null) return; // Item not ready yet, ignore the click
+        if (mGetContent) {
+            onGetContent(item);
+        } else {
+            // Get into the PhotoPage.
+            // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+            Bundle data = new Bundle();
+            data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+            data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+                    getSlotRect(slotIndex));
+            data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+                    mMediaSetPath.toString());
+            data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+                    item.getPath().toString());
+            mActivity.getStateManager().startStateForResult(
+                    PhotoPage.class, REQUEST_PHOTO, data);
+        }
+    }
+
+    private Rect getSlotRect(int slotIndex) {
+        // Get slot rectangle relative to this root pane.
+        Rect offset = new Rect();
+        mRootPane.getBoundsOf(mSlotView, offset);
+        Rect r = mSlotView.getSlotRect(slotIndex);
+        r.offset(offset.left - mSlotView.getScrollX(),
+                offset.top - mSlotView.getScrollY());
+        return r;
+    }
+
     private void onGetContent(final MediaItem item) {
         DataManager dm = mActivity.getDataManager();
         Activity activity = (Activity) mActivity;
@@ -218,18 +307,15 @@
 
     public void onLongTap(int slotIndex) {
         if (mGetContent) return;
-        if (mShowDetails) {
-            onSingleTapUp(slotIndex);
-        } else {
-            MediaItem item = mAlbumDataAdapter.get(slotIndex);
-            if (item == null) return;
-            mSelectionManager.setAutoLeaveSelectionMode(true);
-            mSelectionManager.toggle(item.getPath());
-            mDetailsSource.findIndex(slotIndex);
-            mAlbumView.invalidate();
-        }
+        MediaItem item = mAlbumDataAdapter.get(slotIndex);
+        if (item == null) return;
+        mSelectionManager.setAutoLeaveSelectionMode(true);
+        mSelectionManager.toggle(item.getPath());
+        mDetailsSource.findIndex(slotIndex);
+        mSlotView.invalidate();
     }
 
+    @Override
     public void doCluster(int clusterType) {
         String basePath = mMediaSet.getPath().toString();
         String newPath = FilterUtils.newClusterPath(basePath, clusterType);
@@ -242,25 +328,11 @@
                     GalleryActionBar.getClusterByTypeString(context, clusterType));
         }
 
-        mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+        // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
         mActivity.getStateManager().startStateForResult(
                 AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
     }
 
-    public void doFilter(int filterType) {
-        String basePath = mMediaSet.getPath().toString();
-        String newPath = FilterUtils.switchFilterPath(basePath, filterType);
-        Bundle data = new Bundle(getData());
-        data.putString(AlbumPage.KEY_MEDIA_PATH, newPath);
-        mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
-        mActivity.getStateManager().switchState(this, AlbumPage.class, data);
-    }
-
-    public void onOperationComplete() {
-        mAlbumView.invalidate();
-        // TODO: enable animation
-    }
-
     @Override
     protected void onCreate(Bundle data, Bundle restoreState) {
         mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
@@ -272,60 +344,53 @@
         Context context = mActivity.getAndroidContext();
         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
 
-        startTransition(data);
-
         // Enable auto-select-all for mtp album
         if (data.getBoolean(KEY_AUTO_SELECT_ALL)) {
             mSelectionManager.selectAll();
         }
-    }
 
-    private void startTransition() {
-        final PositionRepository repository =
-                PositionRepository.getInstance(mActivity);
-        mAlbumView.startTransition(new PositionProvider() {
-            private final Position mTempPosition = new Position();
-            public Position getPosition(long identity, Position target) {
-                Position p = repository.get(identity);
-                if (p != null) return p;
-                mTempPosition.set(target);
-                mTempPosition.z = 128;
-                return mTempPosition;
+        // Don't show animation if it is restored
+        if (restoreState == null && data != null) {
+            int[] center = data.getIntArray(KEY_SET_CENTER);
+            if (center != null) {
+                mOpenCenter.setAbsolutePosition(center[0], center[1]);
+                mSlotView.startScatteringAnimation(mOpenCenter);
             }
-        });
-    }
+        }
 
-    private void startTransition(Bundle data) {
-        final PositionRepository repository =
-                PositionRepository.getInstance(mActivity);
-        final int[] center = data == null
-                ? null
-                : data.getIntArray(KEY_SET_CENTER);
-        final Random random = new Random();
-        mAlbumView.startTransition(new PositionProvider() {
-            private final Position mTempPosition = new Position();
-            public Position getPosition(long identity, Position target) {
-                Position p = repository.get(identity);
-                if (p != null) return p;
-                if (center != null) {
-                    random.setSeed(identity);
-                    mTempPosition.set(center[0], center[1],
-                            0, random.nextInt(60) - 30, 0);
-                } else {
-                    mTempPosition.set(target);
-                    mTempPosition.z = 128;
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_PICK_PHOTO: {
+                        pickPhoto(message.arg1);
+                        break;
+                    }
+                    default: throw new AssertionError(message.what);
                 }
-                return mTempPosition;
             }
-        });
+        };
     }
 
     @Override
     protected void onResume() {
         super.onResume();
         mIsActive = true;
+
+        mResumeEffect = mActivity.getTransitionStore().get(KEY_RESUME_ANIMATION);
+        if (mResumeEffect != null) {
+            mAlbumView.setSlotFilter(mResumeEffect);
+            mResumeEffect.setPositionProvider(mPositionProvider);
+            mResumeEffect.start();
+        }
+
         setContentPane(mRootPane);
 
+        Path path = mMediaSet.getPath();
+        boolean enableHomeButton = (mActivity.getStateManager().getStateCount() > 1) |
+                mParentMediaSetString != null;
+        mActivity.getGalleryActionBar().setDisplayOptions(enableHomeButton, true);
+
         // Set the reload bit here to prevent it exit this page in clearLoadingBit().
         setLoadingBit(BIT_LOADING_RELOAD);
         mAlbumDataAdapter.resume();
@@ -342,6 +407,9 @@
     protected void onPause() {
         super.onPause();
         mIsActive = false;
+
+        mAlbumView.setSlotFilter(null);
+
         mAlbumDataAdapter.pause();
         mAlbumView.pause();
         DetailsHelper.pause();
@@ -349,6 +417,7 @@
         if (mSyncTask != null) {
             mSyncTask.cancel();
             mSyncTask = null;
+            clearLoadingBit(BIT_LOADING_SYNC);
         }
         mActionModeHandler.pause();
     }
@@ -362,26 +431,22 @@
     }
 
     private void initializeViews() {
-        mStaticBackground = new StaticBackground((Context) mActivity);
-        mRootPane.addComponent(mStaticBackground);
-
         mSelectionManager = new SelectionManager(mActivity, false);
         mSelectionManager.setSelectionListener(this);
-        mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
         Config.AlbumPage config = Config.AlbumPage.get((Context) mActivity);
-        mAlbumView = new AlbumView(mActivity, config.slotViewSpec,
-                0 /* don't cache thumbnail */);
-        mAlbumView.setSelectionDrawer(mGridDrawer);
-        mRootPane.addComponent(mAlbumView);
-        mAlbumView.setListener(new SlotView.SimpleListener() {
+        mSlotView = new SlotView(mActivity, config.slotViewSpec);
+        mAlbumView = new AlbumSlotRenderer(mActivity, mSlotView, mSelectionManager);
+        mSlotView.setSlotRenderer(mAlbumView);
+        mRootPane.addComponent(mSlotView);
+        mSlotView.setListener(new SlotView.SimpleListener() {
             @Override
             public void onDown(int index) {
                 AlbumPage.this.onDown(index);
             }
 
             @Override
-            public void onUp() {
-                AlbumPage.this.onUp();
+            public void onUp(boolean followedByLongPress) {
+                AlbumPage.this.onUp(followedByLongPress);
             }
 
             @Override
@@ -400,17 +465,17 @@
                 return onItemSelected(item);
             }
         });
-        mStaticBackground.setImage(R.drawable.background,
-                R.drawable.background_portrait);
     }
 
     private void initializeData(Bundle data) {
         mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH));
+        mParentMediaSetString = data.getString(KEY_PARENT_MEDIA_PATH);
         mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath);
-        Utils.assertTrue(mMediaSet != null,
-                "MediaSet is null. Path = %s", mMediaSetPath);
+        if (mMediaSet == null) {
+            Utils.fail("MediaSet is null. Path = %s", mMediaSetPath);
+        }
         mSelectionManager.setSourceMediaSet(mMediaSet);
-        mAlbumDataAdapter = new AlbumDataAdapter(mActivity, mMediaSet);
+        mAlbumDataAdapter = new AlbumDataLoader(mActivity, mMediaSet);
         mAlbumDataAdapter.setLoadingListener(new MyLoadingListener());
         mAlbumView.setModel(mAlbumDataAdapter);
     }
@@ -418,8 +483,6 @@
     private void showDetails() {
         mShowDetails = true;
         if (mDetailsHelper == null) {
-            mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext(),
-                    mSelectionManager);
             mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
             mDetailsHelper.setCloseListener(new CloseListener() {
                 public void onClose() {
@@ -427,15 +490,14 @@
                 }
             });
         }
-        mAlbumView.setSelectionDrawer(mHighlightDrawer);
         mDetailsHelper.show();
     }
 
     private void hideDetails() {
         mShowDetails = false;
         mDetailsHelper.hide();
-        mAlbumView.setSelectionDrawer(mGridDrawer);
-        mAlbumView.invalidate();
+        mAlbumView.setHighlightItemPath(null);
+        mSlotView.invalidate();
     }
 
     @Override
@@ -459,13 +521,20 @@
                 menu.findItem(R.id.action_slideshow).setVisible(true);
             }
 
-            MenuItem groupBy = menu.findItem(R.id.action_group_by);
             FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true);
 
+            MenuItem groupBy = menu.findItem(R.id.action_group_by);
             if (groupBy != null) {
                 groupBy.setVisible(mShowClusterMenu);
             }
 
+            MenuItem switchCamera = menu.findItem(R.id.action_camera);
+            if (switchCamera != null) {
+                switchCamera.setVisible(
+                        MediaSetUtils.isCameraSource(mMediaSetPath)
+                        && GalleryUtils.isCameraAvailable(activity));
+            }
+
             actionBar.setTitle(mMediaSet.getName());
         }
         actionBar.setSubtitle(null);
@@ -476,6 +545,10 @@
     @Override
     protected boolean onItemSelected(MenuItem item) {
         switch (item.getItemId()) {
+            case android.R.id.home: {
+                onUpPressed();
+                return true;
+            }
             case R.id.action_cancel:
                 mActivity.getStateManager().finishState(this);
                 return true;
@@ -504,6 +577,10 @@
                 }
                 return true;
             }
+            case R.id.action_camera: {
+                GalleryUtils.startCameraActivity((Activity) mActivity);
+                return true;
+            }
             default:
                 return false;
         }
@@ -516,18 +593,17 @@
                 // data could be null, if there is no images in the album
                 if (data == null) return;
                 mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
-                mAlbumView.setCenterIndex(mFocusIndex);
+                mSlotView.setCenterIndex(mFocusIndex);
                 break;
             }
             case REQUEST_PHOTO: {
                 if (data == null) return;
-                mFocusIndex = data.getIntExtra(PhotoPage.KEY_INDEX_HINT, 0);
-                mAlbumView.setCenterIndex(mFocusIndex);
-                startTransition();
+                mFocusIndex = data.getIntExtra(PhotoPage.KEY_RETURN_INDEX_HINT, 0);
+                mSlotView.makeSlotVisible(mFocusIndex);
                 break;
             }
             case REQUEST_DO_ANIMATION: {
-                startTransition(null);
+                mSlotView.startRisingAnimation();
                 break;
             }
         }
@@ -569,31 +645,33 @@
         ((Activity) mActivity).runOnUiThread(new Runnable() {
             @Override
             public void run() {
-                if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
-                    mInitialSynced = true;
-                }
-                if (!mIsActive) return;
-                clearLoadingBit(BIT_LOADING_SYNC);
-                if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
-                    Toast.makeText((Context) mActivity, R.string.sync_album_error,
-                            Toast.LENGTH_LONG).show();
+                GLRoot root = mActivity.getGLRoot();
+                root.lockRenderThread();
+                try {
+                    if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+                        mInitialSynced = true;
+                    }
+                    clearLoadingBit(BIT_LOADING_SYNC);
+                    if (resultCode == MediaSet.SYNC_RESULT_ERROR && mIsActive
+                            && (mAlbumDataAdapter.size() == 0)) {
+                        // show error toast only if the album is empty
+                        Toast.makeText((Context) mActivity, R.string.sync_album_error,
+                                Toast.LENGTH_LONG).show();
+                    }
+                } finally {
+                    root.unlockRenderThread();
                 }
             }
         });
     }
 
     private void setLoadingBit(int loadTaskBit) {
-        if (mLoadingBits == 0) {
-            GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
-        }
         mLoadingBits |= loadTaskBit;
     }
 
     private void clearLoadingBit(int loadTaskBit) {
         mLoadingBits &= ~loadTaskBit;
-        if (mLoadingBits == 0) {
-            GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
-
+        if (mLoadingBits == 0 && mIsActive) {
             if (mAlbumDataAdapter.size() == 0) {
                 Toast.makeText((Context) mActivity,
                         R.string.empty_album, Toast.LENGTH_LONG).show();
@@ -610,7 +688,6 @@
 
         @Override
         public void onLoadingFinished() {
-            if (!mIsActive) return;
             clearLoadingBit(BIT_LOADING_RELOAD);
         }
     }
@@ -643,7 +720,7 @@
         public MediaDetails getDetails() {
             MediaObject item = mAlbumDataAdapter.get(mIndex);
             if (item != null) {
-                mHighlightDrawer.setHighlightItem(item.getPath());
+                mAlbumView.setHighlightItemPath(item.getPath());
                 return item.getDetails();
             } else {
                 return null;
diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java
index ef5847e..7509e63 100644
--- a/src/com/android/gallery3d/app/AlbumPicker.java
+++ b/src/com/android/gallery3d/app/AlbumPicker.java
@@ -16,12 +16,12 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.data.DataManager;
-
 import android.content.Intent;
 import android.os.Bundle;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+
 public class AlbumPicker extends PickerActivity {
 
     public static final String KEY_ALBUM_PATH = "album-path";
diff --git a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
similarity index 83%
rename from src/com/android/gallery3d/app/AlbumSetDataAdapter.java
rename to src/com/android/gallery3d/app/AlbumSetDataLoader.java
index 5318a61..39d4a8b 100644
--- a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
+++ b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
@@ -18,6 +18,8 @@
 
 import android.os.Handler;
 import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
 
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.ContentListener;
@@ -25,7 +27,6 @@
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.ui.AlbumSetView;
 import com.android.gallery3d.ui.SynchronizedHandler;
 
 import java.util.Arrays;
@@ -33,23 +34,26 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.FutureTask;
 
-public class AlbumSetDataAdapter implements AlbumSetView.Model {
+public class AlbumSetDataLoader {
     @SuppressWarnings("unused")
     private static final String TAG = "AlbumSetDataAdapter";
 
     private static final int INDEX_NONE = -1;
 
     private static final int MIN_LOAD_COUNT = 4;
-    private static final int MAX_COVER_COUNT = 1;
 
     private static final int MSG_LOAD_START = 1;
     private static final int MSG_LOAD_FINISH = 2;
     private static final int MSG_RUN_OBJECT = 3;
 
-    private static final MediaItem[] EMPTY_MEDIA_ITEMS = new MediaItem[0];
+    public static interface DataListener {
+        public void onContentChanged(int index);
+        public void onSizeChanged(int size);
+    }
 
     private final MediaSet[] mData;
-    private final MediaItem[][] mCoverData;
+    private final MediaItem[] mCoverItem;
+    private final int[] mTotalCount;
     private final long[] mItemVersion;
     private final long[] mSetVersion;
 
@@ -63,7 +67,7 @@
     private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
     private int mSize;
 
-    private AlbumSetView.ModelListener mModelListener;
+    private DataListener mDataListener;
     private LoadingListener mLoadingListener;
     private ReloadTask mReloadTask;
 
@@ -71,10 +75,11 @@
 
     private final MySourceListener mSourceListener = new MySourceListener();
 
-    public AlbumSetDataAdapter(GalleryActivity activity, MediaSet albumSet, int cacheSize) {
+    public AlbumSetDataLoader(GalleryActivity activity, MediaSet albumSet, int cacheSize) {
         mSource = Utils.checkNotNull(albumSet);
-        mCoverData = new MediaItem[cacheSize][];
+        mCoverItem = new MediaItem[cacheSize];
         mData = new MediaSet[cacheSize];
+        mTotalCount = new int[cacheSize];
         mItemVersion = new long[cacheSize];
         mSetVersion = new long[cacheSize];
         Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
@@ -110,33 +115,32 @@
         mReloadTask.start();
     }
 
-    public MediaSet getMediaSet(int index) {
+    private void assertIsActive(int index) {
         if (index < mActiveStart && index >= mActiveEnd) {
             throw new IllegalArgumentException(String.format(
                     "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
         }
+    }
+
+    public MediaSet getMediaSet(int index) {
+        assertIsActive(index);
         return mData[index % mData.length];
     }
 
-    public MediaItem[] getCoverItems(int index) {
-        if (index < mActiveStart && index >= mActiveEnd) {
-            throw new IllegalArgumentException(String.format(
-                    "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
-        }
-        MediaItem[] result = mCoverData[index % mCoverData.length];
+    public MediaItem getCoverItem(int index) {
+        assertIsActive(index);
+        return mCoverItem[index % mCoverItem.length];
+    }
 
-        // If the result is not ready yet, return an empty array
-        return result == null ? EMPTY_MEDIA_ITEMS : result;
+    public int getTotalCount(int index) {
+        assertIsActive(index);
+        return mTotalCount[index % mTotalCount.length];
     }
 
     public int getActiveStart() {
         return mActiveStart;
     }
 
-    public int getActiveEnd() {
-        return mActiveEnd;
-    }
-
     public boolean isActive(int index) {
         return index >= mActiveStart && index < mActiveEnd;
     }
@@ -147,15 +151,15 @@
 
     private void clearSlot(int slotIndex) {
         mData[slotIndex] = null;
-        mCoverData[slotIndex] = null;
+        mCoverItem[slotIndex] = null;
+        mTotalCount[slotIndex] = 0;
         mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
         mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
     }
 
     private void setContentWindow(int contentStart, int contentEnd) {
         if (contentStart == mContentStart && contentEnd == mContentEnd) return;
-        MediaItem[][] data = mCoverData;
-        int length = data.length;
+        int length = mCoverItem.length;
 
         int start = this.mContentStart;
         int end = this.mContentEnd;
@@ -182,12 +186,12 @@
         if (start == mActiveStart && end == mActiveEnd) return;
 
         Utils.assertTrue(start <= end
-                && end - start <= mCoverData.length && end <= mSize);
+                && end - start <= mCoverItem.length && end <= mSize);
 
         mActiveStart = start;
         mActiveEnd = end;
 
-        int length = mCoverData.length;
+        int length = mCoverItem.length;
         // If no data is visible, keep the cache content
         if (start == end) return;
 
@@ -206,8 +210,8 @@
         }
     }
 
-    public void setModelListener(AlbumSetView.ModelListener listener) {
-        mModelListener = listener;
+    public void setModelListener(DataListener listener) {
+        mDataListener = listener;
     }
 
     public void setLoadingListener(LoadingListener listener) {
@@ -220,7 +224,8 @@
 
         public int size;
         public MediaSet item;
-        public MediaItem covers[];
+        public MediaItem cover;
+        public int totalCount;
     }
 
     private class GetUpdateInfo implements Callable<UpdateInfo> {
@@ -268,22 +273,23 @@
             mSourceVersion = info.version;
             if (mSize != info.size) {
                 mSize = info.size;
-                if (mModelListener != null) mModelListener.onSizeChanged(mSize);
+                if (mDataListener != null) mDataListener.onSizeChanged(mSize);
                 if (mContentEnd > mSize) mContentEnd = mSize;
                 if (mActiveEnd > mSize) mActiveEnd = mSize;
             }
             // Note: info.index could be INDEX_NONE, i.e., -1
             if (info.index >= mContentStart && info.index < mContentEnd) {
-                int pos = info.index % mCoverData.length;
+                int pos = info.index % mCoverItem.length;
                 mSetVersion[pos] = info.version;
                 long itemVersion = info.item.getDataVersion();
                 if (mItemVersion[pos] == itemVersion) return null;
                 mItemVersion[pos] = itemVersion;
                 mData[pos] = info.item;
-                mCoverData[pos] = info.covers;
-                if (mModelListener != null
+                mCoverItem[pos] = info.cover;
+                mTotalCount[pos] = info.totalCount;
+                if (mDataListener != null
                         && info.index >= mActiveStart && info.index < mActiveEnd) {
-                    mModelListener.onWindowContentChanged(info.index);
+                    mDataListener.onContentChanged(info.index);
                 }
             }
             return null;
@@ -317,11 +323,13 @@
 
         @Override
         public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
             boolean updateComplete = false;
             while (mActive) {
                 synchronized (this) {
                     if (mActive && !mDirty && updateComplete) {
-                        updateLoading(false);
+                        if (!mSource.isLoading()) updateLoading(false);
                         Utils.waitWithoutInterrupt(this);
                         continue;
                     }
@@ -331,7 +339,12 @@
 
                 long version;
                 synchronized (DataManager.LOCK) {
+                    long start = SystemClock.uptimeMillis();
                     version = mSource.reload();
+                    long duration = SystemClock.uptimeMillis() - start;
+                    if (duration > 20) {
+                        Log.v("DebugLoadingTime", "finish reload - " + duration);
+                    }
                 }
                 UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
                 updateComplete = info == null;
@@ -353,8 +366,8 @@
                     if (info.index != INDEX_NONE) {
                         info.item = mSource.getSubMediaSet(info.index);
                         if (info.item == null) continue;
-                        MediaItem cover = info.item.getCoverMediaItem();
-                        info.covers = cover == null ? new MediaItem[0] : new MediaItem[] {cover};
+                        info.cover = info.item.getCoverMediaItem();
+                        info.totalCount = info.item.getTotalMediaItemCount();
                     }
                 }
                 executeAndWait(new UpdateContent(info));
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
index e1dcade..aa72eb8 100644
--- a/src/com/android/gallery3d/app/AlbumSetPage.java
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -21,6 +21,8 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
 import android.os.Vibrator;
 import android.provider.MediaStore;
 import android.view.ActionMode;
@@ -40,21 +42,19 @@
 import com.android.gallery3d.settings.GallerySettings;
 import com.android.gallery3d.ui.ActionModeHandler;
 import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
-import com.android.gallery3d.ui.AlbumSetView;
+import com.android.gallery3d.ui.AlbumSetSlotRenderer;
 import com.android.gallery3d.ui.DetailsHelper;
 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.FadeTexture;
 import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.ui.GLView;
-import com.android.gallery3d.ui.GridDrawer;
-import com.android.gallery3d.ui.HighlightDrawer;
-import com.android.gallery3d.ui.PositionProvider;
-import com.android.gallery3d.ui.PositionRepository;
-import com.android.gallery3d.ui.PositionRepository.Position;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
-import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.HelpUtils;
 
 public class AlbumSetPage extends ActivityState implements
         SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
@@ -62,6 +62,8 @@
     @SuppressWarnings("unused")
     private static final String TAG = "AlbumSetPage";
 
+    private static final int MSG_PICK_ALBUM = 1;
+
     public static final String KEY_MEDIA_PATH = "media-path";
     public static final String KEY_SET_TITLE = "set-title";
     public static final String KEY_SET_SUBTITLE = "set-subtitle";
@@ -74,20 +76,19 @@
     private static final int BIT_LOADING_SYNC = 2;
 
     private boolean mIsActive = false;
-    private StaticBackground mStaticBackground;
-    private AlbumSetView mAlbumSetView;
+    private SlotView mSlotView;
+    private AlbumSetSlotRenderer mAlbumSetView;
 
     private MediaSet mMediaSet;
     private String mTitle;
     private String mSubtitle;
     private boolean mShowClusterMenu;
+    private GalleryActionBar mActionBar;
     private int mSelectedAction;
     private Vibrator mVibrator;
 
     protected SelectionManager mSelectionManager;
-    private AlbumSetDataAdapter mAlbumSetDataAdapter;
-    private GridDrawer mGridDrawer;
-    private HighlightDrawer mHighlightDrawer;
+    private AlbumSetDataLoader mAlbumSetDataAdapter;
 
     private boolean mGetContent;
     private boolean mGetAlbum;
@@ -97,6 +98,7 @@
     private MyDetailsSource mDetailsSource;
     private boolean mShowDetails;
     private EyePosition mEyePosition;
+    private Handler mHandler;
 
     // The eyes' position of the user, the origin is at the center of the
     // device and the unit is in pixels.
@@ -113,24 +115,26 @@
         private final float mMatrix[] = new float[16];
 
         @Override
+        protected void renderBackground(GLCanvas view) {
+            view.clearBuffer();
+        }
+
+        @Override
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
-            mStaticBackground.layout(0, 0, right - left, bottom - top);
             mEyePosition.resetPosition();
 
-            int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+            int slotViewTop = mActionBar.getHeight();
             int slotViewBottom = bottom - top;
             int slotViewRight = right - left;
 
             if (mShowDetails) {
                 mDetailsHelper.layout(left, slotViewTop, right, bottom);
             } else {
-                mAlbumSetView.setSelectionDrawer(mGridDrawer);
+                mAlbumSetView.setHighlightItemPath(null);
             }
 
-            mAlbumSetView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
-            PositionRepository.getInstance(mActivity).setOffset(
-                    0, slotViewTop);
+            mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
         }
 
         @Override
@@ -161,115 +165,105 @@
         } else if (mSelectionManager.inSelectionMode()) {
             mSelectionManager.leaveSelectionMode();
         } else {
-            mAlbumSetView.savePositions(
-                    PositionRepository.getInstance(mActivity));
             super.onBackPressed();
         }
     }
 
-    private void savePositions(int slotIndex, int center[]) {
+    private void getSlotCenter(int slotIndex, int center[]) {
         Rect offset = new Rect();
-        mRootPane.getBoundsOf(mAlbumSetView, offset);
-        mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
-        Rect r = mAlbumSetView.getSlotRect(slotIndex);
-        int scrollX = mAlbumSetView.getScrollX();
-        int scrollY = mAlbumSetView.getScrollY();
+        mRootPane.getBoundsOf(mSlotView, offset);
+        Rect r = mSlotView.getSlotRect(slotIndex);
+        int scrollX = mSlotView.getScrollX();
+        int scrollY = mSlotView.getScrollY();
         center[0] = offset.left + (r.left + r.right) / 2 - scrollX;
         center[1] = offset.top + (r.top + r.bottom) / 2 - scrollY;
     }
 
     public void onSingleTapUp(int slotIndex) {
+        if (!mIsActive) return;
+
+        if (mSelectionManager.inSelectionMode()) {
+            MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+            if (targetSet == null) return; // Content is dirty, we shall reload soon
+            mSelectionManager.toggle(targetSet.getPath());
+            mSlotView.invalidate();
+        } else {
+            // Show pressed-up animation for the single-tap.
+            mAlbumSetView.setPressedIndex(slotIndex);
+            mAlbumSetView.setPressedUp();
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_ALBUM, slotIndex, 0),
+                    FadeTexture.DURATION);
+        }
+    }
+
+    private void pickAlbum(int slotIndex) {
+        if (!mIsActive) return;
+
         MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
         if (targetSet == null) return; // Content is dirty, we shall reload soon
+        String mediaPath = targetSet.getPath().toString();
 
-        if (mShowDetails) {
-            Path path = targetSet.getPath();
-            mHighlightDrawer.setHighlightItem(path);
-            mDetailsHelper.reloadDetails(slotIndex);
-        } else if (!mSelectionManager.inSelectionMode()) {
-            Bundle data = new Bundle(getData());
-            String mediaPath = targetSet.getPath().toString();
-            int[] center = new int[2];
-            savePositions(slotIndex, center);
-            data.putIntArray(AlbumPage.KEY_SET_CENTER, center);
-            if (mGetAlbum && targetSet.isLeafAlbum()) {
-                Activity activity = (Activity) mActivity;
-                Intent result = new Intent()
-                        .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString());
-                activity.setResult(Activity.RESULT_OK, result);
-                activity.finish();
-            } else if (targetSet.getSubMediaSetCount() > 0) {
-                data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
-                mActivity.getStateManager().startStateForResult(
-                        AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
-            } else {
-                if (!mGetContent && (targetSet.getSupportedOperations()
-                        & MediaObject.SUPPORT_IMPORT) != 0) {
-                    data.putBoolean(AlbumPage.KEY_AUTO_SELECT_ALL, true);
-                }
-                data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath);
-                boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
-                // We only show cluster menu in the first AlbumPage in stack
-                data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum);
-                mActivity.getStateManager().startStateForResult(
-                        AlbumPage.class, REQUEST_DO_ANIMATION, data);
-            }
+        Bundle data = new Bundle(getData());
+        int[] center = new int[2];
+        getSlotCenter(slotIndex, center);
+        data.putIntArray(AlbumPage.KEY_SET_CENTER, center);
+        if (mGetAlbum && targetSet.isLeafAlbum()) {
+            Activity activity = (Activity) mActivity;
+            Intent result = new Intent()
+                    .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString());
+            activity.setResult(Activity.RESULT_OK, result);
+            activity.finish();
+        } else if (targetSet.getSubMediaSetCount() > 0) {
+            data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
+            mActivity.getStateManager().startStateForResult(
+                    AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
         } else {
-            mSelectionManager.toggle(targetSet.getPath());
-            mAlbumSetView.invalidate();
+            if (!mGetContent && (targetSet.getSupportedOperations()
+                    & MediaObject.SUPPORT_IMPORT) != 0) {
+                data.putBoolean(AlbumPage.KEY_AUTO_SELECT_ALL, true);
+            }
+            data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath);
+            boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+            // We only show cluster menu in the first AlbumPage in stack
+            data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum);
+            mActivity.getStateManager().startStateForResult(
+                    AlbumPage.class, REQUEST_DO_ANIMATION, data);
         }
     }
 
     private void onDown(int index) {
-        MediaSet set = mAlbumSetDataAdapter.getMediaSet(index);
-        Path path = (set == null) ? null : set.getPath();
-        mSelectionManager.setPressedPath(path);
-        mAlbumSetView.invalidate();
+        mAlbumSetView.setPressedIndex(index);
     }
 
-    private void onUp() {
-        mSelectionManager.setPressedPath(null);
-        mAlbumSetView.invalidate();
+    private void onUp(boolean followedByLongPress) {
+        if (followedByLongPress) {
+            // Avoid showing press-up animations for long-press.
+            mAlbumSetView.setPressedIndex(-1);
+        } else {
+            mAlbumSetView.setPressedUp();
+        }
     }
 
     public void onLongTap(int slotIndex) {
         if (mGetContent || mGetAlbum) return;
-        if (mShowDetails) {
-            onSingleTapUp(slotIndex);
-        } else {
-            MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex);
-            if (set == null) return;
-            mSelectionManager.setAutoLeaveSelectionMode(true);
-            mSelectionManager.toggle(set.getPath());
-            mDetailsSource.findIndex(slotIndex);
-            mAlbumSetView.invalidate();
-        }
+        MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+        if (set == null) return;
+        mSelectionManager.setAutoLeaveSelectionMode(true);
+        mSelectionManager.toggle(set.getPath());
+        mDetailsSource.findIndex(slotIndex);
+        mSlotView.invalidate();
     }
 
+    @Override
     public void doCluster(int clusterType) {
         String basePath = mMediaSet.getPath().toString();
         String newPath = FilterUtils.switchClusterPath(basePath, clusterType);
         Bundle data = new Bundle(getData());
         data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
         data.putInt(KEY_SELECTED_CLUSTER_TYPE, clusterType);
-        mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
         mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
     }
 
-    public void doFilter(int filterType) {
-        String basePath = mMediaSet.getPath().toString();
-        String newPath = FilterUtils.switchFilterPath(basePath, filterType);
-        Bundle data = new Bundle(getData());
-        data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
-        mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
-        mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
-    }
-
-    public void onOperationComplete() {
-        mAlbumSetView.invalidate();
-        // TODO: enable animation
-    }
-
     @Override
     public void onCreate(Bundle data, Bundle restoreState) {
         initializeViews();
@@ -282,25 +276,33 @@
         mEyePosition = new EyePosition(context, this);
         mDetailsSource = new MyDetailsSource();
         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
-        if (actionBar != null) {
-            mSelectedAction = data.getInt(
-                    AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE, FilterUtils.CLUSTER_BY_ALBUM);
-        }
-        startTransition();
+        mActionBar = mActivity.getGalleryActionBar();
+        mSelectedAction = data.getInt(AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE,
+                FilterUtils.CLUSTER_BY_ALBUM);
+
+        mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_PICK_ALBUM: {
+                        pickAlbum(message.arg1);
+                        break;
+                    }
+                    default: throw new AssertionError(message.what);
+                }
+            }
+        };
     }
 
     private void clearLoadingBit(int loadingBit) {
         mLoadingBits &= ~loadingBit;
-        if (mLoadingBits == 0) {
-            GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
-
+        if (mLoadingBits == 0 && mIsActive) {
             // Only show toast when there's no album and we are going to finish
             // the page. Toast is redundant if we are going to stay on this page.
             if ((mAlbumSetDataAdapter.size() == 0)) {
-                Toast.makeText((Context) mActivity,
-                        R.string.empty_album, Toast.LENGTH_LONG).show();
                 if (mActivity.getStateManager().getStateCount() > 1) {
+                    Toast.makeText((Context) mActivity,
+                            R.string.empty_album, Toast.LENGTH_LONG).show();
                     mActivity.getStateManager().finishState(this);
                 }
             }
@@ -308,9 +310,6 @@
     }
 
     private void setLoadingBit(int loadingBit) {
-        if (mLoadingBits == 0) {
-            GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
-        }
         mLoadingBits |= loadingBit;
     }
 
@@ -323,8 +322,10 @@
         mAlbumSetView.pause();
         mEyePosition.pause();
         DetailsHelper.pause();
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
-        if (actionBar != null) actionBar.hideClusterMenu();
+        // Call disableClusterMenu to avoid receiving callback after paused.
+        // Don't hide menu here otherwise the list menu will disappear earlier than
+        // the action bar, which is janky and unwanted behavior.
+        mActionBar.disableClusterMenu(false);
         if (mSyncTask != null) {
             mSyncTask.cancel();
             mSyncTask = null;
@@ -345,9 +346,8 @@
         mAlbumSetView.resume();
         mEyePosition.resume();
         mActionModeHandler.resume();
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
-        if (mShowClusterMenu && actionBar != null) {
-            actionBar.showClusterMenu(mSelectedAction, this);
+        if (mShowClusterMenu) {
+            mActionBar.enableClusterMenu(mSelectedAction, this);
         }
         if (!mInitialSynced) {
             setLoadingBit(BIT_LOADING_SYNC);
@@ -359,7 +359,7 @@
         String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);
         mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
         mSelectionManager.setSourceMediaSet(mMediaSet);
-        mAlbumSetDataAdapter = new AlbumSetDataAdapter(
+        mAlbumSetDataAdapter = new AlbumSetDataLoader(
                 mActivity, mMediaSet, DATA_CACHE_SIZE);
         mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener());
         mAlbumSetView.setModel(mAlbumSetDataAdapter);
@@ -368,22 +368,21 @@
     private void initializeViews() {
         mSelectionManager = new SelectionManager(mActivity, true);
         mSelectionManager.setSelectionListener(this);
-        mStaticBackground = new StaticBackground(mActivity.getAndroidContext());
-        mRootPane.addComponent(mStaticBackground);
 
-        mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
         Config.AlbumSetPage config = Config.AlbumSetPage.get((Context) mActivity);
-        mAlbumSetView = new AlbumSetView(mActivity, mGridDrawer,
-                config.slotViewSpec, config.labelSpec);
-        mAlbumSetView.setListener(new SlotView.SimpleListener() {
+        mSlotView = new SlotView(mActivity, config.slotViewSpec);
+        mAlbumSetView = new AlbumSetSlotRenderer(
+                mActivity, mSelectionManager, mSlotView, config.labelSpec);
+        mSlotView.setSlotRenderer(mAlbumSetView);
+        mSlotView.setListener(new SlotView.SimpleListener() {
             @Override
             public void onDown(int index) {
                 AlbumSetPage.this.onDown(index);
             }
 
             @Override
-            public void onUp() {
-                AlbumSetPage.this.onUp();
+            public void onUp(boolean followedByLongPress) {
+                AlbumSetPage.this.onUp(followedByLongPress);
             }
 
             @Override
@@ -399,20 +398,17 @@
 
         mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
         mActionModeHandler.setActionModeListener(new ActionModeListener() {
+            @Override
             public boolean onActionItemClicked(MenuItem item) {
                 return onItemSelected(item);
             }
         });
-        mRootPane.addComponent(mAlbumSetView);
-
-        mStaticBackground.setImage(R.drawable.background,
-                R.drawable.background_portrait);
+        mRootPane.addComponent(mSlotView);
     }
 
     @Override
     protected boolean onCreateActionBar(Menu menu) {
         Activity activity = (Activity) mActivity;
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
         MenuInflater inflater = activity.getMenuInflater();
 
         final boolean inAlbum = mActivity.getStateManager().hasStateClass(
@@ -428,10 +424,10 @@
                         ? R.string.select_video
                         : R.string.select_item;
             }
-            actionBar.setTitle(id);
+            mActionBar.setTitle(id);
         } else  if (mGetAlbum) {
             inflater.inflate(R.menu.pickup, menu);
-            actionBar.setTitle(R.string.select_album);
+            mActionBar.setTitle(R.string.select_album);
         } else {
             mShowClusterMenu = !inAlbum;
             inflater.inflate(R.menu.albumset, menu);
@@ -439,7 +435,7 @@
 
             if (selectItem != null) {
                 boolean selectAlbums = !inAlbum &&
-                        actionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM;
+                        mActionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM;
                 if (selectAlbums) {
                     selectItem.setTitle(R.string.select_album);
                 } else {
@@ -447,14 +443,17 @@
                 }
             }
 
-            FilterUtils.setupMenuItems(actionBar, mMediaSet.getPath(), false);
+            FilterUtils.setupMenuItems(mActionBar, mMediaSet.getPath(), false);
             MenuItem switchCamera = menu.findItem(R.id.action_camera);
             if (switchCamera != null) {
                 switchCamera.setVisible(GalleryUtils.isCameraAvailable(activity));
             }
+            final MenuItem helpMenu = menu.findItem(R.id.action_general_help);
+            HelpUtils.prepareHelpMenuItem(mActivity.getAndroidContext(),
+                    helpMenu, R.string.help_url_gallery_main);
 
-            actionBar.setTitle(mTitle);
-            actionBar.setSubtitle(mSubtitle);
+            mActionBar.setTitle(mTitle);
+            mActionBar.setSubtitle(mSubtitle);
         }
         return true;
     }
@@ -485,10 +484,7 @@
                 }
                 return true;
             case R.id.action_camera: {
-                Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
-                        .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
-                        | Intent.FLAG_ACTIVITY_NEW_TASK);
-                activity.startActivity(intent);
+                GalleryUtils.startCameraActivity(activity);
                 return true;
             }
             case R.id.action_manage_offline: {
@@ -516,31 +512,14 @@
     protected void onStateResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
             case REQUEST_DO_ANIMATION: {
-                startTransition();
+                mSlotView.startRisingAnimation();
             }
         }
     }
 
-    private void startTransition() {
-        final PositionRepository repository =
-                PositionRepository.getInstance(mActivity);
-        mAlbumSetView.startTransition(new PositionProvider() {
-            private final Position mTempPosition = new Position();
-            public Position getPosition(long identity, Position target) {
-                Position p = repository.get(identity);
-                if (p == null) {
-                    p = mTempPosition;
-                    p.set(target.x, target.y, 128, target.theta, 1);
-                }
-                return p;
-            }
-        });
-    }
-
     private String getSelectedString() {
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
         int count = mSelectionManager.getSelectedCount();
-        int action = actionBar.getClusterTypeAction();
+        int action = mActionBar.getClusterTypeAction();
         int string = action == FilterUtils.CLUSTER_BY_ALBUM
                 ? R.plurals.number_of_albums_selected
                 : R.plurals.number_of_groups_selected;
@@ -548,11 +527,11 @@
         return String.format(format, count);
     }
 
+    @Override
     public void onSelectionModeChange(int mode) {
-
         switch (mode) {
             case SelectionManager.ENTER_SELECTION_MODE: {
-                mActivity.getGalleryActionBar().hideClusterMenu();
+                mActionBar.disableClusterMenu(true);
                 mActionMode = mActionModeHandler.startActionMode();
                 mVibrator.vibrate(100);
                 break;
@@ -560,19 +539,20 @@
             case SelectionManager.LEAVE_SELECTION_MODE: {
                 mActionMode.finish();
                 if (mShowClusterMenu) {
-                    mActivity.getGalleryActionBar().showClusterMenu(mSelectedAction, this);
+                    mActionBar.enableClusterMenu(mSelectedAction, this);
                 }
                 mRootPane.invalidate();
                 break;
             }
             case SelectionManager.SELECT_ALL_MODE: {
-                mActionModeHandler.setTitle(getSelectedString());
+                mActionModeHandler.updateSupportedOperation();
                 mRootPane.invalidate();
                 break;
             }
         }
     }
 
+    @Override
     public void onSelectionChange(Path path, boolean selected) {
         Utils.assertTrue(mActionMode != null);
         mActionModeHandler.setTitle(getSelectedString());
@@ -582,23 +562,21 @@
     private void hideDetails() {
         mShowDetails = false;
         mDetailsHelper.hide();
-        mAlbumSetView.setSelectionDrawer(mGridDrawer);
-        mAlbumSetView.invalidate();
+        mAlbumSetView.setHighlightItemPath(null);
+        mSlotView.invalidate();
     }
 
     private void showDetails() {
         mShowDetails = true;
         if (mDetailsHelper == null) {
-            mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext(),
-                    mSelectionManager);
             mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
             mDetailsHelper.setCloseListener(new CloseListener() {
+                @Override
                 public void onClose() {
                     hideDetails();
                 }
             });
         }
-        mAlbumSetView.setSelectionDrawer(mHighlightDrawer);
         mDetailsHelper.show();
     }
 
@@ -611,42 +589,51 @@
         ((Activity) mActivity).runOnUiThread(new Runnable() {
             @Override
             public void run() {
-                if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
-                    mInitialSynced = true;
-                }
-                if (!mIsActive) return;
-                clearLoadingBit(BIT_LOADING_SYNC);
-                if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
-                    Toast.makeText((Context) mActivity, R.string.sync_album_set_error,
-                            Toast.LENGTH_LONG).show();
+                GLRoot root = mActivity.getGLRoot();
+                root.lockRenderThread();
+                try {
+                    if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+                        mInitialSynced = true;
+                    }
+                    clearLoadingBit(BIT_LOADING_SYNC);
+                    if (resultCode == MediaSet.SYNC_RESULT_ERROR && mIsActive) {
+                        Log.w(TAG, "failed to load album set");
+                    }
+                } finally {
+                    root.unlockRenderThread();
                 }
             }
         });
     }
 
     private class MyLoadingListener implements LoadingListener {
+        @Override
         public void onLoadingStarted() {
             setLoadingBit(BIT_LOADING_RELOAD);
         }
 
+        @Override
         public void onLoadingFinished() {
-            if (!mIsActive) return;
             clearLoadingBit(BIT_LOADING_RELOAD);
         }
     }
 
     private class MyDetailsSource implements DetailsHelper.DetailsSource {
         private int mIndex;
+
+        @Override
         public int size() {
             return mAlbumSetDataAdapter.size();
         }
 
+        @Override
         public int getIndex() {
             return mIndex;
         }
 
         // If requested index is out of active window, suggest a valid index.
         // If there is no valid index available, return -1.
+        @Override
         public int findIndex(int indexHint) {
             if (mAlbumSetDataAdapter.isActive(indexHint)) {
                 mIndex = indexHint;
@@ -659,10 +646,11 @@
             return mIndex;
         }
 
+        @Override
         public MediaDetails getDetails() {
             MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex);
             if (item != null) {
-                mHighlightDrawer.setHighlightItem(item.getPath());
+                mAlbumSetView.setHighlightItemPath(item.getPath());
                 return item.getDetails();
             } else {
                 return null;
diff --git a/src/com/android/gallery3d/app/AppBridge.java b/src/com/android/gallery3d/app/AppBridge.java
new file mode 100644
index 0000000..5d6b1ec
--- /dev/null
+++ b/src/com/android/gallery3d/app/AppBridge.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.app;
+
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.gallery3d.ui.ScreenNail;
+
+// This is the bridge to connect a PhotoPage to the external environment.
+public abstract class AppBridge implements Parcelable {
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  These are requests sent from PhotoPage to the app
+    //////////////////////////////////////////////////////////////////////////
+
+    public abstract boolean isPanorama();
+    public abstract ScreenNail attachScreenNail();
+    public abstract void detachScreenNail();
+
+    // Return true if the tap is consumed.
+    public abstract boolean onSingleTapUp(int x, int y);
+
+    // This is used to notify that the screen nail will be drawn in full screen
+    // or not in next draw() call.
+    public abstract void onFullScreenChanged(boolean full);
+
+    //////////////////////////////////////////////////////////////////////////
+    //  These are requests send from app to PhotoPage
+    //////////////////////////////////////////////////////////////////////////
+
+    public interface Server {
+        // Set the camera frame relative to GLRootView.
+        public void setCameraRelativeFrame(Rect frame);
+        // Switch to the previous or next picture using the capture animation.
+        // The offset is -1 to switch to the previous picture, 1 to switch to
+        // the next picture.
+        public boolean switchWithCaptureAnimation(int offset);
+        // Enable or disable the swiping gestures (the default is enabled).
+        public void setSwipingEnabled(boolean enabled);
+        // Notify that the ScreenNail is changed.
+        public void notifyScreenNailChanged();
+    }
+
+    // If server is null, the services are not available.
+    public abstract void setServer(Server server);
+}
diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java
index 914ea55..a95427b 100644
--- a/src/com/android/gallery3d/app/Config.java
+++ b/src/com/android/gallery3d/app/Config.java
@@ -16,19 +16,19 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ui.SlotView;
-import com.android.gallery3d.ui.AlbumSetView;
-
 import android.content.Context;
 import android.content.res.Resources;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.AlbumSetSlotRenderer;
+import com.android.gallery3d.ui.SlotView;
+
 final class Config {
     public static class AlbumSetPage {
         private static AlbumSetPage sInstance;
 
         public SlotView.Spec slotViewSpec;
-        public AlbumSetView.LabelSpec labelSpec;
+        public AlbumSetSlotRenderer.LabelSpec labelSpec;
 
         public static synchronized AlbumSetPage get(Context context) {
             if (sInstance == null) {
@@ -45,7 +45,7 @@
             slotViewSpec.rowsPort = r.getInteger(R.integer.albumset_rows_port);
             slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.albumset_slot_gap);
 
-            labelSpec = new AlbumSetView.LabelSpec();
+            labelSpec = new AlbumSetSlotRenderer.LabelSpec();
             labelSpec.labelBackgroundHeight = r.getDimensionPixelSize(
                     R.dimen.albumset_label_background_height);
             labelSpec.titleOffset = r.getDimensionPixelSize(
@@ -105,41 +105,5 @@
             cachePinMargin = r.getDimensionPixelSize(R.dimen.cache_pin_margin);
         }
     }
-
-    public static class PhotoPage {
-        private static PhotoPage sInstance;
-
-        // These are all height values. See the comment in FilmStripView for
-        // the meaning of these values.
-        public final int filmstripTopMargin;
-        public final int filmstripMidMargin;
-        public final int filmstripBottomMargin;
-        public final int filmstripThumbSize;
-        public final int filmstripContentSize;
-        public final int filmstripGripSize;
-        public final int filmstripBarSize;
-
-        // These are width values.
-        public final int filmstripGripWidth;
-
-        public static synchronized PhotoPage get(Context context) {
-            if (sInstance == null) {
-                sInstance = new PhotoPage(context);
-            }
-            return sInstance;
-        }
-
-        public PhotoPage(Context context) {
-            Resources r = context.getResources();
-            filmstripTopMargin = r.getDimensionPixelSize(R.dimen.filmstrip_top_margin);
-            filmstripMidMargin = r.getDimensionPixelSize(R.dimen.filmstrip_mid_margin);
-            filmstripBottomMargin = r.getDimensionPixelSize(R.dimen.filmstrip_bottom_margin);
-            filmstripThumbSize = r.getDimensionPixelSize(R.dimen.filmstrip_thumb_size);
-            filmstripContentSize = r.getDimensionPixelSize(R.dimen.filmstrip_content_size);
-            filmstripGripSize = r.getDimensionPixelSize(R.dimen.filmstrip_grip_size);
-            filmstripBarSize = r.getDimensionPixelSize(R.dimen.filmstrip_bar_size);
-            filmstripGripWidth = r.getDimensionPixelSize(R.dimen.filmstrip_grip_width);
-        }
-    }
 }
 
diff --git a/src/com/android/gallery3d/app/ControllerOverlay.java b/src/com/android/gallery3d/app/ControllerOverlay.java
index 847d9e6..0347213 100644
--- a/src/com/android/gallery3d/app/ControllerOverlay.java
+++ b/src/com/android/gallery3d/app/ControllerOverlay.java
@@ -51,10 +51,5 @@
 
   void showErrorMessage(String message);
 
-  void hide();
-
   void setTimes(int currentTime, int totalTime);
-
-  void resetTime();
-
 }
diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java
index 997f1fd..4f450d8 100644
--- a/src/com/android/gallery3d/app/CropImage.java
+++ b/src/com/android/gallery3d/app/CropImage.java
@@ -38,6 +38,7 @@
 import android.os.Message;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Images;
+import android.util.FloatMath;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.Window;
@@ -57,6 +58,7 @@
 import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.BucketNames;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.GalleryUtils;
@@ -119,9 +121,8 @@
     private static final int STATE_LOADED = 1;
     private static final int STATE_SAVING = 2;
 
-    public static final String DOWNLOAD_STRING = "download";
     public static final File DOWNLOAD_BUCKET = new File(
-            Environment.getExternalStorageDirectory(), DOWNLOAD_STRING);
+            Environment.getExternalStorageDirectory(), BucketNames.DOWNLOAD);
 
     public static final String CROP_ACTION = "com.android.camera.action.CROP";
 
@@ -162,8 +163,12 @@
         getGLRoot().setContentPane(mCropView);
 
         ActionBar actionBar = getActionBar();
-        actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
-                ActionBar.DISPLAY_HOME_AS_UP);
+        actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE,
+                ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
+        Bundle extra = getIntent().getExtras();
+        if (extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
+            actionBar.setTitle(R.string.set_wallpaper);
+        }
 
         mMainHandler = new SynchronizedHandler(getGLRoot()) {
             @Override
@@ -232,6 +237,11 @@
         return true;
     }
 
+    @Override
+    public void onBackPressed() {
+        finish();
+    }
+
     private class SaveOutput implements Job<Intent> {
         private final RectF mCropRect;
 
@@ -554,8 +564,7 @@
         }
 
         if (outputX * outputY > MAX_PIXEL_COUNT) {
-            float scale = (float) Math.sqrt(
-                    (double) MAX_PIXEL_COUNT / outputX / outputY);
+            float scale = FloatMath.sqrt((float) MAX_PIXEL_COUNT / outputX / outputY);
             Log.w(TAG, "scale down the cropped image: " + scale);
             outputX = Math.round(scale * outputX);
             outputY = Math.round(scale * outputY);
@@ -603,8 +612,14 @@
             int sample = BitmapUtils.computeSampleSizeLarger(
                     Math.max(scaleX, scaleY));
             options.inSampleSize = sample;
+
+            // The decoding result is what we want if
+            //   1. The size of the decoded bitmap match the destination's size
+            //   2. The destination covers the whole output bitmap
+            //   3. No rotation
             if ((rect.width() / sample) == dest.width()
                     && (rect.height() / sample) == dest.height()
+                    && (outputX == dest.width()) && (outputY == dest.height())
                     && rotation == 0) {
                 // To prevent concurrent access in GLThread
                 synchronized (mRegionDecoder) {
@@ -708,7 +723,7 @@
             BitmapRegionDecoder regionDecoder) {
 
         if (regionDecoder == null) {
-            Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
+            Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
             finish();
             return;
         }
@@ -734,7 +749,7 @@
 
     private void onBitmapAvailable(Bitmap bitmap) {
         if (bitmap == null) {
-            Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
+            Toast.makeText(this, R.string.fail_to_load_image, Toast.LENGTH_SHORT).show();
             finish();
             return;
         }
@@ -887,13 +902,9 @@
     private MediaItem getMediaItemFromIntentData() {
         Uri uri = getIntent().getData();
         DataManager manager = getDataManager();
-        if (uri == null) {
-            Log.w(TAG, "no data given");
-            return null;
-        }
-        Path path = manager.findPathByUri(uri);
+        Path path = manager.findPathByUri(uri, getIntent().getType());
         if (path == null) {
-            Log.w(TAG, "cannot get path for: " + uri);
+            Log.w(TAG, "cannot get path for: " + uri + ", or no data given");
             return null;
         }
         return (MediaItem) manager.getMediaObject(path);
diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java
index 8a57824..7ca86e5 100644
--- a/src/com/android/gallery3d/app/DialogPicker.java
+++ b/src/com/android/gallery3d/app/DialogPicker.java
@@ -16,12 +16,11 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.Intent;
 import android.os.Bundle;
 
+import com.android.gallery3d.util.GalleryUtils;
+
 public class DialogPicker extends PickerActivity {
 
     @Override
diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java
index 7b4495a..89e0846 100644
--- a/src/com/android/gallery3d/app/EyePosition.java
+++ b/src/com/android/gallery3d/app/EyePosition.java
@@ -16,19 +16,20 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.os.SystemClock;
+import android.util.FloatMath;
 import android.view.Display;
 import android.view.Surface;
 import android.view.WindowManager;
 
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
 public class EyePosition {
     private static final String TAG = "EyePosition";
 
@@ -41,9 +42,9 @@
     private static final int GYROSCOPE_SETTLE_DOWN = 15;
     private static final float GYROSCOPE_RESTORE_FACTOR = 0.995f;
 
-    private static final double USER_ANGEL = Math.toRadians(10);
-    private static final float USER_ANGEL_COS = (float) Math.cos(USER_ANGEL);
-    private static final float USER_ANGEL_SIN = (float) Math.sin(USER_ANGEL);
+    private static final float USER_ANGEL = (float) Math.toRadians(10);
+    private static final float USER_ANGEL_COS = FloatMath.cos(USER_ANGEL);
+    private static final float USER_ANGEL_SIN = FloatMath.sin(USER_ANGEL);
     private static final float MAX_VIEW_RANGE = (float) 0.5;
     private static final int NOT_STARTED = -1;
 
@@ -126,8 +127,8 @@
         float ty = -1 + t * y;
         float tz = t * z;
 
-        float length = (float) Math.sqrt(tx * tx + ty * ty + tz * tz);
-        float glength = (float) Math.sqrt(temp);
+        float length = FloatMath.sqrt(tx * tx + ty * ty + tz * tz);
+        float glength = FloatMath.sqrt(temp);
 
         mX = Utils.clamp((x * USER_ANGEL_COS / glength
                 + tx * USER_ANGEL_SIN / length) * mUserDistance,
@@ -135,7 +136,7 @@
         mY = -Utils.clamp((y * USER_ANGEL_COS / glength
                 + ty * USER_ANGEL_SIN / length) * mUserDistance,
                 -mLimit, mLimit);
-        mZ = (float) -Math.sqrt(
+        mZ = -FloatMath.sqrt(
                 mUserDistance * mUserDistance - mX * mX - mY * mY);
         mListener.onEyePositionChanged(mX, mY, mZ);
     }
@@ -173,7 +174,7 @@
         mY = Utils.clamp((float) (mY + y * t / Math.hypot(mZ, mY)),
                 -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR;
 
-        mZ = (float) -Math.sqrt(
+        mZ = -FloatMath.sqrt(
                 mUserDistance * mUserDistance - mX * mX - mY * mY);
         mListener.onEyePositionChanged(mX, mY, mZ);
     }
diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java
index 9b8ea2d..c15457e 100644
--- a/src/com/android/gallery3d/app/FilterUtils.java
+++ b/src/com/android/gallery3d/app/FilterUtils.java
@@ -214,11 +214,6 @@
         return "/cluster/{" + base + "}/" + kind;
     }
 
-    // Change the topmost filter to the specified type.
-    public static String switchFilterPath(String base, int filterType) {
-        return newFilterPath(removeOneFilterFromPath(base), filterType);
-    }
-
     // Change the topmost clustering to the specified type.
     public static String switchClusterPath(String base, int clusterType) {
         return newClusterPath(removeOneClusterFromPath(base), clusterType);
@@ -258,39 +253,4 @@
         }
         return sb.toString();
     }
-
-    // Remove the topmost filter (if any) from the path.
-    private static String removeOneFilterFromPath(String base) {
-        boolean[] done = new boolean[1];
-        return removeOneFilterFromPath(base, done);
-    }
-
-    private static String removeOneFilterFromPath(String base, boolean[] done) {
-        if (done[0]) return base;
-
-        String[] segments = Path.split(base);
-        if (segments[0].equals("filter") && segments[1].equals("mediatype")) {
-            done[0] = true;
-            return Path.splitSequence(segments[3])[0];
-        }
-
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < segments.length; i++) {
-            sb.append("/");
-            if (segments[i].startsWith("{")) {
-                sb.append("{");
-                String[] sets = Path.splitSequence(segments[i]);
-                for (int j = 0; j < sets.length; j++) {
-                    if (j > 0) {
-                        sb.append(",");
-                    }
-                    sb.append(removeOneFilterFromPath(sets[j], done));
-                }
-                sb.append("}");
-            } else {
-                sb.append(segments[i]);
-            }
-        }
-        return sb.toString();
-    }
 }
diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java
index 253af2b..468d202 100644
--- a/src/com/android/gallery3d/app/Gallery.java
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -17,14 +17,16 @@
 package com.android.gallery3d.app;
 
 import android.app.Dialog;
+import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
 import android.content.Intent;
+import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.provider.OpenableColumns;
 import android.view.Menu;
-import android.view.MenuItem;
 import android.view.Window;
 import android.widget.Toast;
 
@@ -40,6 +42,7 @@
 
 public final class Gallery extends AbstractGalleryActivity implements OnCancelListener {
     public static final String EXTRA_SLIDESHOW = "slideshow";
+    public static final String EXTRA_DREAM = "dream";
     public static final String EXTRA_CROP = "crop";
 
     public static final String ACTION_REVIEW = "com.android.camera.action.REVIEW";
@@ -49,7 +52,6 @@
     public static final String KEY_MEDIA_TYPES = "mediaTypes";
 
     private static final String TAG = "Gallery";
-    private GalleryActionBar mActionBar;
     private Dialog mVersionCheckDialog;
 
     @Override
@@ -57,10 +59,8 @@
         super.onCreate(savedInstanceState);
         requestWindowFeature(Window.FEATURE_ACTION_BAR);
         requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
-        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
 
         setContentView(R.layout.main);
-        mActionBar = new GalleryActionBar(this);
 
         if (savedInstanceState != null) {
             getStateManager().restoreFromState(savedInstanceState);
@@ -115,7 +115,6 @@
         data.putInt(KEY_TYPE_BITS, typeBits);
         data.putString(AlbumSetPage.KEY_MEDIA_PATH,
                 getDataManager().getTopSetPath(typeBits));
-        getStateManager().setLaunchGalleryOnTop(true);
         getStateManager().startState(AlbumSetPage.class, data);
     }
 
@@ -134,11 +133,10 @@
 
     private void startViewAction(Intent intent) {
         Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
-        getStateManager().setLaunchGalleryOnTop(true);
         if (slideshow) {
             getActionBar().hide();
             DataManager manager = getDataManager();
-            Path path = manager.findPathByUri(intent.getData());
+            Path path = manager.findPathByUri(intent.getData(), intent.getType());
             if (path == null || manager.getMediaObject(path)
                     instanceof MediaItem) {
                 path = Path.fromString(
@@ -148,6 +146,9 @@
             data.putString(SlideshowPage.KEY_SET_PATH, path.toString());
             data.putBoolean(SlideshowPage.KEY_RANDOM_ORDER, true);
             data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+            if (intent.getBooleanExtra(EXTRA_DREAM, false)) {
+                data.putBoolean(SlideshowPage.KEY_DREAM, true);
+            }
             getStateManager().startState(SlideshowPage.class, data);
         } else {
             Bundle data = new Bundle();
@@ -165,7 +166,6 @@
                 data.putInt(KEY_TYPE_BITS, typeBits);
                 data.putString(AlbumSetPage.KEY_MEDIA_PATH,
                         getDataManager().getTopSetPath(typeBits));
-                getStateManager().setLaunchGalleryOnTop(true);
                 getStateManager().startState(AlbumSetPage.class, data);
             } else if (contentType.startsWith(
                     ContentResolver.CURSOR_DIR_BASE_TYPE)) {
@@ -175,7 +175,7 @@
                             KEY_MEDIA_TYPES, String.valueOf(mediaType))
                             .build();
                 }
-                Path setPath = dm.findPathByUri(uri);
+                Path setPath = dm.findPathByUri(uri, null);
                 MediaSet mediaSet = null;
                 if (setPath != null) {
                     mediaSet = (MediaSet) dm.getMediaObject(setPath);
@@ -183,6 +183,8 @@
                 if (mediaSet != null) {
                     if (mediaSet.isLeafAlbum()) {
                         data.putString(AlbumPage.KEY_MEDIA_PATH, setPath.toString());
+                        data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+                                dm.getTopSetPath(DataManager.INCLUDE_ALL));
                         getStateManager().startState(AlbumPage.class, data);
                     } else {
                         data.putString(AlbumSetPage.KEY_MEDIA_PATH, setPath.toString());
@@ -192,15 +194,38 @@
                     startDefaultPage();
                 }
             } else {
-                Path itemPath = dm.findPathByUri(uri);
+                Path itemPath = dm.findPathByUri(uri, intent.getType());
                 Path albumPath = dm.getDefaultSetOf(itemPath);
                 // TODO: Make this parameter public so other activities can reference it.
                 boolean singleItemOnly = intent.getBooleanExtra("SingleItemOnly", false);
-                if (!singleItemOnly && albumPath != null) {
-                    data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
-                            albumPath.toString());
+                if (!singleItemOnly && (albumPath != null)) {
+                    data.putString(PhotoPage.KEY_MEDIA_SET_PATH, albumPath.toString());
                 }
                 data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString());
+                if (intent.getBooleanExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, false)) {
+                    data.putBoolean(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+                }
+
+                // Displays the filename as title, reading the filename from the interface:
+                // {@link android.provider.OpenableColumns#DISPLAY_NAME}.
+                AsyncQueryHandler queryHandler = new AsyncQueryHandler(getContentResolver()) {
+                    @Override
+                    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                        try {
+                            if ((cursor != null) && cursor.moveToFirst()) {
+                                String displayName = cursor.getString(0);
+
+                                // Just show empty title if other apps don't set DISPLAY_NAME
+                                setTitle((displayName == null) ? "" : displayName);
+                            }
+                        } finally {
+                            Utils.closeSilently(cursor);
+                        }
+                    }
+                };
+                queryHandler.startQuery(0, null, uri, new String[] {OpenableColumns.DISPLAY_NAME},
+                        null, null, null);
+
                 getStateManager().startState(PhotoPage.class, data);
             }
         }
@@ -213,41 +238,6 @@
     }
 
     @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            return getStateManager().itemSelected(item);
-        } finally {
-            root.unlockRenderThread();
-        }
-    }
-
-    @Override
-    public void onBackPressed() {
-        // send the back event to the top sub-state
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            getStateManager().onBackPressed();
-        } finally {
-            root.unlockRenderThread();
-        }
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            getStateManager().destroy();
-        } finally {
-            root.unlockRenderThread();
-        }
-    }
-
-    @Override
     protected void onResume() {
         Utils.assertTrue(getStateManager().getStateCount() > 0);
         super.onResume();
@@ -265,11 +255,6 @@
     }
 
     @Override
-    public GalleryActionBar getGalleryActionBar() {
-        return mActionBar;
-    }
-
-    @Override
     public void onCancel(DialogInterface dialog) {
         if (dialog == mVersionCheckDialog) {
             mVersionCheckDialog = null;
diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java
index 717f16c..5c61648 100644
--- a/src/com/android/gallery3d/app/GalleryActionBar.java
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -16,11 +16,10 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-
 import android.app.ActionBar;
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.ActionBar.OnMenuVisibilityListener;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.view.LayoutInflater;
@@ -32,11 +31,23 @@
 import android.widget.ShareActionProvider;
 import android.widget.TextView;
 
+import com.android.gallery3d.R;
+
 import java.util.ArrayList;
 
 public class GalleryActionBar implements ActionBar.OnNavigationListener {
     private static final String TAG = "GalleryActionBar";
 
+    private ClusterRunner mClusterRunner;
+    private CharSequence[] mTitles;
+    private ArrayList<Integer> mActions;
+    private Context mContext;
+    private LayoutInflater mInflater;
+    private GalleryActivity mActivity;
+    private ActionBar mActionBar;
+    private int mCurrentIndex;
+    private ClusterAdapter mAdapter = new ClusterAdapter();
+
     public interface ClusterRunner {
         public void doCluster(int id);
     }
@@ -103,15 +114,23 @@
         }
     }
 
-    private ClusterRunner mClusterRunner;
-    private CharSequence[] mTitles;
-    private ArrayList<Integer> mActions;
-    private Context mContext;
-    private LayoutInflater mInflater;
-    private GalleryActivity mActivity;
-    private ActionBar mActionBar;
-    private int mCurrentIndex;
-    private ClusterAdapter mAdapter = new ClusterAdapter();
+    public static String getClusterByTypeString(Context context, int type) {
+        for (ActionItem item : sClusterItems) {
+            if (item.action == type) {
+                return context.getString(item.clusterBy);
+            }
+        }
+        return null;
+    }
+
+    public static ShareActionProvider initializeShareActionProvider(Menu menu) {
+        MenuItem item = menu.findItem(R.id.action_share);
+        ShareActionProvider shareActionProvider = null;
+        if (item != null) {
+            shareActionProvider = (ShareActionProvider) item.getActionProvider();
+        }
+        return shareActionProvider;
+    }
 
     public GalleryActionBar(GalleryActivity activity) {
         mActionBar = ((Activity) activity).getActionBar();
@@ -121,11 +140,6 @@
         mCurrentIndex = 0;
     }
 
-    public static int getHeight(Activity activity) {
-        ActionBar actionBar = activity.getActionBar();
-        return actionBar != null ? actionBar.getHeight() : 0;
-    }
-
     private void createDialogData() {
         ArrayList<CharSequence> titles = new ArrayList<CharSequence>();
         mActions = new ArrayList<Integer>();
@@ -139,6 +153,10 @@
         titles.toArray(mTitles);
     }
 
+    public int getHeight() {
+        return mActionBar != null ? mActionBar.getHeight() : 0;
+    }
+
     public void setClusterItemEnabled(int id, boolean enabled) {
         for (ActionItem item : sClusterItems) {
             if (item.action == id) {
@@ -161,37 +179,27 @@
         return sClusterItems[mCurrentIndex].action;
     }
 
-    public static String getClusterByTypeString(Context context, int type) {
-        for (ActionItem item : sClusterItems) {
-            if (item.action == type) {
-                return context.getString(item.clusterBy);
+    public void enableClusterMenu(int action, ClusterRunner runner) {
+        if (mActionBar != null) {
+            // Don't set cluster runner until action bar is ready.
+            mClusterRunner = null;
+            mActionBar.setListNavigationCallbacks(mAdapter, this);
+            mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+            setSelectedAction(action);
+            mClusterRunner = runner;
+        }
+    }
+
+    // The only use case not to hideMenu in this method is to ensure
+    // all elements disappear at the same time when exiting gallery.
+    // hideMenu should always be true in all other cases.
+    public void disableClusterMenu(boolean hideMenu) {
+        if (mActionBar != null) {
+            mClusterRunner = null;
+            if (hideMenu) {
+                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
             }
         }
-        return null;
-    }
-
-    public static ShareActionProvider initializeShareActionProvider(Menu menu) {
-        MenuItem item = menu.findItem(R.id.action_share);
-        ShareActionProvider shareActionProvider = null;
-        if (item != null) {
-            shareActionProvider = (ShareActionProvider) item.getActionProvider();
-        }
-        return shareActionProvider;
-    }
-
-    public void showClusterMenu(int action, ClusterRunner runner) {
-        Log.v(TAG, "showClusterMenu: runner=" + runner);
-        // Don't set cluster runner until action bar is ready.
-        mClusterRunner = null;
-        mActionBar.setListNavigationCallbacks(mAdapter, this);
-        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
-        setSelectedAction(action);
-        mClusterRunner = runner;
-    }
-
-    public void hideClusterMenu() {
-        mClusterRunner = null;
-        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
     }
 
     public void showClusterDialog(final ClusterRunner clusterRunner) {
@@ -200,11 +208,28 @@
         new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems(
                 mTitles, new DialogInterface.OnClickListener() {
             public void onClick(DialogInterface dialog, int which) {
-                clusterRunner.doCluster(actions.get(which).intValue());
+                // Need to lock rendering when operations invoked by system UI (main thread) are
+                // modifying slot data used in GL thread for rendering.
+                mActivity.getGLRoot().lockRenderThread();
+                try {
+                    clusterRunner.doCluster(actions.get(which).intValue());
+                } finally {
+                    mActivity.getGLRoot().unlockRenderThread();
+                }
             }
         }).create().show();
     }
 
+    public void setDisplayOptions(boolean displayHomeAsUp, boolean showTitle) {
+        if (mActionBar != null) {
+            int options = (displayHomeAsUp ? ActionBar.DISPLAY_HOME_AS_UP : 0) |
+                    (showTitle ? ActionBar.DISPLAY_SHOW_TITLE : 0);
+            mActionBar.setDisplayOptions(options,
+                    ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
+            mActionBar.setHomeButtonEnabled(displayHomeAsUp);
+        }
+    }
+
     public void setTitle(String title) {
         if (mActionBar != null) mActionBar.setTitle(title);
     }
@@ -217,15 +242,25 @@
         if (mActionBar != null) mActionBar.setSubtitle(title);
     }
 
-    public void setNavigationMode(int mode) {
-        if (mActionBar != null) mActionBar.setNavigationMode(mode);
+    public void show() {
+        if (mActionBar != null) mActionBar.show();
     }
 
-    public int getHeight() {
-        return mActionBar == null ? 0 : mActionBar.getHeight();
+    public void hide() {
+        if (mActionBar != null) mActionBar.hide();
+    }
+
+    public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        if (mActionBar != null) mActionBar.addOnMenuVisibilityListener(listener);
+    }
+
+    public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        if (mActionBar != null) mActionBar.removeOnMenuVisibilityListener(listener);
     }
 
     public boolean setSelectedAction(int type) {
+        if (mActionBar == null) return false;
+
         for (int i = 0, n = sClusterItems.length; i < n; i++) {
             ActionItem item = sClusterItems[i];
             if (item.action == type) {
@@ -240,6 +275,8 @@
     @Override
     public boolean onNavigationItemSelected(int itemPosition, long itemId) {
         if (itemPosition != mCurrentIndex && mClusterRunner != null) {
+            // Need to lock rendering when operations invoked by system UI (main thread) are
+            // modifying slot data used in GL thread for rendering.
             mActivity.getGLRoot().lockRenderThread();
             try {
                 mClusterRunner.doCluster(sClusterItems[itemPosition].action);
diff --git a/src/com/android/gallery3d/app/GalleryActivity.java b/src/com/android/gallery3d/app/GalleryActivity.java
index 02f2f72..33c77fb 100644
--- a/src/com/android/gallery3d/app/GalleryActivity.java
+++ b/src/com/android/gallery3d/app/GalleryActivity.java
@@ -17,12 +17,11 @@
 package com.android.gallery3d.app;
 
 import com.android.gallery3d.ui.GLRoot;
-import com.android.gallery3d.ui.PositionRepository;
 
 public interface GalleryActivity extends GalleryContext {
     public StateManager getStateManager();
     public GLRoot getGLRoot();
-    public PositionRepository getPositionRepository();
-    public GalleryApp getGalleryApplication();
     public GalleryActionBar getGalleryActionBar();
+    public OrientationManager getOrientationManager();
+    public TransitionStore getTransitionStore();
 }
diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java
index b3a305e..a2d7494 100644
--- a/src/com/android/gallery3d/app/GalleryApp.java
+++ b/src/com/android/gallery3d/app/GalleryApp.java
@@ -16,16 +16,16 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.DownloadCache;
-import com.android.gallery3d.data.ImageCacheService;
-import com.android.gallery3d.util.ThreadPool;
-
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Looper;
 
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
 public interface GalleryApp {
     public DataManager getDataManager();
     public ImageCacheService getImageCacheService();
diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java
index 8d25ebf..852926b 100644
--- a/src/com/android/gallery3d/app/GalleryAppImpl.java
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -16,6 +16,9 @@
 
 package com.android.gallery3d.app;
 
+import android.app.Application;
+import android.content.Context;
+
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.DownloadCache;
 import com.android.gallery3d.data.ImageCacheService;
@@ -24,9 +27,6 @@
 import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.ThreadPool;
 
-import android.app.Application;
-import android.content.Context;
-
 import java.io.File;
 
 public class GalleryAppImpl extends Application implements GalleryApp {
@@ -35,6 +35,7 @@
     private static final long DOWNLOAD_CAPACITY = 64 * 1024 * 1024; // 64M
 
     private ImageCacheService mImageCacheService;
+    private Object mLock = new Object();
     private DataManager mDataManager;
     private ThreadPool mThreadPool;
     private DownloadCache mDownloadCache;
@@ -42,6 +43,7 @@
     @Override
     public void onCreate() {
         super.onCreate();
+        com.android.camera.Util.initialize(this);
         GalleryUtils.initialize(this);
         WidgetUtils.initialize(this);
         PicasaSource.initialize(this);
@@ -59,11 +61,14 @@
         return mDataManager;
     }
 
-    public synchronized ImageCacheService getImageCacheService() {
-        if (mImageCacheService == null) {
-            mImageCacheService = new ImageCacheService(getAndroidContext());
+    public ImageCacheService getImageCacheService() {
+        // This method may block on file I/O so a dedicated lock is needed here.
+        synchronized (mLock) {
+            if (mImageCacheService == null) {
+                mImageCacheService = new ImageCacheService(getAndroidContext());
+            }
+            return mImageCacheService;
         }
-        return mImageCacheService;
     }
 
     public synchronized ThreadPool getThreadPool() {
diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java
index 022b4a7..06f4fe4 100644
--- a/src/com/android/gallery3d/app/GalleryContext.java
+++ b/src/com/android/gallery3d/app/GalleryContext.java
@@ -16,23 +16,19 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.ImageCacheService;
-import com.android.gallery3d.util.ThreadPool;
-
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Looper;
 
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.util.ThreadPool;
+
 public interface GalleryContext {
-    public ImageCacheService getImageCacheService();
     public DataManager getDataManager();
 
     public Context getAndroidContext();
 
     public Looper getMainLooper();
     public Resources getResources();
-    public ContentResolver getContentResolver();
     public ThreadPool getThreadPool();
 }
diff --git a/src/com/android/gallery3d/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java
index 27f92e4..cdbbfdc 100644
--- a/src/com/android/gallery3d/app/ManageCachePage.java
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -16,29 +16,7 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.MediaObject;
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.AlbumSetView;
-import com.android.gallery3d.ui.CacheStorageUsageInfo;
-import com.android.gallery3d.ui.GLCanvas;
-import com.android.gallery3d.ui.GLView;
-import com.android.gallery3d.ui.ManageCacheDrawer;
-import com.android.gallery3d.ui.MenuExecutor;
-import com.android.gallery3d.ui.SelectionDrawer;
-import com.android.gallery3d.ui.SelectionManager;
-import com.android.gallery3d.ui.SlotView;
-import com.android.gallery3d.ui.StaticBackground;
-import com.android.gallery3d.ui.SynchronizedHandler;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.app.Activity;
-import android.content.Context;
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.Handler;
@@ -52,6 +30,25 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.CacheStorageUsageInfo;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ManageCacheDrawer;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 import java.util.ArrayList;
 
 public class ManageCachePage extends ActivityState implements
@@ -61,21 +58,17 @@
 
     private static final String TAG = "ManageCachePage";
 
-    private static final float USER_DISTANCE_METER = 0.3f;
     private static final int DATA_CACHE_SIZE = 256;
     private static final int MSG_REFRESH_STORAGE_INFO = 1;
     private static final int MSG_REQUEST_LAYOUT = 2;
     private static final int PROGRESS_BAR_MAX = 10000;
 
-    private StaticBackground mStaticBackground;
-    private AlbumSetView mAlbumSetView;
-
+    private SlotView mSlotView;
     private MediaSet mMediaSet;
 
     protected SelectionManager mSelectionManager;
-    protected SelectionDrawer mSelectionDrawer;
-    private AlbumSetDataAdapter mAlbumSetDataAdapter;
-    private float mUserDistance; // in pixel
+    protected ManageCacheDrawer mSelectionDrawer;
+    private AlbumSetDataLoader mAlbumSetDataAdapter;
 
     private EyePosition mEyePosition;
 
@@ -96,6 +89,11 @@
         private float mMatrix[] = new float[16];
 
         @Override
+        protected void renderBackground(GLCanvas view) {
+            view.clearBuffer();
+        }
+
+        @Override
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
             // Hack: our layout depends on other components on the screen.
@@ -107,10 +105,9 @@
             }
             mLayoutReady = false;
 
-            mStaticBackground.layout(0, 0, right - left, bottom - top);
             mEyePosition.resetPosition();
             Activity activity = (Activity) mActivity;
-            int slotViewTop = GalleryActionBar.getHeight(activity);
+            int slotViewTop = mActivity.getGalleryActionBar().getHeight();
             int slotViewBottom = bottom - top;
 
             View footer = activity.findViewById(R.id.footer);
@@ -120,7 +117,7 @@
                 slotViewBottom = location[1];
             }
 
-            mAlbumSetView.layout(0, slotViewTop, right - left, slotViewBottom);
+            mSlotView.layout(0, slotViewTop, right - left, slotViewBottom);
         }
 
         @Override
@@ -134,6 +131,7 @@
         }
     };
 
+    @Override
     public void onEyePositionChanged(float x, float y, float z) {
         mRootPane.lockRendering();
         mX = x;
@@ -144,15 +142,11 @@
     }
 
     private void onDown(int index) {
-        MediaSet set = mAlbumSetDataAdapter.getMediaSet(index);
-        Path path = (set == null) ? null : set.getPath();
-        mSelectionManager.setPressedPath(path);
-        mAlbumSetView.invalidate();
+        mSelectionDrawer.setPressedIndex(index);
     }
 
     private void onUp() {
-        mSelectionManager.setPressedPath(null);
-        mAlbumSetView.invalidate();
+        mSelectionDrawer.setPressedIndex(-1);
     }
 
     public void onSingleTapUp(int slotIndex) {
@@ -188,7 +182,7 @@
         refreshCacheStorageInfo();
 
         mSelectionManager.toggle(path);
-        mAlbumSetView.invalidate();
+        mSlotView.invalidate();
     }
 
     @Override
@@ -230,7 +224,7 @@
     public void onPause() {
         super.onPause();
         mAlbumSetDataAdapter.pause();
-        mAlbumSetView.pause();
+        mSelectionDrawer.pause();
         mEyePosition.pause();
 
         if (mUpdateStorageInfo != null) {
@@ -260,7 +254,7 @@
         super.onResume();
         setContentPane(mRootPane);
         mAlbumSetDataAdapter.resume();
-        mAlbumSetView.resume();
+        mSelectionDrawer.resume();
         mEyePosition.resume();
         mUpdateStorageInfo = mActivity.getThreadPool().submit(mUpdateStorageInfoJob);
         FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
@@ -269,7 +263,6 @@
     }
 
     private void initializeData(Bundle data) {
-        mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
         String mediaPath = data.getString(ManageCachePage.KEY_MEDIA_PATH);
         mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
         mSelectionManager.setSourceMediaSet(mMediaSet);
@@ -278,9 +271,9 @@
         mSelectionManager.setAutoLeaveSelectionMode(false);
         mSelectionManager.enterSelectionMode();
 
-        mAlbumSetDataAdapter = new AlbumSetDataAdapter(
+        mAlbumSetDataAdapter = new AlbumSetDataLoader(
                 mActivity, mMediaSet, DATA_CACHE_SIZE);
-        mAlbumSetView.setModel(mAlbumSetDataAdapter);
+        mSelectionDrawer.setModel(mAlbumSetDataAdapter);
     }
 
     private void initializeViews() {
@@ -288,22 +281,20 @@
 
         mSelectionManager = new SelectionManager(mActivity, true);
         mSelectionManager.setSelectionListener(this);
-        mStaticBackground = new StaticBackground(activity);
-        mRootPane.addComponent(mStaticBackground);
 
         Config.ManageCachePage config = Config.ManageCachePage.get(activity);
-        mSelectionDrawer = new ManageCacheDrawer((Context) mActivity,
-                mSelectionManager, config.cachePinSize, config.cachePinMargin);
-        mAlbumSetView = new AlbumSetView(mActivity, mSelectionDrawer,
-                config.slotViewSpec, config.labelSpec);
-        mAlbumSetView.setListener(new SlotView.SimpleListener() {
+        mSlotView = new SlotView(mActivity, config.slotViewSpec);
+        mSelectionDrawer = new ManageCacheDrawer(mActivity, mSelectionManager, mSlotView,
+                config.labelSpec, config.cachePinSize, config.cachePinMargin);
+        mSlotView.setSlotRenderer(mSelectionDrawer);
+        mSlotView.setListener(new SlotView.SimpleListener() {
             @Override
             public void onDown(int index) {
                 ManageCachePage.this.onDown(index);
             }
 
             @Override
-            public void onUp() {
+            public void onUp(boolean followedByLongPress) {
                 ManageCachePage.this.onUp();
             }
 
@@ -312,36 +303,39 @@
                 ManageCachePage.this.onSingleTapUp(slotIndex);
             }
         });
-        mRootPane.addComponent(mAlbumSetView);
+        mRootPane.addComponent(mSlotView);
         initializeFooterViews();
     }
 
     private void initializeFooterViews() {
         Activity activity = (Activity) mActivity;
 
-        FrameLayout footer = (FrameLayout) activity.findViewById(R.id.footer);
         LayoutInflater inflater = activity.getLayoutInflater();
         mFooterContent = inflater.inflate(R.layout.manage_offline_bar, null);
 
         mFooterContent.findViewById(R.id.done).setOnClickListener(this);
-        mStaticBackground.setImage(R.drawable.background, R.drawable.background_portrait);
         refreshCacheStorageInfo();
     }
 
     @Override
     public void onClick(View view) {
         Utils.assertTrue(view.getId() == R.id.done);
+        GLRoot root = mActivity.getGLRoot();
+        root.lockRenderThread();
+        try {
+            ArrayList<Path> ids = mSelectionManager.getSelected(false);
+            if (ids.size() == 0) {
+                onBackPressed();
+                return;
+            }
+            showToast();
 
-        ArrayList<Path> ids = mSelectionManager.getSelected(false);
-        if (ids.size() == 0) {
-            onBackPressed();
-            return;
+            MenuExecutor menuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+            menuExecutor.startAction(R.id.action_toggle_full_caching,
+                    R.string.process_caching_requests, this);
+        } finally {
+            root.unlockRenderThread();
         }
-        showToast();
-
-        MenuExecutor menuExecutor = new MenuExecutor(mActivity, mSelectionManager);
-        menuExecutor.startAction(R.id.action_toggle_full_caching,
-                R.string.process_caching_requests, this);
     }
 
     private void showToast() {
@@ -388,17 +382,28 @@
         }
     }
 
+    @Override
     public void onProgressComplete(int result) {
         onBackPressed();
     }
 
+    @Override
     public void onProgressUpdate(int index) {
     }
 
+    @Override
     public void onSelectionModeChange(int mode) {
     }
 
+    @Override
     public void onSelectionChange(Path path, boolean selected) {
     }
 
+    @Override
+    public void onConfirmDialogDismissed(boolean confirmed) {
+    }
+
+    @Override
+    public void onConfirmDialogShown() {
+    }
 }
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
index 099e9f5..78fe1ee 100644
--- a/src/com/android/gallery3d/app/MovieActivity.java
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -18,14 +18,18 @@
 
 import android.app.ActionBar;
 import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
 import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.MediaStore;
-import android.provider.MediaStore.Video.VideoColumns;
+import android.provider.OpenableColumns;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -35,17 +39,25 @@
 import android.widget.ShareActionProvider;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
 
 /**
  * This activity plays a video from a specified URI.
+ *
+ * The client of this activity can pass a logo bitmap in the intent (KEY_LOGO_BITMAP)
+ * to set the action bar logo so the playback process looks more seamlessly integrated with
+ * the original activity.
  */
 public class MovieActivity extends Activity {
     @SuppressWarnings("unused")
     private static final String TAG = "MovieActivity";
+    public static final String KEY_LOGO_BITMAP = "logo-bitmap";
+    public static final String KEY_TREAT_UP_AS_BACK = "treat-up-as-back";
 
     private MoviePlayer mPlayer;
     private boolean mFinishOnCompletion;
     private Uri mUri;
+    private boolean mTreatUpAsBack;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -55,11 +67,15 @@
         requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
 
         setContentView(R.layout.movie_view);
-        View rootView = findViewById(R.id.root);
+        View rootView = findViewById(R.id.movie_view_root);
+        rootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
         Intent intent = getIntent();
         initializeActionBar(intent);
         mFinishOnCompletion = intent.getBooleanExtra(
                 MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+        mTreatUpAsBack = intent.getBooleanExtra(KEY_TREAT_UP_AS_BACK, false);
         mPlayer = new MoviePlayer(rootView, this, intent.getData(), savedInstanceState,
                 !mFinishOnCompletion) {
             @Override
@@ -82,29 +98,51 @@
         winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
         winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
         win.setAttributes(winParams);
+
+        // We set the background in the theme to have the launching animation.
+        // But for the performance (and battery), we remove the background here.
+        win.setBackgroundDrawable(null);
     }
 
     private void initializeActionBar(Intent intent) {
-        ActionBar actionBar = getActionBar();
+        mUri = intent.getData();
+        final ActionBar actionBar = getActionBar();
+        Bitmap logo = intent.getParcelableExtra(KEY_LOGO_BITMAP);
+        if (logo != null) {
+            actionBar.setLogo(new BitmapDrawable(getResources(), logo));
+        }
         actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
                 ActionBar.DISPLAY_HOME_AS_UP);
+
         String title = intent.getStringExtra(Intent.EXTRA_TITLE);
-        mUri = intent.getData();
-        if (title == null) {
-            Cursor cursor = null;
-            try {
-                cursor = getContentResolver().query(mUri,
-                        new String[] {VideoColumns.TITLE}, null, null, null);
-                if (cursor != null && cursor.moveToNext()) {
-                    title = cursor.getString(0);
+        if (title != null) {
+            actionBar.setTitle(title);
+        } else {
+            // Displays the filename as title, reading the filename from the
+            // interface: {@link android.provider.OpenableColumns#DISPLAY_NAME}.
+            AsyncQueryHandler queryHandler =
+                    new AsyncQueryHandler(getContentResolver()) {
+                @Override
+                protected void onQueryComplete(int token, Object cookie,
+                        Cursor cursor) {
+                    try {
+                        if ((cursor != null) && cursor.moveToFirst()) {
+                            String displayName = cursor.getString(0);
+
+                            // Just show empty title if other apps don't set
+                            // DISPLAY_NAME
+                            actionBar.setTitle((displayName == null) ? "" :
+                                    displayName);
+                        }
+                    } finally {
+                        Utils.closeSilently(cursor);
+                    }
                 }
-            } catch (Throwable t) {
-                Log.w(TAG, "cannot get title from: " + intent.getDataString(), t);
-            } finally {
-                if (cursor != null) cursor.close();
-            }
+            };
+            queryHandler.startQuery(0, null, mUri,
+                    new String[] {OpenableColumns.DISPLAY_NAME}, null, null,
+                    null);
         }
-        if (title != null) actionBar.setTitle(title);
     }
 
     @Override
@@ -114,7 +152,10 @@
         getMenuInflater().inflate(R.menu.movie, menu);
         ShareActionProvider provider = GalleryActionBar.initializeShareActionProvider(menu);
 
-        if (provider != null) {
+        // Document says EXTRA_STREAM should be a content: Uri
+        // So, we only share the video if it's "content:".
+        if (provider != null && ContentResolver.SCHEME_CONTENT
+                .equals(mUri.getScheme())) {
             Intent intent = new Intent(Intent.ACTION_SEND);
             intent.setType("video/*");
             intent.putExtra(Intent.EXTRA_STREAM, mUri);
@@ -127,7 +168,12 @@
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == android.R.id.home) {
-            finish();
+            if (mTreatUpAsBack) {
+                finish();
+            } else {
+                startActivity(new Intent(this, Gallery.class));
+                finish();
+            }
             return true;
         }
         return false;
diff --git a/src/com/android/gallery3d/app/MovieControllerOverlay.java b/src/com/android/gallery3d/app/MovieControllerOverlay.java
index 752213e..f2c9e05 100644
--- a/src/com/android/gallery3d/app/MovieControllerOverlay.java
+++ b/src/com/android/gallery3d/app/MovieControllerOverlay.java
@@ -16,14 +16,11 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.R;
-
 import android.content.Context;
+import android.graphics.Rect;
 import android.os.Handler;
 import android.view.Gravity;
 import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -31,7 +28,6 @@
 import android.view.animation.Animation.AnimationListener;
 import android.view.animation.AnimationUtils;
 import android.widget.FrameLayout;
-import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.ImageView.ScaleType;
 import android.widget.LinearLayout;
@@ -39,6 +35,8 @@
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import com.android.gallery3d.R;
+
 /**
  * The playback controller for the Movie Player.
  */
@@ -88,8 +86,6 @@
     LayoutParams matchParent =
         new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
 
-    LayoutInflater inflater = LayoutInflater.from(context);
-
     background = new View(context);
     background.setBackgroundColor(context.getResources().getColor(R.color.darker_transparent));
     addView(background, matchParent);
@@ -103,6 +99,9 @@
     ProgressBar spinner = new ProgressBar(context);
     spinner.setIndeterminate(true);
     loadingView.addView(spinner, wrapContent);
+    TextView loadingText = createOverlayTextView(context);
+    loadingText.setText(R.string.loading_video);
+    loadingView.addView(loadingText, wrapContent);
     addView(loadingView, wrapContent);
 
     playPauseReplayView = new ImageView(context);
@@ -114,10 +113,7 @@
     playPauseReplayView.setOnClickListener(this);
     addView(playPauseReplayView, wrapContent);
 
-    errorView = new TextView(context);
-    errorView.setGravity(Gravity.CENTER);
-    errorView.setBackgroundColor(0xCC000000);
-    errorView.setTextColor(0xFFFFFFFF);
+    errorView = createOverlayTextView(context);
     addView(errorView, matchParent);
 
     handler = new Handler();
@@ -136,6 +132,14 @@
     hide();
   }
 
+  private TextView createOverlayTextView(Context context) {
+    TextView view = new TextView(context);
+    view.setGravity(Gravity.CENTER);
+    view.setTextColor(0xFFFFFFFF);
+    view.setPadding(0, 15, 0, 15);
+    return view;
+  }
+
   public void setListener(Listener listener) {
     this.listener = listener;
   }
@@ -171,15 +175,11 @@
   public void showErrorMessage(String message) {
     state = State.ERROR;
     int padding = (int) (getMeasuredWidth() * ERROR_MESSAGE_RELATIVE_PADDING);
-    errorView.setPadding(padding, 10, padding, 10);
+    errorView.setPadding(padding, errorView.getPaddingTop(), padding, errorView.getPaddingBottom());
     errorView.setText(message);
     showMainView(errorView);
   }
 
-  public void resetTime() {
-    timeBar.resetTime();
-  }
-
   public void setTimes(int currentTime, int totalTime) {
     timeBar.setTime(currentTime, totalTime);
   }
@@ -228,6 +228,7 @@
   }
 
   private void startHiding() {
+    startHideAnimation(background);
     startHideAnimation(timeBar);
     startHideAnimation(playPauseReplayView);
   }
@@ -303,39 +304,54 @@
     return true;
   }
 
+  // The paddings of 4 sides which covered by system components. E.g.
+  //    +-----------------+\
+  //    |   Action Bar    | insets.top
+  //    +-----------------+/
+  //    |                 |
+  //    |  Content Area   |  insets.right = insets.left = 0
+  //    |                 |
+  //    +-----------------+\
+  //    | Navigation Bar  | insets.bottom
+  //    +-----------------+/
+  // Please see View.fitSystemWindows() for more details.
+  private final Rect mWindowInsets = new Rect();
+
   @Override
-  protected void onLayout(boolean changed, int l, int t, int r, int b) {
-    int bw;
-    int bh;
-    int y;
-    int h = b - t;
-    int w = r - l;
+  protected boolean fitSystemWindows(Rect insets) {
+    // We don't set the paddings of this View, otherwise,
+    // the content will get cropped outside window
+    mWindowInsets.set(insets);
+    return true;
+  }
+
+  @Override
+  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+    Rect insets = mWindowInsets;
+    int pl = insets.left; // the left paddings
+    int pr = insets.right;
+    int pt = insets.top;
+    int pb = insets.bottom;
+
+    int h = bottom - top;
+    int w = right - left;
     boolean error = errorView.getVisibility() == View.VISIBLE;
 
-    bw = timeBar.getBarHeight();
-    bh = bw;
-    y = b - bh;
+    int y = h - pb;
+    // Put both TimeBar and Background just above the bottom system component.
+    // But extend the background to the width of the screen, since we don't
+    // care if it will be covered by a system component and it looks better.
+    background.layout(0, y - timeBar.getBarHeight(), w, y);
+    timeBar.layout(pl, y - timeBar.getPreferredHeight(), w - pr, y);
 
-    background.layout(l, y, r, b);
-
-    timeBar.layout(l, b - timeBar.getPreferredHeight(), r, b);
     // Needed, otherwise the framework will not re-layout in case only the padding is changed
     timeBar.requestLayout();
 
-    // play pause / next / previous buttons
-    int cx = l + w / 2; // center x
-    int playbackButtonsCenterline = t + h / 2;
-    bw = playPauseReplayView.getMeasuredWidth();
-    bh = playPauseReplayView.getMeasuredHeight();
-    playPauseReplayView.layout(
-        cx - bw / 2, playbackButtonsCenterline - bh / 2, cx + bw / 2,
-        playbackButtonsCenterline + bh / 2);
-
-    // Space available on each side of the error message for the next and previous buttons
-    int errorMessagePadding = (int) (w * ERROR_MESSAGE_RELATIVE_PADDING);
+    // Put the play/pause/next/ previous button in the center of the screen
+    layoutCenteredView(playPauseReplayView, 0, 0, w, h);
 
     if (mainView != null) {
-      layoutCenteredView(mainView, l, t, r, b);
+      layoutCenteredView(mainView, 0, 0, w, h);
     }
   }
 
@@ -386,5 +402,4 @@
     maybeStartHiding();
     listener.onSeekEnd(time);
   }
-
 }
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
index 3de534b..13a77f4 100644
--- a/src/com/android/gallery3d/app/MoviePlayer.java
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import android.app.ActionBar;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -25,6 +24,7 @@
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.graphics.Color;
 import android.media.AudioManager;
 import android.media.MediaPlayer;
 import android.net.Uri;
@@ -60,22 +60,25 @@
     private static final String CMDNAME = "command";
     private static final String CMDPAUSE = "pause";
 
+    private static final long BLACK_TIMEOUT = 500;
+
     // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
     // Otherwise, we pause the player.
     private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
 
     private Context mContext;
     private final VideoView mVideoView;
+    private final View mRootView;
     private final Bookmarker mBookmarker;
     private final Uri mUri;
     private final Handler mHandler = new Handler();
     private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
-    private final ActionBar mActionBar;
-    private final ControllerOverlay mController;
+    private final MovieControllerOverlay mController;
 
     private long mResumeableTime = Long.MAX_VALUE;
     private int mVideoPosition = 0;
     private boolean mHasPaused = false;
+    private int mLastSystemUiVis = 0;
 
     // If the time bar is being dragged.
     private boolean mDragging;
@@ -83,6 +86,9 @@
     // If the time bar is visible.
     private boolean mShowing;
 
+    // Control when system UI can be shown
+    private boolean mAllowShowingSystemUI;
+
     private final Runnable mPlayingChecker = new Runnable() {
         @Override
         public void run() {
@@ -94,6 +100,13 @@
         }
     };
 
+    private final Runnable mRemoveBackground = new Runnable() {
+        @Override
+        public void run() {
+            mRootView.setBackground(null);
+        }
+    };
+
     private final Runnable mProgressChecker = new Runnable() {
         @Override
         public void run() {
@@ -102,12 +115,12 @@
         }
     };
 
-    public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri,
-            Bundle savedInstance, boolean canReplay) {
+    public MoviePlayer(View rootView, final MovieActivity movieActivity,
+            Uri videoUri, Bundle savedInstance, boolean canReplay) {
         mContext = movieActivity.getApplicationContext();
+        mRootView = rootView;
         mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
         mBookmarker = new Bookmarker(movieActivity);
-        mActionBar = movieActivity.getActionBar();
         mUri = videoUri;
 
         mController = new MovieControllerOverlay(mContext);
@@ -119,24 +132,58 @@
         mVideoView.setOnCompletionListener(this);
         mVideoView.setVideoURI(mUri);
         mVideoView.setOnTouchListener(new View.OnTouchListener() {
+            @Override
             public boolean onTouch(View v, MotionEvent event) {
                 mController.show();
                 return true;
             }
         });
 
+        // The SurfaceView is transparent before drawing the first frame.
+        // This makes the UI flashing when open a video. (black -> old screen
+        // -> video) However, we have no way to know the timing of the first
+        // frame. So, we hide the VideoView for a while to make sure the
+        // video has been drawn on it.
+        mVideoView.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                mVideoView.setVisibility(View.VISIBLE);
+            }
+        }, BLACK_TIMEOUT);
+
         // When the user touches the screen or uses some hard key, the framework
         // will change system ui visibility from invisible to visible. We show
-        // the media control at this point.
+        // the media control and enable system UI (e.g. ActionBar) to be visible at this point
         mVideoView.setOnSystemUiVisibilityChangeListener(
                 new View.OnSystemUiVisibilityChangeListener() {
+            @Override
             public void onSystemUiVisibilityChange(int visibility) {
-                if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+                int diff = mLastSystemUiVis ^ visibility;
+                mLastSystemUiVis = visibility;
+                if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+                        && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+                    mAllowShowingSystemUI = true;
                     mController.show();
+
+                    // We need to set the background to clear ghosting images
+                    // when ActionBar slides in. However, if we keep the background,
+                    // there will be one additional layer in HW composer, which is bad
+                    // to battery. As a solution, we remove the background when we
+                    // hide the action bar
+                    mHandler.removeCallbacks(mRemoveBackground);
+                    mRootView.setBackgroundColor(Color.BLACK);
+                } else {
+                    mHandler.removeCallbacks(mRemoveBackground);
+
+                    // Wait for the slide out animation, one second should be enough
+                    mHandler.postDelayed(mRemoveBackground, 1000);
                 }
             }
         });
 
+        // Hide system UI by default
+        showSystemUi(false);
+
         mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
         mAudioBecomingNoisyReceiver.register();
 
@@ -161,8 +208,13 @@
     }
 
     private void showSystemUi(boolean visible) {
-        int flag = visible ? 0 : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
-                View.SYSTEM_UI_FLAG_LOW_PROFILE;
+        int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+        if (!visible) {
+            flag |= View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN
+                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+        }
         mVideoView.setSystemUiVisibility(flag);
     }
 
@@ -251,6 +303,7 @@
             mHandler.postDelayed(mPlayingChecker, 250);
         } else {
             mController.showPlaying();
+            mController.hide();
         }
 
         mVideoView.start();
@@ -317,15 +370,18 @@
     @Override
     public void onShown() {
         mShowing = true;
-        mActionBar.show();
-        showSystemUi(true);
         setProgress();
+
+        // System UI is invisible by default until the flag is set by user interaction
+        // See VideoView's onSystemUiVisibilityChange listener for details.
+        if (mAllowShowingSystemUI) {
+            showSystemUi(true);
+        }
     }
 
     @Override
     public void onHidden() {
         mShowing = false;
-        mActionBar.hide();
         showSystemUi(false);
     }
 
diff --git a/src/com/android/gallery3d/app/OrientationManager.java b/src/com/android/gallery3d/app/OrientationManager.java
new file mode 100644
index 0000000..c3e91fb
--- /dev/null
+++ b/src/com/android/gallery3d/app/OrientationManager.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.provider.Settings;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.ViewConfiguration;
+
+import com.android.gallery3d.ui.OrientationSource;
+
+import java.util.ArrayList;
+
+public class OrientationManager implements OrientationSource {
+    private static final String TAG = "OrientationManager";
+
+    public interface Listener {
+        public void onOrientationCompensationChanged();
+    }
+
+    // Orientation hysteresis amount used in rounding, in degrees
+    private static final int ORIENTATION_HYSTERESIS = 5;
+
+    private Activity mActivity;
+    private ArrayList<Listener> mListeners;
+    private MyOrientationEventListener mOrientationListener;
+    // The degrees of the device rotated clockwise from its natural orientation.
+    private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+    // If the framework orientation is locked.
+    private boolean mOrientationLocked = false;
+    // The orientation compensation: if the framwork orientation is locked, the
+    // device orientation and the framework orientation may be different, so we
+    // need to rotate the UI. For example, if this value is 90, the UI
+    // components should be rotated 90 degrees counter-clockwise.
+    private int mOrientationCompensation = 0;
+
+    // This is true if "Settings -> Display -> Rotation Lock" is checked. We
+    // don't allow the orientation to be unlocked if the value is true.
+    private boolean mRotationLockedSetting = false;
+
+    public OrientationManager(Activity activity) {
+        mActivity = activity;
+        mListeners = new ArrayList<Listener>();
+        mOrientationListener = new MyOrientationEventListener(activity);
+    }
+
+    public void resume() {
+        ContentResolver resolver = mActivity.getContentResolver();
+        mRotationLockedSetting = Settings.System.getInt(
+                resolver, Settings.System.ACCELEROMETER_ROTATION, 0) != 1;
+        mOrientationListener.enable();
+    }
+
+    public void pause() {
+        mOrientationListener.disable();
+    }
+
+    public void addListener(Listener listener) {
+        synchronized (mListeners) {
+            mListeners.add(listener);
+        }
+    }
+
+    public void removeListener(Listener listener) {
+        synchronized (mListeners) {
+            mListeners.remove(listener);
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Orientation handling
+    //
+    //  We can choose to lock the framework orientation or not. If we lock the
+    //  framework orientation, we calculate a a compensation value according to
+    //  current device orientation and send it to listeners. If we don't lock
+    //  the framework orientation, we always set the compensation value to 0.
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Lock the framework orientation to the current device orientation
+    public void lockOrientation() {
+        if (mOrientationLocked) return;
+        mOrientationLocked = true;
+        if (mActivity.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE) {
+            Log.d(TAG, "lock orientation to landscape");
+            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+        } else {
+            Log.d(TAG, "lock orientation to portrait");
+            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        }
+        updateCompensation();
+    }
+
+    // Unlock the framework orientation, so it can change when the device
+    // rotates.
+    public void unlockOrientation() {
+        if (!mOrientationLocked) return;
+        if (mRotationLockedSetting) return;
+        mOrientationLocked = false;
+        Log.d(TAG, "unlock orientation");
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+        disableCompensation();
+    }
+
+    // Calculate the compensation value and send it to listeners.
+    private void updateCompensation() {
+        if (mOrientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
+            return;
+        }
+
+        int orientationCompensation =
+            (mOrientation + getDisplayRotation(mActivity)) % 360;
+
+        if (mOrientationCompensation != orientationCompensation) {
+            mOrientationCompensation = orientationCompensation;
+            notifyListeners();
+        }
+    }
+
+    // Make the compensation value 0 and send it to listeners.
+    private void disableCompensation() {
+        if (mOrientationCompensation != 0) {
+            mOrientationCompensation = 0;
+            notifyListeners();
+        }
+    }
+
+    private void notifyListeners() {
+        synchronized (mListeners) {
+            for (int i = 0, n = mListeners.size(); i < n; i++) {
+                mListeners.get(i).onOrientationCompensationChanged();
+            }
+        }
+    }
+
+    // This listens to the device orientation, so we can update the compensation.
+    private class MyOrientationEventListener extends OrientationEventListener {
+        public MyOrientationEventListener(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onOrientationChanged(int orientation) {
+            // We keep the last known orientation. So if the user first orient
+            // the camera then point the camera to floor or sky, we still have
+            // the correct orientation.
+            if (orientation == ORIENTATION_UNKNOWN) return;
+            mOrientation = roundOrientation(orientation, mOrientation);
+            // If the framework orientation is locked, we update the
+            // compensation value and notify the listeners.
+            if (mOrientationLocked) updateCompensation();
+        }
+    }
+
+    @Override
+    public int getDisplayRotation() {
+        return getDisplayRotation(mActivity);
+    }
+
+    @Override
+    public int getCompensation() {
+        return mOrientationCompensation;
+    }
+
+    private static int roundOrientation(int orientation, int orientationHistory) {
+        boolean changeOrientation = false;
+        if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) {
+            changeOrientation = true;
+        } else {
+            int dist = Math.abs(orientation - orientationHistory);
+            dist = Math.min(dist, 360 - dist);
+            changeOrientation = (dist >= 45 + ORIENTATION_HYSTERESIS);
+        }
+        if (changeOrientation) {
+            return ((orientation + 45) / 90 * 90) % 360;
+        }
+        return orientationHistory;
+    }
+
+    private static int getDisplayRotation(Activity activity) {
+        int rotation = activity.getWindowManager().getDefaultDisplay()
+                .getRotation();
+        switch (rotation) {
+            case Surface.ROTATION_0: return 0;
+            case Surface.ROTATION_90: return 90;
+            case Surface.ROTATION_180: return 180;
+            case Surface.ROTATION_270: return 270;
+        }
+        return 0;
+    }
+}
diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java
index dcd9d04..e4bb8ee 100644
--- a/src/com/android/gallery3d/app/PackagesMonitor.java
+++ b/src/com/android/gallery3d/app/PackagesMonitor.java
@@ -16,14 +16,14 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.picasasource.PicasaSource;
-
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
 
+import com.android.gallery3d.picasasource.PicasaSource;
+
 public class PackagesMonitor extends BroadcastReceiver {
     public static final String KEY_PACKAGES_VERSION  = "packages-version";
 
@@ -33,7 +33,24 @@
     }
 
     @Override
-    public void onReceive(Context context, Intent intent) {
+    public void onReceive(final Context context, final Intent intent) {
+        final PendingResult result = goAsync();
+        new Thread("GalleryPackagesMonitorAsync") {
+            @Override
+            public void run() {
+                try {
+                    onReceiveAsync(context, intent);
+                } catch (Throwable t) {
+                    Log.e("PackagesMonitor", "onReceiveAsync", t);
+                } finally {
+                    result.finish();
+                }
+            }
+        }.start();
+    }
+
+    // Runs in a background thread.
+    private void onReceiveAsync(Context context, Intent intent) {
         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
 
         int version = prefs.getInt(KEY_PACKAGES_VERSION, 1);
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index d7d1168..6a4961e 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -16,29 +16,33 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.ContentListener;
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.MediaObject;
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.PhotoView;
-import com.android.gallery3d.ui.PhotoView.ImageData;
-import com.android.gallery3d.ui.SynchronizedHandler;
-import com.android.gallery3d.ui.TileImageViewAdapter;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureListener;
-import com.android.gallery3d.util.ThreadPool;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.graphics.Bitmap;
 import android.graphics.BitmapRegionDecoder;
 import android.os.Handler;
 import android.os.Message;
 
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BitmapPool;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalMediaItem;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.BitmapScreenNail;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -54,16 +58,16 @@
     private static final int MSG_LOAD_START = 1;
     private static final int MSG_LOAD_FINISH = 2;
     private static final int MSG_RUN_OBJECT = 3;
+    private static final int MSG_UPDATE_IMAGE_REQUESTS = 4;
 
     private static final int MIN_LOAD_COUNT = 8;
     private static final int DATA_CACHE_SIZE = 32;
-    private static final int IMAGE_CACHE_SIZE = 5;
+    private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
+    private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
 
     private static final int BIT_SCREEN_NAIL = 1;
     private static final int BIT_FULL_IMAGE = 2;
 
-    private static final long VERSION_OUT_OF_RANGE = MediaObject.nextVersionNumber();
-
     // sImageFetchSeq is the fetching sequence for images.
     // We want to fetch the current screennail first (offset = 0), the next
     // screennail (offset = +1), then the previous screennail (offset = -1) etc.
@@ -112,14 +116,13 @@
     private int mContentStart = 0;
     private int mContentEnd = 0;
 
-    /*
-     * The ImageCache is a version-to-ImageEntry map. It only holds
-     * the ImageEntries in the range of [mActiveStart, mActiveEnd).
-     * We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.
-     * Besides, the [mActiveStart, mActiveEnd) range must be contained
-     * within the[mContentStart, mContentEnd) range.
-     */
-    private HashMap<Long, ImageEntry> mImageCache = new HashMap<Long, ImageEntry>();
+    // The ImageCache is a Path-to-ImageEntry map. It only holds the
+    // ImageEntries in the range of [mActiveStart, mActiveEnd).  We also keep
+    // mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.  Besides, the
+    // [mActiveStart, mActiveEnd) range must be contained within
+    // the [mContentStart, mContentEnd) range.
+    private HashMap<Path, ImageEntry> mImageCache =
+            new HashMap<Path, ImageEntry>();
     private int mActiveStart = 0;
     private int mActiveEnd = 0;
 
@@ -127,10 +130,13 @@
     // mCurrentIndex triggers the data loading and image loading.
     private int mCurrentIndex;
 
-    // mChanges keeps the version number (of MediaItem) about the previous,
-    // current, and next image. If the version number changes, we invalidate
-    // the model. This is used after a database reload or mCurrentIndex changes.
-    private final long mChanges[] = new long[3];
+    // mChanges keeps the version number (of MediaItem) about the images. If any
+    // of the version number changes, we notify the view. This is used after a
+    // database reload or mCurrentIndex changes.
+    private final long mChanges[] = new long[IMAGE_CACHE_SIZE];
+    // mPaths keeps the corresponding Path (of MediaItem) for the images. This
+    // is used to determine the item movement.
+    private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE];
 
     private final Handler mMainHandler;
     private final ThreadPool mThreadPool;
@@ -142,10 +148,12 @@
     private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
     private int mSize = 0;
     private Path mItemPath;
+    private int mCameraIndex;
+    private boolean mIsPanorama;
     private boolean mIsActive;
+    private boolean mNeedFullImage;
 
     public interface DataListener extends LoadingListener {
-        public void onPhotoAvailable(long version, boolean fullImage);
         public void onPhotoChanged(int index, Path item);
     }
 
@@ -156,14 +164,19 @@
     // The path of the current viewing item will be stored in mItemPath.
     // If mItemPath is not null, mCurrentIndex is only a hint for where we
     // can find the item. If mItemPath is null, then we use the mCurrentIndex to
-    // find the image being viewed.
-    public PhotoDataAdapter(GalleryActivity activity,
-            PhotoView view, MediaSet mediaSet, Path itemPath, int indexHint) {
+    // find the image being viewed. cameraIndex is the index of the camera
+    // preview. If cameraIndex < 0, there is no camera preview.
+    public PhotoDataAdapter(GalleryActivity activity, PhotoView view,
+            MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex,
+            boolean isPanorama) {
         mSource = Utils.checkNotNull(mediaSet);
         mPhotoView = Utils.checkNotNull(view);
         mItemPath = Utils.checkNotNull(itemPath);
         mCurrentIndex = indexHint;
+        mCameraIndex = cameraIndex;
+        mIsPanorama = isPanorama;
         mThreadPool = activity.getThreadPool();
+        mNeedFullImage = true;
 
         Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
 
@@ -176,11 +189,19 @@
                         ((Runnable) message.obj).run();
                         return;
                     case MSG_LOAD_START: {
-                        if (mDataListener != null) mDataListener.onLoadingStarted();
+                        if (mDataListener != null) {
+                            mDataListener.onLoadingStarted();
+                        }
                         return;
                     }
                     case MSG_LOAD_FINISH: {
-                        if (mDataListener != null) mDataListener.onLoadingFinished();
+                        if (mDataListener != null) {
+                            mDataListener.onLoadingFinished();
+                        }
+                        return;
+                    }
+                    case MSG_UPDATE_IMAGE_REQUESTS: {
+                        updateImageRequests();
                         return;
                     }
                     default: throw new AssertionError();
@@ -191,59 +212,116 @@
         updateSlidingWindow();
     }
 
-    private long getVersion(int index) {
-        if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE;
+    private MediaItem getItemInternal(int index) {
+        if (index < 0 || index >= mSize) return null;
         if (index >= mContentStart && index < mContentEnd) {
-            MediaItem item = mData[index % DATA_CACHE_SIZE];
-            if (item != null) return item.getDataVersion();
+            return mData[index % DATA_CACHE_SIZE];
         }
-        return MediaObject.INVALID_DATA_VERSION;
+        return null;
     }
 
-    private void fireModelInvalidated() {
-        for (int i = -1; i <= 1; ++i) {
-            long current = getVersion(mCurrentIndex + i);
-            long change = mChanges[i + 1];
-            if (current != change) {
-                mPhotoView.notifyImageInvalidated(i);
-                mChanges[i + 1] = current;
+    private long getVersion(int index) {
+        MediaItem item = getItemInternal(index);
+        if (item == null) return MediaObject.INVALID_DATA_VERSION;
+        return item.getDataVersion();
+    }
+
+    private Path getPath(int index) {
+        MediaItem item = getItemInternal(index);
+        if (item == null) return null;
+        return item.getPath();
+    }
+
+    private void fireDataChange() {
+        // First check if data actually changed.
+        boolean changed = false;
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            long newVersion = getVersion(mCurrentIndex + i);
+            if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) {
+                mChanges[i + SCREEN_NAIL_MAX] = newVersion;
+                changed = true;
             }
         }
+
+        if (!changed) return;
+
+        // Now calculate the fromIndex array. fromIndex represents the item
+        // movement. It records the index where the picture come from. The
+        // special value Integer.MAX_VALUE means it's a new picture.
+        final int N = IMAGE_CACHE_SIZE;
+        int fromIndex[] = new int[N];
+
+        // Remember the old path array.
+        Path oldPaths[] = new Path[N];
+        System.arraycopy(mPaths, 0, oldPaths, 0, N);
+
+        // Update the mPaths array.
+        for (int i = 0; i < N; ++i) {
+            mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX);
+        }
+
+        // Calculate the fromIndex array.
+        for (int i = 0; i < N; i++) {
+            Path p = mPaths[i];
+            if (p == null) {
+                fromIndex[i] = Integer.MAX_VALUE;
+                continue;
+            }
+
+            // Try to find the same path in the old array
+            int j;
+            for (j = 0; j < N; j++) {
+                if (oldPaths[j] == p) {
+                    break;
+                }
+            }
+            fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
+        }
+
+        mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex,
+                mSize - 1 - mCurrentIndex);
     }
 
     public void setDataListener(DataListener listener) {
         mDataListener = listener;
     }
 
-    private void updateScreenNail(long version, Future<Bitmap> future) {
-        ImageEntry entry = mImageCache.get(version);
+    private void updateScreenNail(Path path, Future<ScreenNail> future) {
+        ImageEntry entry = mImageCache.get(path);
+        ScreenNail screenNail = future.get();
+
         if (entry == null || entry.screenNailTask != future) {
-            Bitmap screenNail = future.get();
             if (screenNail != null) screenNail.recycle();
             return;
         }
 
         entry.screenNailTask = null;
-        entry.screenNail = future.get();
 
-        if (entry.screenNail == null) {
+        // Combine the ScreenNails if we already have a BitmapScreenNail
+        if (entry.screenNail instanceof BitmapScreenNail) {
+            BitmapScreenNail original = (BitmapScreenNail) entry.screenNail;
+            screenNail = original.combine(screenNail);
+        }
+
+        if (screenNail == null) {
             entry.failToLoad = true;
         } else {
-            if (mDataListener != null) {
-                mDataListener.onPhotoAvailable(version, false);
-            }
-            for (int i = -1; i <=1; ++i) {
-                if (version == getVersion(mCurrentIndex + i)) {
-                    if (i == 0) updateTileProvider(entry);
-                    mPhotoView.notifyImageInvalidated(i);
-                }
+            entry.failToLoad = false;
+            entry.screenNail = screenNail;
+        }
+
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            if (path == getPath(mCurrentIndex + i)) {
+                if (i == 0) updateTileProvider(entry);
+                mPhotoView.notifyImageChange(i);
+                break;
             }
         }
         updateImageRequests();
     }
 
-    private void updateFullImage(long version, Future<BitmapRegionDecoder> future) {
-        ImageEntry entry = mImageCache.get(version);
+    private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
+        ImageEntry entry = mImageCache.get(path);
         if (entry == null || entry.fullImageTask != future) {
             BitmapRegionDecoder fullImage = future.get();
             if (fullImage != null) fullImage.recycle();
@@ -253,12 +331,9 @@
         entry.fullImageTask = null;
         entry.fullImage = future.get();
         if (entry.fullImage != null) {
-            if (mDataListener != null) {
-                mDataListener.onPhotoAvailable(version, true);
-            }
-            if (version == getVersion(mCurrentIndex)) {
+            if (path == getPath(mCurrentIndex)) {
                 updateTileProvider(entry);
-                mPhotoView.notifyImageInvalidated(0);
+                mPhotoView.notifyImageChange(0);
             }
         }
         updateImageRequests();
@@ -273,7 +348,7 @@
         mReloadTask = new ReloadTask();
         mReloadTask.start();
 
-        mPhotoView.notifyModelInvalidated();
+        fireDataChange();
     }
 
     public void pause() {
@@ -287,33 +362,24 @@
         for (ImageEntry entry : mImageCache.values()) {
             if (entry.fullImageTask != null) entry.fullImageTask.cancel();
             if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+            if (entry.screenNail != null) entry.screenNail.recycle();
         }
         mImageCache.clear();
         mTileProvider.clear();
     }
 
-    private ImageData getImage(int index) {
+    private MediaItem getItem(int index) {
         if (index < 0 || index >= mSize || !mIsActive) return null;
         Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
 
-        ImageEntry entry = mImageCache.get(getVersion(index));
-        Bitmap screennail = entry == null ? null : entry.screenNail;
-        if (screennail != null) {
-            return new ImageData(screennail, entry.rotation);
-        } else {
-            return new ImageData(null, 0);
+        if (index >= mContentStart && index < mContentEnd) {
+            return mData[index % DATA_CACHE_SIZE];
         }
-    }
-
-    public ImageData getPreviousImage() {
-        return getImage(mCurrentIndex - 1);
-    }
-
-    public ImageData getNextImage() {
-        return getImage(mCurrentIndex + 1);
+        return null;
     }
 
     private void updateCurrentIndex(int index) {
+        if (mCurrentIndex == index) return;
         mCurrentIndex = index;
         updateSlidingWindow();
 
@@ -323,29 +389,93 @@
         updateImageCache();
         updateImageRequests();
         updateTileProvider();
-        mPhotoView.notifyOnNewImage();
 
         if (mDataListener != null) {
             mDataListener.onPhotoChanged(index, mItemPath);
         }
-        fireModelInvalidated();
+
+        fireDataChange();
     }
 
-    public void next() {
-        updateCurrentIndex(mCurrentIndex + 1);
-    }
-
-    public void previous() {
-        updateCurrentIndex(mCurrentIndex - 1);
-    }
-
-    public void jumpTo(int index) {
-        if (mCurrentIndex == index) return;
+    @Override
+    public void moveTo(int index) {
         updateCurrentIndex(index);
     }
 
-    public Bitmap getBackupImage() {
-        return mTileProvider.getBackupImage();
+    @Override
+    public ScreenNail getScreenNail(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index < 0 || index >= mSize || !mIsActive) return null;
+        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+        MediaItem item = getItem(index);
+        if (item == null) return null;
+
+        ImageEntry entry = mImageCache.get(item.getPath());
+        if (entry == null) return null;
+
+        // Create a default ScreenNail if the real one is not available yet.
+        if (entry.screenNail == null) {
+            entry.screenNail = newPlaceholderScreenNail(item);
+            if (offset == 0) updateTileProvider(entry);
+        }
+
+        return entry.screenNail;
+    }
+
+    @Override
+    public void getImageSize(int offset, PhotoView.Size size) {
+        MediaItem item = getItem(mCurrentIndex + offset);
+        if (item == null) {
+            size.width = 0;
+            size.height = 0;
+        } else {
+            size.width = item.getWidth();
+            size.height = item.getHeight();
+        }
+    }
+
+    @Override
+    public int getImageRotation(int offset) {
+        MediaItem item = getItem(mCurrentIndex + offset);
+        return (item == null) ? 0 : item.getFullImageRotation();
+    }
+
+    @Override
+    public void setNeedFullImage(boolean enabled) {
+        mNeedFullImage = enabled;
+        mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS);
+    }
+
+    @Override
+    public boolean isCamera(int offset) {
+        return mCurrentIndex + offset == mCameraIndex;
+    }
+
+    @Override
+    public boolean isPanorama(int offset) {
+        return isCamera(offset) && mIsPanorama;
+    }
+
+    @Override
+    public boolean isVideo(int offset) {
+        MediaItem item = getItem(mCurrentIndex + offset);
+        return (item == null)
+                ? false
+                : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+    }
+
+    @Override
+    public int getLoadingState(int offset) {
+        ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset));
+        if (entry == null) return LOADING_INIT;
+        if (entry.failToLoad) return LOADING_FAIL;
+        if (entry.screenNail != null) return LOADING_COMPLETE;
+        return LOADING_INIT;
+    }
+
+    public ScreenNail getScreenNail() {
+        return mTileProvider.getScreenNail();
     }
 
     public int getImageHeight() {
@@ -356,22 +486,13 @@
         return mTileProvider.getImageWidth();
     }
 
-    public int getImageRotation() {
-        ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
-        return entry == null ? 0 : entry.rotation;
-    }
-
     public int getLevelCount() {
         return mTileProvider.getLevelCount();
     }
 
     public Bitmap getTile(int level, int x, int y, int tileSize,
-            int borderSize) {
-        return mTileProvider.getTile(level, x, y, tileSize, borderSize);
-    }
-
-    public boolean isFailedToLoad() {
-        return mTileProvider.isFailedToLoad();
+            int borderSize, BitmapPool pool) {
+        return mTileProvider.getTile(level, x, y, tileSize, borderSize, pool);
     }
 
     public boolean isEmpty() {
@@ -382,8 +503,12 @@
         return mCurrentIndex;
     }
 
-    public MediaItem getCurrentMediaItem() {
-        return mData[mCurrentIndex % DATA_CACHE_SIZE];
+    public MediaItem getMediaItem(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index >= mContentStart && index < mContentEnd) {
+            return mData[index % DATA_CACHE_SIZE];
+        }
+        return null;
     }
 
     public void setCurrentPhoto(Path path, int indexHint) {
@@ -392,17 +517,17 @@
         mCurrentIndex = indexHint;
         updateSlidingWindow();
         updateImageCache();
-        fireModelInvalidated();
+        fireDataChange();
 
         // We need to reload content if the path doesn't match.
-        MediaItem item = getCurrentMediaItem();
+        MediaItem item = getMediaItem(0);
         if (item != null && item.getPath() != path) {
             if (mReloadTask != null) mReloadTask.notifyDirty();
         }
     }
 
     private void updateTileProvider() {
-        ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
+        ImageEntry entry = mImageCache.get(getPath(mCurrentIndex));
         if (entry == null) { // in loading
             mTileProvider.clear();
         } else {
@@ -411,21 +536,20 @@
     }
 
     private void updateTileProvider(ImageEntry entry) {
-        Bitmap screenNail = entry.screenNail;
+        ScreenNail screenNail = entry.screenNail;
         BitmapRegionDecoder fullImage = entry.fullImage;
         if (screenNail != null) {
             if (fullImage != null) {
-                mTileProvider.setBackupImage(screenNail,
+                mTileProvider.setScreenNail(screenNail,
                         fullImage.getWidth(), fullImage.getHeight());
                 mTileProvider.setRegionDecoder(fullImage);
             } else {
                 int width = screenNail.getWidth();
                 int height = screenNail.getHeight();
-                mTileProvider.setBackupImage(screenNail, width, height);
+                mTileProvider.setScreenNail(screenNail, width, height);
             }
         } else {
             mTileProvider.clear();
-            if (entry.failToLoad) mTileProvider.setFailedToLoad();
         }
     }
 
@@ -472,6 +596,7 @@
         for (int i = 0; i < sImageFetchSeq.length; i++) {
             int offset = sImageFetchSeq[i].indexOffset;
             int bit = sImageFetchSeq[i].imageBit;
+            if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue;
             task = startTaskIfNeeded(currentIndex + offset, bit);
             if (task != null) break;
         }
@@ -481,17 +606,17 @@
             if (entry.screenNailTask != null && entry.screenNailTask != task) {
                 entry.screenNailTask.cancel();
                 entry.screenNailTask = null;
-                entry.requestedBits &= ~BIT_SCREEN_NAIL;
+                entry.requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
             }
             if (entry.fullImageTask != null && entry.fullImageTask != task) {
                 entry.fullImageTask.cancel();
                 entry.fullImageTask = null;
-                entry.requestedBits &= ~BIT_FULL_IMAGE;
+                entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
             }
         }
     }
 
-    private static class ScreenNailJob implements Job<Bitmap> {
+    private class ScreenNailJob implements Job<ScreenNail> {
         private MediaItem mItem;
 
         public ScreenNailJob(MediaItem item) {
@@ -499,50 +624,109 @@
         }
 
         @Override
-        public Bitmap run(JobContext jc) {
+        public ScreenNail run(JobContext jc) {
+            // We try to get a ScreenNail first, if it fails, we fallback to get
+            // a Bitmap and then wrap it in a BitmapScreenNail instead.
+            ScreenNail s = mItem.getScreenNail();
+            if (s != null) return s;
+
+            // If this is a temporary item, don't try to get its bitmap because
+            // it won't be available. We will get its bitmap after a data reload.
+            if (isTemporaryItem(mItem)) {
+                return newPlaceholderScreenNail(mItem);
+            }
+
             Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
             if (jc.isCancelled()) return null;
             if (bitmap != null) {
                 bitmap = BitmapUtils.rotateBitmap(bitmap,
                     mItem.getRotation() - mItem.getFullImageRotation(), true);
             }
-            return bitmap;
+            return bitmap == null ? null : new BitmapScreenNail(bitmap);
         }
     }
 
+    private class FullImageJob implements Job<BitmapRegionDecoder> {
+        private MediaItem mItem;
+
+        public FullImageJob(MediaItem item) {
+            mItem = item;
+        }
+
+        @Override
+        public BitmapRegionDecoder run(JobContext jc) {
+            if (isTemporaryItem(mItem)) {
+                return null;
+            }
+            return mItem.requestLargeImage().run(jc);
+        }
+    }
+
+    // Returns true if we think this is a temporary item created by Camera. A
+    // temporary item is an image or a video whose data is still being
+    // processed, but an incomplete entry is created first in MediaProvider, so
+    // we can display them (in grey tile) even if they are not saved to disk
+    // yet. When the image or video data is actually saved, we will get
+    // notification from MediaProvider, reload data, and show the actual image
+    // or video data.
+    private boolean isTemporaryItem(MediaItem mediaItem) {
+        // Must have camera to create a temporary item.
+        if (mCameraIndex < 0) return false;
+        // Must be an item in camera roll.
+        if (!(mediaItem instanceof LocalMediaItem)) return false;
+        LocalMediaItem item = (LocalMediaItem) mediaItem;
+        if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false;
+        // Must have no size, but must have width and height information
+        if (item.getSize() != 0) return false;
+        if (item.getWidth() == 0) return false;
+        if (item.getHeight() == 0) return false;
+        // Must be created in the last 10 seconds.
+        if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false;
+        return true;
+    }
+
+    // Create a default ScreenNail when a ScreenNail is needed, but we don't yet
+    // have one available (because the image data is still being saved, or the
+    // Bitmap is still being loaded.
+    private ScreenNail newPlaceholderScreenNail(MediaItem item) {
+        int width = item.getWidth();
+        int height = item.getHeight();
+        return new BitmapScreenNail(width, height);
+    }
+
     // Returns the task if we started the task or the task is already started.
     private Future<?> startTaskIfNeeded(int index, int which) {
         if (index < mActiveStart || index >= mActiveEnd) return null;
 
-        ImageEntry entry = mImageCache.get(getVersion(index));
+        ImageEntry entry = mImageCache.get(getPath(index));
         if (entry == null) return null;
+        MediaItem item = mData[index % DATA_CACHE_SIZE];
+        Utils.assertTrue(item != null);
+        long version = item.getDataVersion();
 
-        if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) {
+        if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null
+                && entry.requestedScreenNail == version) {
             return entry.screenNailTask;
-        } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) {
+        } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null
+                && entry.requestedFullImage == version) {
             return entry.fullImageTask;
         }
 
-        MediaItem item = mData[index % DATA_CACHE_SIZE];
-        Utils.assertTrue(item != null);
-
-        if (which == BIT_SCREEN_NAIL
-                && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) {
-            entry.requestedBits |= BIT_SCREEN_NAIL;
+        if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) {
+            entry.requestedScreenNail = version;
             entry.screenNailTask = mThreadPool.submit(
                     new ScreenNailJob(item),
-                    new ScreenNailListener(item.getDataVersion()));
+                    new ScreenNailListener(item));
             // request screen nail
             return entry.screenNailTask;
         }
-        if (which == BIT_FULL_IMAGE
-                && (entry.requestedBits & BIT_FULL_IMAGE) == 0
+        if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version
                 && (item.getSupportedOperations()
                 & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
-            entry.requestedBits |= BIT_FULL_IMAGE;
+            entry.requestedFullImage = version;
             entry.fullImageTask = mThreadPool.submit(
-                    item.requestLargeImage(),
-                    new FullImageListener(item.getDataVersion()));
+                    new FullImageJob(item),
+                    new FullImageListener(item));
             // request full image
             return entry.fullImageTask;
         }
@@ -550,15 +734,13 @@
     }
 
     private void updateImageCache() {
-        HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet());
+        HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet());
         for (int i = mActiveStart; i < mActiveEnd; ++i) {
             MediaItem item = mData[i % DATA_CACHE_SIZE];
-            long version = item == null
-                    ? MediaObject.INVALID_DATA_VERSION
-                    : item.getDataVersion();
-            if (version == MediaObject.INVALID_DATA_VERSION) continue;
-            ImageEntry entry = mImageCache.get(version);
-            toBeRemoved.remove(version);
+            if (item == null) continue;
+            Path path = item.getPath();
+            ImageEntry entry = mImageCache.get(path);
+            toBeRemoved.remove(path);
             if (entry != null) {
                 if (Math.abs(i - mCurrentIndex) > 1) {
                     if (entry.fullImageTask != null) {
@@ -566,30 +748,39 @@
                         entry.fullImageTask = null;
                     }
                     entry.fullImage = null;
-                    entry.requestedBits &= ~BIT_FULL_IMAGE;
+                    entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+                }
+                if (entry.requestedScreenNail != item.getDataVersion()) {
+                    // This ScreenNail is outdated, we want to update it if it's
+                    // still a placeholder.
+                    if (entry.screenNail instanceof BitmapScreenNail) {
+                        BitmapScreenNail s = (BitmapScreenNail) entry.screenNail;
+                        s.updatePlaceholderSize(
+                                item.getWidth(), item.getHeight());
+                    }
                 }
             } else {
                 entry = new ImageEntry();
-                entry.rotation = item.getFullImageRotation();
-                mImageCache.put(version, entry);
+                mImageCache.put(path, entry);
             }
         }
 
         // Clear the data and requests for ImageEntries outside the new window.
-        for (Long version : toBeRemoved) {
-            ImageEntry entry = mImageCache.remove(version);
+        for (Path path : toBeRemoved) {
+            ImageEntry entry = mImageCache.remove(path);
             if (entry.fullImageTask != null) entry.fullImageTask.cancel();
             if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+            if (entry.screenNail != null) entry.screenNail.recycle();
         }
     }
 
     private class FullImageListener
             implements Runnable, FutureListener<BitmapRegionDecoder> {
-        private final long mVersion;
+        private final Path mPath;
         private Future<BitmapRegionDecoder> mFuture;
 
-        public FullImageListener(long version) {
-            mVersion = version;
+        public FullImageListener(MediaItem item) {
+            mPath = item.getPath();
         }
 
         @Override
@@ -601,21 +792,21 @@
 
         @Override
         public void run() {
-            updateFullImage(mVersion, mFuture);
+            updateFullImage(mPath, mFuture);
         }
     }
 
     private class ScreenNailListener
-            implements Runnable, FutureListener<Bitmap> {
-        private final long mVersion;
-        private Future<Bitmap> mFuture;
+            implements Runnable, FutureListener<ScreenNail> {
+        private final Path mPath;
+        private Future<ScreenNail> mFuture;
 
-        public ScreenNailListener(long version) {
-            mVersion = version;
+        public ScreenNailListener(MediaItem item) {
+            mPath = item.getPath();
         }
 
         @Override
-        public void onFutureDone(Future<Bitmap> future) {
+        public void onFutureDone(Future<ScreenNail> future) {
             mFuture = future;
             mMainHandler.sendMessage(
                     mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
@@ -623,17 +814,17 @@
 
         @Override
         public void run() {
-            updateScreenNail(mVersion, mFuture);
+            updateScreenNail(mPath, mFuture);
         }
     }
 
     private static class ImageEntry {
-        public int requestedBits = 0;
-        public int rotation;
         public BitmapRegionDecoder fullImage;
-        public Bitmap screenNail;
-        public Future<Bitmap> screenNailTask;
+        public ScreenNail screenNail;
+        public Future<ScreenNail> screenNailTask;
         public Future<BitmapRegionDecoder> fullImageTask;
+        public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+        public long requestedFullImage = MediaObject.INVALID_DATA_VERSION;
         public boolean failToLoad = false;
     }
 
@@ -738,7 +929,7 @@
             updateImageCache();
             updateTileProvider();
             updateImageRequests();
-            fireModelInvalidated();
+            fireDataChange();
             return null;
         }
 
@@ -746,11 +937,6 @@
             if (mSize == 0) return;
             if (mCurrentIndex >= mSize) {
                 mCurrentIndex = mSize - 1;
-                mPhotoView.notifyOnNewImage();
-                mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT);
-            } else {
-                mPhotoView.notifyOnNewImage();
-                mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT);
             }
         }
     }
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index ed67b08..36ccc67 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -16,13 +16,15 @@
 
 package com.android.gallery3d.app;
 
-import android.app.ActionBar;
 import android.app.ActionBar.OnMenuVisibilityListener;
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Rect;
 import android.net.Uri;
+import android.nfc.NfcAdapter;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
@@ -30,12 +32,12 @@
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.View.MeasureSpec;
 import android.view.WindowManager;
 import android.widget.ShareActionProvider;
 import android.widget.Toast;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaDetails;
 import com.android.gallery3d.data.MediaItem;
@@ -43,46 +45,61 @@
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.MtpDevice;
 import com.android.gallery3d.data.Path;
+import com.android.gallery3d.data.SnailAlbum;
+import com.android.gallery3d.data.SnailItem;
+import com.android.gallery3d.data.SnailSource;
 import com.android.gallery3d.picasasource.PicasaSource;
 import com.android.gallery3d.ui.DetailsHelper;
 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
 import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
-import com.android.gallery3d.ui.FilmStripView;
 import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
 import com.android.gallery3d.ui.GLView;
 import com.android.gallery3d.ui.ImportCompleteListener;
 import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
 import com.android.gallery3d.ui.PhotoView;
-import com.android.gallery3d.ui.PositionRepository;
-import com.android.gallery3d.ui.PositionRepository.Position;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SynchronizedHandler;
-import com.android.gallery3d.ui.UserInteractionListener;
 import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
 
-public class PhotoPage extends ActivityState
-        implements PhotoView.PhotoTapListener, FilmStripView.Listener,
-        UserInteractionListener {
+public class PhotoPage extends ActivityState implements
+        PhotoView.Listener, OrientationManager.Listener, AppBridge.Server {
     private static final String TAG = "PhotoPage";
 
     private static final int MSG_HIDE_BARS = 1;
+    private static final int MSG_LOCK_ORIENTATION = 2;
+    private static final int MSG_UNLOCK_ORIENTATION = 3;
+    private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
+    private static final int MSG_UPDATE_ACTION_BAR = 5;
+    private static final int MSG_UNFREEZE_GLROOT = 6;
+    private static final int MSG_WANT_BARS = 7;
 
     private static final int HIDE_BARS_TIMEOUT = 3500;
+    private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
 
     private static final int REQUEST_SLIDESHOW = 1;
     private static final int REQUEST_CROP = 2;
     private static final int REQUEST_CROP_PICASA = 3;
+    private static final int REQUEST_EDIT = 4;
+    private static final int REQUEST_PLAY_VIDEO = 5;
 
     public static final String KEY_MEDIA_SET_PATH = "media-set-path";
     public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
     public static final String KEY_INDEX_HINT = "index-hint";
+    public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
+    public static final String KEY_APP_BRIDGE = "app-bridge";
+    public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
+
+    public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
 
     private GalleryApp mApplication;
     private SelectionManager mSelectionManager;
 
     private PhotoView mPhotoView;
     private PhotoPage.Model mModel;
-    private FilmStripView mFilmStripView;
     private DetailsHelper mDetailsHelper;
     private boolean mShowDetails;
     private Path mPendingSharePath;
@@ -92,29 +109,38 @@
     private MediaSet mMediaSet;
     private Menu mMenu;
 
-    private final Intent mResultIntent = new Intent();
     private int mCurrentIndex = 0;
     private Handler mHandler;
     private boolean mShowBars = true;
-    private ActionBar mActionBar;
+    private volatile boolean mActionBarAllowed = true;
+    private GalleryActionBar mActionBar;
     private MyMenuVisibilityListener mMenuVisibilityListener;
     private boolean mIsMenuVisible;
-    private boolean mIsInteracting;
     private MediaItem mCurrentPhoto = null;
     private MenuExecutor mMenuExecutor;
     private boolean mIsActive;
     private ShareActionProvider mShareActionProvider;
+    private String mSetPathString;
+    // This is the original mSetPathString before adding the camera preview item.
+    private String mOriginalSetPathString;
+    private AppBridge mAppBridge;
+    private SnailItem mScreenNailItem;
+    private SnailAlbum mScreenNailSet;
+    private OrientationManager mOrientationManager;
+    private boolean mHasActivityResult;
+    private boolean mTreatBackAsUp;
+
+    private NfcAdapter mNfcAdapter;
 
     public static interface Model extends PhotoView.Model {
         public void resume();
         public void pause();
         public boolean isEmpty();
-        public MediaItem getCurrentMediaItem();
-        public int getCurrentIndex();
         public void setCurrentPhoto(Path path, int indexHint);
     }
 
     private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
+        @Override
         public void onMenuVisibilityChanged(boolean isVisible) {
             mIsMenuVisible = isVisible;
             refreshHidingMessage();
@@ -132,91 +158,90 @@
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
             mPhotoView.layout(0, 0, right - left, bottom - top);
-            PositionRepository.getInstance(mActivity).setOffset(0, 0);
-            int filmStripHeight = 0;
-            if (mFilmStripView != null) {
-                mFilmStripView.measure(
-                        MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
-                        MeasureSpec.UNSPECIFIED);
-                filmStripHeight = mFilmStripView.getMeasuredHeight();
-                mFilmStripView.layout(0, bottom - top - filmStripHeight,
-                        right - left, bottom - top);
-            }
             if (mShowDetails) {
-                mDetailsHelper.layout(left, GalleryActionBar.getHeight((Activity) mActivity),
-                        right, bottom);
+                mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
             }
         }
     };
 
-    private void initFilmStripView() {
-        Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity);
-        mFilmStripView = new FilmStripView(mActivity, mMediaSet,
-                config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin,
-                config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize,
-                config.filmstripGripSize, config.filmstripGripWidth);
-        mRootPane.addComponent(mFilmStripView);
-        mFilmStripView.setListener(this);
-        mFilmStripView.setUserInteractionListener(this);
-        mFilmStripView.setFocusIndex(mCurrentIndex);
-        mFilmStripView.setStartIndex(mCurrentIndex);
-        mRootPane.requestLayout();
-        if (mIsActive) mFilmStripView.resume();
-        if (!mShowBars) mFilmStripView.setVisibility(GLView.INVISIBLE);
-    }
-
     @Override
     public void onCreate(Bundle data, Bundle restoreState) {
-        mActionBar = ((Activity) mActivity).getActionBar();
+        mActionBar = mActivity.getGalleryActionBar();
         mSelectionManager = new SelectionManager(mActivity, false);
         mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
 
         mPhotoView = new PhotoView(mActivity);
-        mPhotoView.setPhotoTapListener(this);
+        mPhotoView.setListener(this);
         mRootPane.addComponent(mPhotoView);
         mApplication = (GalleryApp)((Activity) mActivity).getApplication();
+        mOrientationManager = mActivity.getOrientationManager();
+        mOrientationManager.addListener(this);
+        mActivity.getGLRoot().setOrientationSource(mOrientationManager);
 
-        String setPathString = data.getString(KEY_MEDIA_SET_PATH);
+        mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
+        mOriginalSetPathString = mSetPathString;
+        mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
         Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
+        mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
 
-        if (setPathString != null) {
-            mMediaSet = mActivity.getDataManager().getMediaSet(setPathString);
+        if (mSetPathString != null) {
+            mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE);
+            if (mAppBridge != null) {
+                mAppBridge.setServer(this);
+                mOrientationManager.lockOrientation();
+
+                // Get the ScreenNail from AppBridge and register it.
+                int id = SnailSource.newId();
+                Path screenNailSetPath = SnailSource.getSetPath(id);
+                Path screenNailItemPath = SnailSource.getItemPath(id);
+                mScreenNailSet = (SnailAlbum) mActivity.getDataManager()
+                        .getMediaObject(screenNailSetPath);
+                mScreenNailItem = (SnailItem) mActivity.getDataManager()
+                        .getMediaObject(screenNailItemPath);
+                mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+
+                // Combine the original MediaSet with the one for ScreenNail
+                // from AppBridge.
+                mSetPathString = "/combo/item/{" + screenNailSetPath +
+                        "," + mSetPathString + "}";
+
+                // Start from the screen nail.
+                itemPath = screenNailItemPath;
+
+                // Action bar should not be displayed when camera starts.
+                mFlags |= FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR;
+                mShowBars = false;
+            }
+
+            mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
+            mSelectionManager.setSourceMediaSet(mMediaSet);
             mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
-            mMediaSet = (MediaSet)
-                    mActivity.getDataManager().getMediaObject(setPathString);
             if (mMediaSet == null) {
-                Log.w(TAG, "failed to restore " + setPathString);
+                Log.w(TAG, "failed to restore " + mSetPathString);
             }
             PhotoDataAdapter pda = new PhotoDataAdapter(
-                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex);
+                    mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
+                    mAppBridge == null ? -1 : 0,
+                    mAppBridge == null ? false : mAppBridge.isPanorama());
             mModel = pda;
             mPhotoView.setModel(mModel);
 
-            mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex);
-            setStateResult(Activity.RESULT_OK, mResultIntent);
-
             pda.setDataListener(new PhotoDataAdapter.DataListener() {
 
                 @Override
                 public void onPhotoChanged(int index, Path item) {
-                    if (mFilmStripView != null) mFilmStripView.setFocusIndex(index);
                     mCurrentIndex = index;
-                    mResultIntent.putExtra(KEY_INDEX_HINT, index);
                     if (item != null) {
-                        mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString());
-                        MediaItem photo = mModel.getCurrentMediaItem();
+                        MediaItem photo = mModel.getMediaItem(0);
                         if (photo != null) updateCurrentPhoto(photo);
-                    } else {
-                        mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH);
                     }
-                    setStateResult(Activity.RESULT_OK, mResultIntent);
+                    updateBars();
                 }
 
                 @Override
                 public void onLoadingFinished() {
-                    GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
                     if (!mModel.isEmpty()) {
-                        MediaItem photo = mModel.getCurrentMediaItem();
+                        MediaItem photo = mModel.getMediaItem(0);
                         if (photo != null) updateCurrentPhoto(photo);
                     } else if (mIsActive) {
                         mActivity.getStateManager().finishState(PhotoPage.this);
@@ -225,12 +250,6 @@
 
                 @Override
                 public void onLoadingStarted() {
-                    GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
-                }
-
-                @Override
-                public void onPhotoAvailable(long version, boolean fullImage) {
-                    if (mFilmStripView == null) initFilmStripView();
                 }
             });
         } else {
@@ -250,13 +269,39 @@
                         hideBars();
                         break;
                     }
+                    case MSG_LOCK_ORIENTATION: {
+                        mOrientationManager.lockOrientation();
+                        break;
+                    }
+                    case MSG_UNLOCK_ORIENTATION: {
+                        mOrientationManager.unlockOrientation();
+                        break;
+                    }
+                    case MSG_ON_FULL_SCREEN_CHANGED: {
+                        mAppBridge.onFullScreenChanged(message.arg1 == 1);
+                        break;
+                    }
+                    case MSG_UPDATE_ACTION_BAR: {
+                        updateBars();
+                        break;
+                    }
+                    case MSG_WANT_BARS: {
+                        wantBars();
+                        break;
+                    }
+                    case MSG_UNFREEZE_GLROOT: {
+                        mActivity.getGLRoot().unfreeze();
+                        break;
+                    }
                     default: throw new AssertionError(message.what);
                 }
             }
         };
 
-        // start the opening animation
-        mPhotoView.setOpenedItem(itemPath);
+        // start the opening animation only if it's not restored.
+        if (restoreState == null) {
+            mPhotoView.setOpenAnimationRect((Rect) data.getParcelable(KEY_OPEN_ANIMATION_RECT));
+        }
     }
 
     private void updateShareURI(Path path) {
@@ -267,6 +312,10 @@
             intent.setType(MenuExecutor.getMimeType(type));
             intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
             mShareActionProvider.setShareIntent(intent);
+            if (mNfcAdapter != null) {
+                mNfcAdapter.setBeamPushUris(new Uri[]{manager.getContentUri(path)},
+                        (Activity)mActivity);
+            }
             mPendingSharePath = null;
         } else {
             // This happens when ActionBar is not created yet.
@@ -274,29 +323,28 @@
         }
     }
 
-    private void setTitle(String title) {
-        if (title == null) return;
-        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
-                R.bool.show_action_bar_title);
-        if (showTitle)
-            mActionBar.setTitle(title);
-        else
-            mActionBar.setTitle("");
-    }
-
     private void updateCurrentPhoto(MediaItem photo) {
         if (mCurrentPhoto == photo) return;
         mCurrentPhoto = photo;
         if (mCurrentPhoto == null) return;
         updateMenuOperations();
+        updateTitle();
         if (mShowDetails) {
             mDetailsHelper.reloadDetails(mModel.getCurrentIndex());
         }
-        setTitle(photo.getName());
-        mPhotoView.showVideoPlayIcon(
-                photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO);
+        if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
+            updateShareURI(photo.getPath());
+        }
+    }
 
-        updateShareURI(photo.getPath());
+    private void updateTitle() {
+        if (mCurrentPhoto == null) return;
+        boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
+                R.bool.show_action_bar_title);
+        if (showTitle && mCurrentPhoto.getName() != null)
+            mActionBar.setTitle(mCurrentPhoto.getName());
+        else
+            mActionBar.setTitle("");
     }
 
     private void updateMenuOperations() {
@@ -327,90 +375,141 @@
         return true;
     }
 
+    //////////////////////////////////////////////////////////////////////////
+    //  Action Bar show/hide management
+    //////////////////////////////////////////////////////////////////////////
+
     private void showBars() {
         if (mShowBars) return;
         mShowBars = true;
+        mOrientationManager.unlockOrientation();
         mActionBar.show();
-        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
-        params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
-        ((Activity) mActivity).getWindow().setAttributes(params);
-        if (mFilmStripView != null) {
-            mFilmStripView.show();
-        }
+        mActivity.getGLRoot().setLightsOutMode(false);
+        refreshHidingMessage();
     }
 
     private void hideBars() {
         if (!mShowBars) return;
         mShowBars = false;
         mActionBar.hide();
-        WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
-        params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
-        ((Activity) mActivity).getWindow().setAttributes(params);
-        if (mFilmStripView != null) {
-            mFilmStripView.hide();
-        }
+        mActivity.getGLRoot().setLightsOutMode(true);
+        mHandler.removeMessages(MSG_HIDE_BARS);
     }
 
     private void refreshHidingMessage() {
         mHandler.removeMessages(MSG_HIDE_BARS);
-        if (!mIsMenuVisible && !mIsInteracting) {
+        if (!mIsMenuVisible) {
             mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
         }
     }
 
-    @Override
-    public void onUserInteraction() {
-        showBars();
-        refreshHidingMessage();
+    private boolean canShowBars() {
+        // No bars if we are showing camera preview.
+        if (mAppBridge != null && mCurrentIndex == 0) return false;
+        // No bars if it's not allowed.
+        if (!mActionBarAllowed) return false;
+
+        return true;
     }
 
-    public void onUserInteractionTap() {
+    private void wantBars() {
+        if (canShowBars()) showBars();
+    }
+
+    private void toggleBars() {
         if (mShowBars) {
             hideBars();
-            mHandler.removeMessages(MSG_HIDE_BARS);
         } else {
-            showBars();
-            refreshHidingMessage();
+            if (canShowBars()) showBars();
+        }
+    }
+
+    private void updateBars() {
+        if (!canShowBars()) {
+            hideBars();
         }
     }
 
     @Override
-    public void onUserInteractionBegin() {
-        showBars();
-        mIsInteracting = true;
-        refreshHidingMessage();
-    }
-
-    @Override
-    public void onUserInteractionEnd() {
-        mIsInteracting = false;
-
-        // This function could be called from GL thread (in SlotView.render)
-        // and post to the main thread. So, it could be executed while the
-        // activity is paused.
-        if (mIsActive) refreshHidingMessage();
+    public void onOrientationCompensationChanged() {
+        mActivity.getGLRoot().requestLayoutContentPane();
     }
 
     @Override
     protected void onBackPressed() {
         if (mShowDetails) {
             hideDetails();
-        } else {
-            PositionRepository repository = PositionRepository.getInstance(mActivity);
-            repository.clear();
-            if (mCurrentPhoto != null) {
-                Position position = new Position();
-                position.x = mRootPane.getWidth() / 2;
-                position.y = mRootPane.getHeight() / 2;
-                position.z = -1000;
-                repository.putPosition(
-                        Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())),
-                        position);
+        } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
+            // We are leaving this page. Set the result now.
+            setResult();
+            if (mTreatBackAsUp) {
+                onUpPressed();
+            } else {
+                super.onBackPressed();
             }
-            super.onBackPressed();
         }
     }
 
+    private void onUpPressed() {
+        if (mActivity.getStateManager().getStateCount() > 1) {
+            super.onBackPressed();
+            return;
+        }
+
+        if (mOriginalSetPathString == null) return;
+
+        if (mAppBridge == null) {
+            // We're in view mode so set up the stacks on our own.
+            Bundle data = new Bundle(getData());
+            data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
+            data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+                    mActivity.getDataManager().getTopSetPath(
+                            DataManager.INCLUDE_ALL));
+            mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+        } else {
+            // Start the real gallery activity to view the camera roll.
+            Uri uri = Uri.parse("content://media/external/file?bucketId="
+                    + MediaSetUtils.CAMERA_BUCKET_ID);
+            Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setDataAndType(uri, ContentResolver.CURSOR_DIR_BASE_TYPE + "/image");
+            ((Activity) mActivity).startActivity(intent);
+        }
+    }
+
+    private void setResult() {
+        Intent result = null;
+        if (!mPhotoView.getFilmMode()) {
+            result = new Intent();
+            result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
+        }
+        setStateResult(Activity.RESULT_OK, result);
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  AppBridge.Server interface
+    //////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void setCameraRelativeFrame(Rect frame) {
+        mPhotoView.setCameraRelativeFrame(frame);
+    }
+
+    @Override
+    public boolean switchWithCaptureAnimation(int offset) {
+        return mPhotoView.switchWithCaptureAnimation(offset);
+    }
+
+    @Override
+    public void setSwipingEnabled(boolean enabled) {
+        mPhotoView.setSwipingEnabled(enabled);
+    }
+
+    @Override
+    public void notifyScreenNailChanged() {
+        mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+        mScreenNailSet.notifyChange();
+    }
+
     @Override
     protected boolean onCreateActionBar(Menu menu) {
         MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
@@ -418,14 +517,34 @@
         mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
         if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
         mMenu = menu;
-        mShowBars = true;
         updateMenuOperations();
+        updateTitle();
         return true;
     }
 
+    private MenuExecutor.ProgressListener mConfirmDialogListener =
+            new MenuExecutor.ProgressListener() {
+        @Override
+        public void onProgressUpdate(int index) {}
+
+        @Override
+        public void onProgressComplete(int result) {}
+
+        @Override
+        public void onConfirmDialogShown() {
+            mHandler.removeMessages(MSG_HIDE_BARS);
+        }
+
+        @Override
+        public void onConfirmDialogDismissed(boolean confirmed) {
+            refreshHidingMessage();
+        }
+    };
+
     @Override
     protected boolean onItemSelected(MenuItem item) {
-        MediaItem current = mModel.getCurrentMediaItem();
+        refreshHidingMessage();
+        MediaItem current = mModel.getMediaItem(0);
 
         if (current == null) {
             // item is not ready, ignore
@@ -437,7 +556,12 @@
 
         DataManager manager = mActivity.getDataManager();
         int action = item.getItemId();
+        String confirmMsg = null;
         switch (action) {
+            case android.R.id.home: {
+                onUpPressed();
+                return true;
+            }
             case R.id.action_slideshow: {
                 Bundle data = new Bundle();
                 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
@@ -458,6 +582,14 @@
                         : REQUEST_CROP);
                 return true;
             }
+            case R.id.action_edit: {
+                Intent intent = new Intent(Intent.ACTION_EDIT)
+                        .setData(manager.getContentUri(path))
+                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
+                        REQUEST_EDIT);
+                return true;
+            }
             case R.id.action_details: {
                 if (mShowDetails) {
                     hideDetails();
@@ -466,20 +598,21 @@
                 }
                 return true;
             }
+            case R.id.action_delete:
+                confirmMsg = mActivity.getResources().getQuantityString(
+                        R.plurals.delete_selection, 1);
             case R.id.action_setas:
-            case R.id.action_confirm_delete:
             case R.id.action_rotate_ccw:
             case R.id.action_rotate_cw:
             case R.id.action_show_on_map:
-            case R.id.action_edit:
                 mSelectionManager.deSelectAll();
                 mSelectionManager.toggle(path);
-                mMenuExecutor.onMenuClicked(item, null);
+                mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
                 return true;
             case R.id.action_import:
                 mSelectionManager.deSelectAll();
                 mSelectionManager.toggle(path);
-                mMenuExecutor.onMenuClicked(item,
+                mMenuExecutor.onMenuClicked(item, confirmMsg,
                         new ImportCompleteListener(mActivity));
                 return true;
             default :
@@ -497,6 +630,7 @@
         if (mDetailsHelper == null) {
             mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
             mDetailsHelper.setCloseListener(new CloseListener() {
+                @Override
                 public void onClose() {
                     hideDetails();
                 }
@@ -506,10 +640,18 @@
         mDetailsHelper.show();
     }
 
+    ////////////////////////////////////////////////////////////////////////////
+    //  Callbacks from PhotoView
+    ////////////////////////////////////////////////////////////////////////////
+    @Override
     public void onSingleTapUp(int x, int y) {
-        MediaItem item = mModel.getCurrentMediaItem();
-        if (item == null) {
-            // item is not ready, ignore
+        if (mAppBridge != null) {
+            if (mAppBridge.onSingleTapUp(x, y)) return;
+        }
+
+        MediaItem item = mModel.getMediaItem(0);
+        if (item == null || item == mScreenNailItem) {
+            // item is not ready or it is camera preview, ignore
             return;
         }
 
@@ -528,47 +670,79 @@
         if (playVideo) {
             playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
         } else {
-            onUserInteractionTap();
+            toggleBars();
         }
     }
 
+    @Override
+    public void lockOrientation() {
+        mHandler.sendEmptyMessage(MSG_LOCK_ORIENTATION);
+    }
+
+    @Override
+    public void unlockOrientation() {
+        mHandler.sendEmptyMessage(MSG_UNLOCK_ORIENTATION);
+    }
+
+    @Override
+    public void onActionBarAllowed(boolean allowed) {
+        mActionBarAllowed = allowed;
+        mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
+    }
+
+    @Override
+    public void onActionBarWanted() {
+        mHandler.sendEmptyMessage(MSG_WANT_BARS);
+    }
+
+    @Override
+    public void onFullScreenChanged(boolean full) {
+        Message m = mHandler.obtainMessage(
+                MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
+        m.sendToTarget();
+    }
+
     public static void playVideo(Activity activity, Uri uri, String title) {
         try {
             Intent intent = new Intent(Intent.ACTION_VIEW)
-                    .setDataAndType(uri, "video/*");
-            intent.putExtra(Intent.EXTRA_TITLE, title);
-            activity.startActivity(intent);
+                    .setDataAndType(uri, "video/*")
+                    .putExtra(Intent.EXTRA_TITLE, title)
+                    .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
+            activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
         } catch (ActivityNotFoundException e) {
             Toast.makeText(activity, activity.getString(R.string.video_err),
                     Toast.LENGTH_SHORT).show();
         }
     }
 
-    // Called by FileStripView.
-    // Returns false if it cannot jump to the specified index at this time.
-    public boolean onSlotSelected(int slotIndex) {
-        return mPhotoView.jumpTo(slotIndex);
+    private void setCurrentPhotoByIntent(Intent intent) {
+        if (intent == null) return;
+        Path path = mApplication.getDataManager()
+                .findPathByUri(intent.getData(), intent.getType());
+        if (path != null) {
+            mModel.setCurrentPhoto(path, mCurrentIndex);
+        }
     }
 
     @Override
     protected void onStateResult(int requestCode, int resultCode, Intent data) {
+        mHasActivityResult = true;
         switch (requestCode) {
+            case REQUEST_EDIT:
+                setCurrentPhotoByIntent(data);
+                break;
             case REQUEST_CROP:
                 if (resultCode == Activity.RESULT_OK) {
-                    if (data == null) break;
-                    Path path = mApplication
-                            .getDataManager().findPathByUri(data.getData());
-                    if (path != null) {
-                        mModel.setCurrentPhoto(path, mCurrentIndex);
-                    }
+                    setCurrentPhotoByIntent(data);
                 }
                 break;
             case REQUEST_CROP_PICASA: {
-                int message = resultCode == Activity.RESULT_OK
-                        ? R.string.crop_saved
-                        : R.string.crop_not_saved;
-                Toast.makeText(mActivity.getAndroidContext(),
-                        message, Toast.LENGTH_SHORT).show();
+                if (resultCode == Activity.RESULT_OK) {
+                    Context context = mActivity.getAndroidContext();
+                    String message = context.getString(R.string.crop_saved,
+                            context.getString(R.string.folder_download));
+                    Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+                }
                 break;
             }
             case REQUEST_SLIDESHOW: {
@@ -582,36 +756,104 @@
         }
     }
 
+    private class PreparePhotoFallback implements OnGLIdleListener {
+        private PhotoFallbackEffect mPhotoFallback = new PhotoFallbackEffect();
+        private boolean mResultReady = false;
+
+        public synchronized PhotoFallbackEffect get() {
+            while (!mResultReady) {
+                Utils.waitWithoutInterrupt(this);
+            }
+            return mPhotoFallback;
+        }
+
+        @Override
+        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+            mPhotoFallback = mPhotoView.buildFallbackEffect(mRootPane, canvas);
+            synchronized (this) {
+                mResultReady = true;
+                notifyAll();
+            }
+            return false;
+        }
+    }
+
+    private void preparePhotoFallbackView() {
+        GLRoot root = mActivity.getGLRoot();
+        PreparePhotoFallback task = new PreparePhotoFallback();
+        root.unlockRenderThread();
+        PhotoFallbackEffect anim;
+        try {
+            root.addOnGLIdleListener(task);
+            anim = task.get();
+        } finally {
+            root.lockRenderThread();
+        }
+        mActivity.getTransitionStore().put(
+                AlbumPage.KEY_RESUME_ANIMATION, anim);
+    }
+
     @Override
     public void onPause() {
         super.onPause();
         mIsActive = false;
-        if (mFilmStripView != null) {
-            mFilmStripView.pause();
-        }
+
+        mActivity.getGLRoot().unfreeze();
+        mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
+        if (isFinishing()) preparePhotoFallbackView();
+
         DetailsHelper.pause();
         mPhotoView.pause();
         mModel.pause();
         mHandler.removeMessages(MSG_HIDE_BARS);
         mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+
         mMenuExecutor.pause();
     }
 
     @Override
+    public void onCurrentImageUpdated() {
+        mActivity.getGLRoot().unfreeze();
+    }
+
+    @Override
     protected void onResume() {
         super.onResume();
+        mActivity.getGLRoot().freeze();
         mIsActive = true;
         setContentPane(mRootPane);
+
         mModel.resume();
         mPhotoView.resume();
-        if (mFilmStripView != null) {
-            mFilmStripView.resume();
-        }
         if (mMenuVisibilityListener == null) {
             mMenuVisibilityListener = new MyMenuVisibilityListener();
         }
+        mActionBar.setDisplayOptions(mSetPathString != null, true);
         mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
-        onUserInteraction();
+
+        if (mAppBridge != null && !mHasActivityResult) {
+            mPhotoView.resetToFirstPicture();
+        }
+        mHasActivityResult = false;
+        mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mAppBridge != null) {
+            mAppBridge.setServer(null);
+            mScreenNailItem.setScreenNail(null);
+            mAppBridge.detachScreenNail();
+            mAppBridge = null;
+            mScreenNailSet = null;
+            mScreenNailItem = null;
+        }
+        mOrientationManager.removeListener(this);
+        mActivity.getGLRoot().setOrientationSource(null);
+
+        // Remove all pending messages.
+        mHandler.removeCallbacksAndMessages(null);
+        super.onDestroy();
     }
 
     private class MyDetailsSource implements DetailsSource {
@@ -619,7 +861,7 @@
 
         @Override
         public MediaDetails getDetails() {
-            return mModel.getCurrentMediaItem().getDetails();
+            return mModel.getMediaItem(0).getDetails();
         }
 
         @Override
diff --git a/src/com/android/gallery3d/app/PickerActivity.java b/src/com/android/gallery3d/app/PickerActivity.java
index 944192d..f5b2cbd 100644
--- a/src/com/android/gallery3d/app/PickerActivity.java
+++ b/src/com/android/gallery3d/app/PickerActivity.java
@@ -16,10 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.ui.GLRoot;
-import com.android.gallery3d.ui.GLRootView;
-
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -28,6 +24,10 @@
 import android.view.View.OnClickListener;
 import android.view.Window;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+
 public class PickerActivity extends AbstractGalleryActivity
         implements OnClickListener {
 
@@ -74,19 +74,7 @@
             finish();
             return true;
         }
-        return false;
-    }
-
-    @Override
-    public void onBackPressed() {
-        // send the back event to the top sub-state
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            getStateManager().getTopState().onBackPressed();
-        } finally {
-            root.unlockRenderThread();
-        }
+        return super.onOptionsItemSelected(item);
     }
 
     @Override
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
index 0c54ada..111333e 100644
--- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -16,18 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.PhotoView;
-import com.android.gallery3d.ui.PhotoView.ImageData;
-import com.android.gallery3d.ui.SynchronizedHandler;
-import com.android.gallery3d.ui.TileImageViewAdapter;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureListener;
-import com.android.gallery3d.util.ThreadPool;
-
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
@@ -35,6 +23,18 @@
 import android.os.Handler;
 import android.os.Message;
 
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
 public class SinglePhotoDataAdapter extends TileImageViewAdapter
         implements PhotoPage.Model {
 
@@ -49,6 +49,7 @@
 
     private PhotoView mPhotoView;
     private ThreadPool mThreadPool;
+    private int mLoadingState = LOADING_INIT;
 
     public SinglePhotoDataAdapter(
             GalleryActivity activity, PhotoView view, MediaItem item) {
@@ -109,16 +110,12 @@
         return false;
     }
 
-    public int getImageRotation() {
-        return mItem.getRotation();
-    }
-
     private void onDecodeLargeComplete(ImageBundle bundle) {
         try {
-            setBackupImage(bundle.backupImage,
+            setScreenNail(bundle.backupImage,
                     bundle.decoder.getWidth(), bundle.decoder.getHeight());
             setRegionDecoder(bundle.decoder);
-            mPhotoView.notifyImageInvalidated(0);
+            mPhotoView.notifyImageChange(0);
         } catch (Throwable t) {
             Log.w(TAG, "fail to decode large", t);
         }
@@ -127,10 +124,14 @@
     private void onDecodeThumbComplete(Future<Bitmap> future) {
         try {
             Bitmap backup = future.get();
-            if (backup == null) return;
-            setBackupImage(backup, backup.getWidth(), backup.getHeight());
-            mPhotoView.notifyOnNewImage();
-            mPhotoView.notifyImageInvalidated(0); // the current image
+            if (backup == null) {
+                mLoadingState = LOADING_FAIL;
+                return;
+            } else {
+                mLoadingState = LOADING_COMPLETE;
+            }
+            setScreenNail(backup, backup.getWidth(), backup.getHeight());
+            mPhotoView.notifyImageChange(0);
         } catch (Throwable t) {
             Log.w(TAG, "fail to decode thumb", t);
         }
@@ -158,35 +159,69 @@
         }
     }
 
-    public ImageData getNextImage() {
-        return null;
-    }
-
-    public ImageData getPreviousImage() {
-        return null;
-    }
-
-    public void next() {
+    @Override
+    public void moveTo(int index) {
         throw new UnsupportedOperationException();
     }
 
-    public void previous() {
-        throw new UnsupportedOperationException();
+    @Override
+    public void getImageSize(int offset, PhotoView.Size size) {
+        if (offset == 0) {
+            size.width = mItem.getWidth();
+            size.height = mItem.getHeight();
+        } else {
+            size.width = 0;
+            size.height = 0;
+        }
     }
 
-    public void jumpTo(int index) {
-        throw new UnsupportedOperationException();
+    @Override
+    public int getImageRotation(int offset) {
+        return (offset == 0) ? mItem.getFullImageRotation() : 0;
     }
 
-    public MediaItem getCurrentMediaItem() {
-        return mItem;
+    @Override
+    public ScreenNail getScreenNail(int offset) {
+        return (offset == 0) ? getScreenNail() : null;
     }
 
+    @Override
+    public void setNeedFullImage(boolean enabled) {
+        // currently not necessary.
+    }
+
+    @Override
+    public boolean isCamera(int offset) {
+        return false;
+    }
+
+    @Override
+    public boolean isPanorama(int offset) {
+        return false;
+    }
+
+    @Override
+    public boolean isVideo(int offset) {
+        return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+    }
+
+    @Override
+    public MediaItem getMediaItem(int offset) {
+        return offset == 0 ? mItem : null;
+    }
+
+    @Override
     public int getCurrentIndex() {
         return 0;
     }
 
+    @Override
     public void setCurrentPhoto(Path path, int indexHint) {
         // ignore
     }
+
+    @Override
+    public int getLoadingState(int offset) {
+        return mLoadingState;
+    }
 }
diff --git a/src/com/android/gallery3d/app/SlideshowDream.java b/src/com/android/gallery3d/app/SlideshowDream.java
index 80f7fe0..9963da6 100644
--- a/src/com/android/gallery3d/app/SlideshowDream.java
+++ b/src/com/android/gallery3d/app/SlideshowDream.java
@@ -1,16 +1,10 @@
 package com.android.gallery3d.app;
 
-import android.app.Activity;
 import android.content.Intent;
-import android.graphics.Canvas;
-import android.net.Uri;
 import android.os.Bundle;
-import android.os.Environment;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.ViewFlipper;
+import android.support.v13.dreams.BasicDream;
 
-public class SlideshowDream extends Activity {
+public class SlideshowDream extends BasicDream {
     @Override
     public void onCreate(Bundle bndl) {
         super.onCreate(bndl);
@@ -20,6 +14,7 @@
 //            Uri.fromFile(Environment.getExternalStoragePublicDirectory(
 //                        Environment.DIRECTORY_PICTURES)))
                 .putExtra(Gallery.EXTRA_SLIDESHOW, true)
+                .putExtra(Gallery.EXTRA_DREAM, true)
                 .setFlags(getIntent().getFlags());
         startActivity(i);
         finish();
diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java
index 8697629..a6ae527 100644
--- a/src/com/android/gallery3d/app/SlideshowPage.java
+++ b/src/com/android/gallery3d/app/SlideshowPage.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.app;
 
 import android.app.Activity;
-import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.os.Bundle;
@@ -49,6 +48,7 @@
     public static final String KEY_PHOTO_INDEX = "photo-index";
     public static final String KEY_RANDOM_ORDER = "random-order";
     public static final String KEY_REPEAT = "repeat";
+    public static final String KEY_DREAM = "dream";
 
     private static final long SLIDESHOW_DELAY = 3000; // 3 seconds
 
@@ -105,7 +105,14 @@
 
     @Override
     public void onCreate(Bundle data, Bundle restoreState) {
-        mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR | FLAG_SCREEN_ON);
+        mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR);
+        if (data.getBoolean(KEY_DREAM)) {
+            // Dream screensaver only keeps screen on for plugged devices.
+            mFlags |= FLAG_SCREEN_ON_WHEN_PLUGGED;
+        } else {
+            // User-initiated slideshow would always keep screen on.
+            mFlags |= FLAG_SCREEN_ON_ALWAYS;
+        }
 
         mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
             @Override
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
index 556a06a..02e9976 100644
--- a/src/com/android/gallery3d/app/StateManager.java
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -16,8 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.common.Utils;
-
 import android.app.Activity;
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -26,6 +24,8 @@
 import android.view.Menu;
 import android.view.MenuItem;
 
+import com.android.gallery3d.common.Utils;
+
 import java.util.Stack;
 
 public class StateManager {
@@ -37,12 +37,10 @@
     private static final String KEY_DATA = "data";
     private static final String KEY_STATE = "bundle";
     private static final String KEY_CLASS = "class";
-    private static final String KEY_LAUNCH_GALLERY_ON_TOP = "launch-gallery-on-top";
 
     private GalleryActivity mContext;
     private Stack<StateEntry> mStack = new Stack<StateEntry>();
     private ActivityState.ResultEntry mResult;
-    private boolean mLaunchGalleryOnTop = false;
 
     public StateManager(GalleryActivity context) {
         mContext = context;
@@ -68,10 +66,6 @@
         if (mIsResumed) state.resume();
     }
 
-    public void setLaunchGalleryOnTop(boolean enabled) {
-        mLaunchGalleryOnTop = enabled;
-    }
-
     public void startStateForResult(Class<? extends ActivityState> klass,
             int requestCode, Bundle data) {
         Log.v(TAG, "startStateForResult " + klass + ", " + requestCode);
@@ -99,11 +93,10 @@
     }
 
     public boolean createOptionsMenu(Menu menu) {
-        if (!mStack.isEmpty()) {
-            ((Activity) mContext).setProgressBarIndeterminateVisibility(false);
-            return getTopState().onCreateActionBar(menu);
-        } else {
+        if (mStack.isEmpty()) {
             return false;
+        } else {
+            return getTopState().onCreateActionBar(menu);
         }
     }
 
@@ -135,18 +128,12 @@
 
     public boolean itemSelected(MenuItem item) {
         if (!mStack.isEmpty()) {
+            if (getTopState().onItemSelected(item)) return true;
             if (item.getItemId() == android.R.id.home) {
                 if (mStack.size() > 1) {
                     getTopState().onBackPressed();
-                } else if (mLaunchGalleryOnTop) {
-                    Activity activity = (Activity) mContext;
-                    Intent intent = new Intent(activity, Gallery.class)
-                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                    ((Activity) mContext).startActivity(intent);
                 }
                 return true;
-            } else {
-                return getTopState().onItemSelected(item);
             }
         }
         return false;
@@ -159,7 +146,22 @@
     }
 
     void finishState(ActivityState state) {
-        Log.v(TAG, "finishState " + state.getClass());
+        // The finish() request could be rejected (only happens under Monkey),
+        // If it is rejected, we won't close the last page.
+        if (mStack.size() == 1) {
+            Activity activity = (Activity) mContext.getAndroidContext();
+            if (mResult != null) {
+                activity.setResult(mResult.resultCode, mResult.resultData);
+            }
+            activity.finish();
+            if (!activity.isFinishing()) {
+                Log.w(TAG, "finish is rejected, keep the last state");
+                return;
+            }
+            Log.v(TAG, "no more state, finish activity");
+        }
+
+        Log.v(TAG, "finishState " + state);
         if (state != mStack.peek().activityState) {
             if (state.isDestroyed()) {
                 Log.d(TAG, "The state is already destroyed");
@@ -173,25 +175,12 @@
 
         // Remove the top state.
         mStack.pop();
+        state.mIsFinishing = true;
         if (mIsResumed) state.onPause();
         mContext.getGLRoot().setContentPane(null);
         state.onDestroy();
 
-        if (mStack.isEmpty()) {
-            Log.v(TAG, "no more state, finish activity");
-            Activity activity = (Activity) mContext.getAndroidContext();
-            if (mResult != null) {
-                activity.setResult(mResult.resultCode, mResult.resultData);
-            }
-            activity.finish();
-
-            // The finish() request is rejected (only happens under Monkey),
-            // so we start the default page instead.
-            if (!activity.isFinishing()) {
-                Log.v(TAG, "finish() failed, start default page");
-                ((Gallery) mContext).startDefaultPage();
-            }
-        } else {
+        if (!mStack.isEmpty()) {
             // Restore the immediately previous state
             ActivityState top = mStack.peek().activityState;
             if (mIsResumed) top.resume();
@@ -235,7 +224,6 @@
     @SuppressWarnings("unchecked")
     public void restoreFromState(Bundle inState) {
         Log.v(TAG, "restoreFromState");
-        mLaunchGalleryOnTop = inState.getBoolean(KEY_LAUNCH_GALLERY_ON_TOP, false);
         Parcelable list[] = inState.getParcelableArray(KEY_MAIN);
         for (Parcelable parcelable : list) {
             Bundle bundle = (Bundle) parcelable;
@@ -261,7 +249,6 @@
     public void saveState(Bundle outState) {
         Log.v(TAG, "saveState");
 
-        outState.putBoolean(KEY_LAUNCH_GALLERY_ON_TOP, mLaunchGalleryOnTop);
         Parcelable list[] = new Parcelable[mStack.size()];
         int i = 0;
         for (StateEntry entry : mStack) {
diff --git a/src/com/android/gallery3d/app/TimeBar.java b/src/com/android/gallery3d/app/TimeBar.java
index 0ac8efb..1f5bfd9 100644
--- a/src/com/android/gallery3d/app/TimeBar.java
+++ b/src/com/android/gallery3d/app/TimeBar.java
@@ -16,9 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.R;
-
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -29,6 +26,9 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+
 /**
  * The time bar view, which includes the current and total time, the progress bar,
  * and the scrubber.
diff --git a/src/com/android/gallery3d/app/TransitionStore.java b/src/com/android/gallery3d/app/TransitionStore.java
new file mode 100644
index 0000000..9c09e7b
--- /dev/null
+++ b/src/com/android/gallery3d/app/TransitionStore.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import java.util.HashMap;
+
+public class TransitionStore {
+    private HashMap<Object, Object> mStorage = new HashMap<Object, Object>();
+
+    public void put(Object key, Object value) {
+        mStorage.put(key, value);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T get(Object key) {
+        return (T) mStorage.get(key);
+    }
+
+    public void clear() {
+        mStorage.clear();
+    }
+}
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
index 07a3d53..c08c1d7 100644
--- a/src/com/android/gallery3d/app/Wallpaper.java
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -18,9 +18,9 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.graphics.Point;
 import android.net.Uri;
 import android.os.Bundle;
-import android.view.Display;
 
 /**
  * Wallpaper picker for the gallery application. This just redirects to the
@@ -78,9 +78,10 @@
             case STATE_PHOTO_PICKED: {
                 int width = getWallpaperDesiredMinimumWidth();
                 int height = getWallpaperDesiredMinimumHeight();
-                Display display = getWindowManager().getDefaultDisplay();
-                float spotlightX = (float) display.getWidth() / width;
-                float spotlightY = (float) display.getHeight() / height;
+                Point size = new Point();
+                getWindowManager().getDefaultDisplay().getSize(size);
+                float spotlightX = (float) size.x / width;
+                float spotlightY = (float) size.y / height;
                 Intent request = new Intent(CropImage.ACTION_CROP)
                         .setDataAndType(mPickedItem, IMAGE_TYPE)
                         .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
@@ -91,6 +92,7 @@
                         .putExtra(CropImage.KEY_SPOTLIGHT_X, spotlightX)
                         .putExtra(CropImage.KEY_SPOTLIGHT_Y, spotlightY)
                         .putExtra(CropImage.KEY_SCALE, true)
+                        .putExtra(CropImage.KEY_SCALE_UP_IF_NEEDED, true)
                         .putExtra(CropImage.KEY_NO_FACE_DETECTION, true)
                         .putExtra(CropImage.KEY_SET_AS_WALLPAPER, true);
                 startActivity(request);
diff --git a/src/com/android/gallery3d/data/BitmapPool.java b/src/com/android/gallery3d/data/BitmapPool.java
new file mode 100644
index 0000000..0fbd84e
--- /dev/null
+++ b/src/com/android/gallery3d/data/BitmapPool.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.Log;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+
+public class BitmapPool {
+    private static final String TAG = "BitmapPool";
+
+    private final ArrayList<Bitmap> mPool;
+    private final int mPoolLimit;
+
+    // mOneSize is true if the pool can only cache Bitmap with one size.
+    private final boolean mOneSize;
+    private final int mWidth, mHeight;  // only used if mOneSize is true
+
+    // Construct a BitmapPool which caches bitmap with the specified size.
+    public BitmapPool(int width, int height, int poolLimit) {
+        mWidth = width;
+        mHeight = height;
+        mPoolLimit = poolLimit;
+        mPool = new ArrayList<Bitmap>(poolLimit);
+        mOneSize = true;
+    }
+
+    // Construct a BitmapPool which caches bitmap with any size;
+    public BitmapPool(int poolLimit) {
+        mWidth = -1;
+        mHeight = -1;
+        mPoolLimit = poolLimit;
+        mPool = new ArrayList<Bitmap>(poolLimit);
+        mOneSize = false;
+    }
+
+    // Get a Bitmap from the pool.
+    public synchronized Bitmap getBitmap() {
+        Utils.assertTrue(mOneSize);
+        int size = mPool.size();
+        return size > 0 ? mPool.remove(size - 1) : null;
+    }
+
+    // Get a Bitmap from the pool with the specified size.
+    public synchronized Bitmap getBitmap(int width, int height) {
+        Utils.assertTrue(!mOneSize);
+        for (int i = mPool.size() - 1; i >= 0; i--) {
+            Bitmap b = mPool.get(i);
+            if (b.getWidth() == width && b.getHeight() == height) {
+                return mPool.remove(i);
+            }
+        }
+        return null;
+    }
+
+    // Put a Bitmap into the pool, if the Bitmap has a proper size. Otherwise
+    // the Bitmap will be recycled. If the pool is full, an old Bitmap will be
+    // recycled.
+    public void recycle(Bitmap bitmap) {
+        if (bitmap == null) return;
+        if (mOneSize && ((bitmap.getWidth() != mWidth) ||
+                (bitmap.getHeight() != mHeight))) {
+            bitmap.recycle();
+            return;
+        }
+        synchronized (this) {
+            if (mPool.size() >= mPoolLimit) mPool.remove(0);
+            mPool.add(bitmap);
+        }
+    }
+
+    public synchronized void clear() {
+        mPool.clear();
+    }
+
+    private Bitmap findCachedBitmap(JobContext jc,
+            byte[] data, int offset, int length, Options options) {
+        if (mOneSize) return getBitmap();
+        DecodeUtils.decodeBounds(jc, data, offset, length, options);
+        return getBitmap(options.outWidth, options.outHeight);
+    }
+
+    private Bitmap findCachedBitmap(JobContext jc,
+            FileDescriptor fileDescriptor, Options options) {
+        if (mOneSize) return getBitmap();
+        DecodeUtils.decodeBounds(jc, fileDescriptor, options);
+        return getBitmap(options.outWidth, options.outHeight);
+    }
+
+    public Bitmap decode(JobContext jc,
+            byte[] data, int offset, int length, BitmapFactory.Options options) {
+        if (options == null) options = new BitmapFactory.Options();
+        if (options.inSampleSize < 1) options.inSampleSize = 1;
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        options.inBitmap = (options.inSampleSize == 1)
+                ? findCachedBitmap(jc, data, offset, length, options) : null;
+        try {
+            Bitmap bitmap = DecodeUtils.decode(jc, data, offset, length, options);
+            if (options.inBitmap != null && options.inBitmap != bitmap) {
+                recycle(options.inBitmap);
+                options.inBitmap = null;
+            }
+            return bitmap;
+        } catch (IllegalArgumentException e) {
+            if (options.inBitmap == null) throw e;
+
+            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+            recycle(options.inBitmap);
+            options.inBitmap = null;
+            return DecodeUtils.decode(jc, data, offset, length, options);
+        }
+    }
+
+    // This is the same as the method above except the source data comes
+    // from a file descriptor instead of a byte array.
+    public Bitmap decode(JobContext jc,
+            FileDescriptor fileDescriptor, Options options) {
+        if (options == null) options = new BitmapFactory.Options();
+        if (options.inSampleSize < 1) options.inSampleSize = 1;
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        options.inBitmap = (options.inSampleSize == 1)
+                ? findCachedBitmap(jc, fileDescriptor, options) : null;
+        try {
+            Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
+            if (options.inBitmap != null&& options.inBitmap != bitmap) {
+                recycle(options.inBitmap);
+                options.inBitmap = null;
+            }
+            return bitmap;
+        } catch (IllegalArgumentException e) {
+            if (options.inBitmap == null) throw e;
+
+            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+            recycle(options.inBitmap);
+            options.inBitmap = null;
+            return DecodeUtils.decode(jc, fileDescriptor, options);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/data/BytesBufferPool.java b/src/com/android/gallery3d/data/BytesBufferPool.java
new file mode 100644
index 0000000..d2da323
--- /dev/null
+++ b/src/com/android/gallery3d/data/BytesBufferPool.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class BytesBufferPool {
+
+    private static final int READ_STEP = 4096;
+
+    public static class BytesBuffer {
+        public byte[] data;
+        public int offset;
+        public int length;
+
+        private BytesBuffer(int capacity) {
+            this.data = new byte[capacity];
+        }
+
+        // an helper function to read content from FileDescriptor
+        public void readFrom(JobContext jc, FileDescriptor fd) throws IOException {
+            FileInputStream fis = new FileInputStream(fd);
+            length = 0;
+            try {
+                int capacity = data.length;
+                while (true) {
+                    int step = Math.min(READ_STEP, capacity - length);
+                    int rc = fis.read(data, length, step);
+                    if (rc < 0 || jc.isCancelled()) return;
+                    length += rc;
+
+                    if (length == capacity) {
+                        byte[] newData = new byte[data.length * 2];
+                        System.arraycopy(data, 0, newData, 0, data.length);
+                        data = newData;
+                        capacity = data.length;
+                    }
+                }
+            } finally {
+                fis.close();
+            }
+        }
+    }
+
+    private final int mPoolSize;
+    private final int mBufferSize;
+    private final ArrayList<BytesBuffer> mList;
+
+    public BytesBufferPool(int poolSize, int bufferSize) {
+        mList = new ArrayList<BytesBuffer>(poolSize);
+        mPoolSize = poolSize;
+        mBufferSize = bufferSize;
+    }
+
+    public synchronized BytesBuffer get() {
+        int n = mList.size();
+        return n > 0 ? mList.remove(n - 1) : new BytesBuffer(mBufferSize);
+    }
+
+    public synchronized void recycle(BytesBuffer buffer) {
+        if (buffer.data.length != mBufferSize) return;
+        if (mList.size() < mPoolSize) {
+            buffer.offset = 0;
+            buffer.length = 0;
+            mList.add(buffer);
+        }
+    }
+
+    public synchronized void clear() {
+        mList.clear();
+    }
+}
diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java
index e1e601d..35be2dc 100644
--- a/src/com/android/gallery3d/data/ChangeNotifier.java
+++ b/src/com/android/gallery3d/data/ChangeNotifier.java
@@ -16,10 +16,10 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-
 import android.net.Uri;
 
+import com.android.gallery3d.app.GalleryApp;
+
 import java.util.concurrent.atomic.AtomicBoolean;
 
 // This handles change notification for media sets.
@@ -38,14 +38,11 @@
         return mContentDirty.compareAndSet(true, false);
     }
 
+    // For debugging only.
     public void fakeChange() {
         onChange(false);
     }
 
-    public void clearDirty() {
-        mContentDirty.set(false);
-    }
-
     protected void onChange(boolean selfChange) {
         if (mContentDirty.compareAndSet(false, true)) {
             mMediaSet.notifyContentChanged();
diff --git a/src/com/android/gallery3d/data/ClusterAlbumSet.java b/src/com/android/gallery3d/data/ClusterAlbumSet.java
index 16501c2..863e94a 100644
--- a/src/com/android/gallery3d/data/ClusterAlbumSet.java
+++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java
@@ -16,11 +16,11 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-
 import android.content.Context;
 import android.net.Uri;
 
+import com.android.gallery3d.app.GalleryApp;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 
diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java
index 6d22311..73c8ef3 100644
--- a/src/com/android/gallery3d/data/ComboAlbum.java
+++ b/src/com/android/gallery3d/data/ComboAlbum.java
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
 import com.android.gallery3d.util.Future;
 
 import java.util.ArrayList;
diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java
index 916b163..6d9a2a3 100644
--- a/src/com/android/gallery3d/data/ComboAlbumSet.java
+++ b/src/com/android/gallery3d/data/ComboAlbumSet.java
@@ -65,6 +65,14 @@
     }
 
     @Override
+    public boolean isLoading() {
+        for (int i = 0, n = mSets.length; i < n; ++i) {
+            if (mSets[i].isLoading()) return true;
+        }
+        return false;
+    }
+
+    @Override
     public long reload() {
         boolean changed = false;
         for (int i = 0, n = mSets.length; i < n; ++i) {
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
index f7dac5e..0eb6af5 100644
--- a/src/com/android/gallery3d/data/DataManager.java
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -16,16 +16,16 @@
 
 package com.android.gallery3d.data;
 
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
 import com.android.gallery3d.app.GalleryApp;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.MediaSet.ItemConsumer;
 import com.android.gallery3d.data.MediaSource.PathId;
 import com.android.gallery3d.picasasource.PicasaSource;
 
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -115,6 +115,7 @@
         addSource(new ClusterSource(mApplication));
         addSource(new FilterSource(mApplication));
         addSource(new UriSource(mApplication));
+        addSource(new SnailSource(mApplication));
 
         if (mActiveCount > 0) {
             for (MediaSource source : mSourceMap.values()) {
@@ -145,10 +146,6 @@
         return path.getObject();
     }
 
-    public MediaSet peekMediaSet(Path path) {
-        return (MediaSet) path.getObject();
-    }
-
     public MediaObject getMediaObject(Path path) {
         MediaObject obj = path.getObject();
         if (obj != null) return obj;
@@ -159,11 +156,16 @@
             return null;
         }
 
-        MediaObject object = source.createMediaObject(path);
-        if (object == null) {
-            Log.w(TAG, "cannot create media object: " + path);
+        try {
+            MediaObject object = source.createMediaObject(path);
+            if (object == null) {
+                Log.w(TAG, "cannot create media object: " + path);
+            }
+            return object;
+        } catch (Throwable t) {
+            Log.w(TAG, "exception in creating media object: " + path, t);
+            return null;
         }
-        return object;
     }
 
     public MediaObject getMediaObject(String s) {
@@ -240,18 +242,10 @@
         return getMediaObject(path).getMediaType();
     }
 
-    public MediaDetails getDetails(Path path) {
-        return getMediaObject(path).getDetails();
-    }
-
-    public void cache(Path path, int flag) {
-        getMediaObject(path).cache(flag);
-    }
-
-    public Path findPathByUri(Uri uri) {
+    public Path findPathByUri(Uri uri, String type) {
         if (uri == null) return null;
         for (MediaSource source : mSourceMap.values()) {
-            Path path = source.findPathByUri(uri);
+            Path path = source.findPathByUri(uri, type);
             if (path != null) return path;
         }
         return null;
diff --git a/src/com/android/gallery3d/data/DataSourceType.java b/src/com/android/gallery3d/data/DataSourceType.java
new file mode 100644
index 0000000..2761188
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataSourceType.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.util.MediaSetUtils;
+
+public final class DataSourceType {
+    public static final int TYPE_NOT_CATEGORIZED = 0;
+    public static final int TYPE_LOCAL = 1;
+    public static final int TYPE_PICASA = 2;
+    public static final int TYPE_MTP = 3;
+    public static final int TYPE_CAMERA = 4;
+
+    private static final Path PICASA_ROOT = Path.fromString("/picasa");
+    private static final Path LOCAL_ROOT = Path.fromString("/local");
+    private static final Path MTP_ROOT = Path.fromString("/mtp");
+
+    public static int identifySourceType(MediaSet set) {
+        if (set == null) {
+            return TYPE_NOT_CATEGORIZED;
+        }
+
+        Path path = set.getPath();
+        if (MediaSetUtils.isCameraSource(path)) return TYPE_CAMERA;
+
+        Path prefix = path.getPrefixPath();
+
+        if (prefix == PICASA_ROOT) return TYPE_PICASA;
+        if (prefix == MTP_ROOT) return TYPE_MTP;
+        if (prefix == LOCAL_ROOT) return TYPE_LOCAL;
+
+        return TYPE_NOT_CATEGORIZED;
+    }
+}
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
index b205576..e51dc3f 100644
--- a/src/com/android/gallery3d/data/DecodeUtils.java
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -16,20 +16,17 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.ThreadPool.CancelListener;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
-import android.content.ContentResolver;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapFactory.Options;
 import android.graphics.BitmapRegionDecoder;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.ParcelFileDescriptor;
+import android.util.FloatMath;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
 
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -40,35 +37,38 @@
 
     private static class DecodeCanceller implements CancelListener {
         Options mOptions;
+
         public DecodeCanceller(Options options) {
             mOptions = options;
         }
+
+        @Override
         public void onCancel() {
             mOptions.requestCancelDecode();
         }
     }
 
-    public static Bitmap requestDecode(JobContext jc, final String filePath,
-            Options options) {
-        if (options == null) options = new Options();
-        jc.setCancelListener(new DecodeCanceller(options));
-        return ensureGLCompatibleBitmap(
-                BitmapFactory.decodeFile(filePath, options));
-    }
-
-    public static Bitmap requestDecode(JobContext jc, FileDescriptor fd, Options options) {
+    public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) {
         if (options == null) options = new Options();
         jc.setCancelListener(new DecodeCanceller(options));
         return ensureGLCompatibleBitmap(
                 BitmapFactory.decodeFileDescriptor(fd, null, options));
     }
 
-    public static Bitmap requestDecode(JobContext jc, byte[] bytes,
+    public static void decodeBounds(JobContext jc, FileDescriptor fd,
             Options options) {
-        return requestDecode(jc, bytes, 0, bytes.length, options);
+        Utils.assertTrue(options != null);
+        options.inJustDecodeBounds = true;
+        jc.setCancelListener(new DecodeCanceller(options));
+        BitmapFactory.decodeFileDescriptor(fd, null, options);
+        options.inJustDecodeBounds = false;
     }
 
-    public static Bitmap requestDecode(JobContext jc, byte[] bytes, int offset,
+    public static Bitmap decode(JobContext jc, byte[] bytes, Options options) {
+        return decode(jc, bytes, 0, bytes.length, options);
+    }
+
+    public static Bitmap decode(JobContext jc, byte[] bytes, int offset,
             int length, Options options) {
         if (options == null) options = new Options();
         jc.setCancelListener(new DecodeCanceller(options));
@@ -76,13 +76,22 @@
                 BitmapFactory.decodeByteArray(bytes, offset, length, options));
     }
 
-    public static Bitmap requestDecode(JobContext jc, final String filePath,
-            Options options, int targetSize) {
+    public static void decodeBounds(JobContext jc, byte[] bytes, int offset,
+            int length, Options options) {
+        Utils.assertTrue(options != null);
+        options.inJustDecodeBounds = true;
+        jc.setCancelListener(new DecodeCanceller(options));
+        BitmapFactory.decodeByteArray(bytes, offset, length, options);
+        options.inJustDecodeBounds = false;
+    }
+
+    public static Bitmap decodeThumbnail(
+            JobContext jc, String filePath, Options options, int targetSize, int type) {
         FileInputStream fis = null;
         try {
             fis = new FileInputStream(filePath);
             FileDescriptor fd = fis.getFD();
-            return requestDecode(jc, fd, options, targetSize);
+            return decodeThumbnail(jc, fd, options, targetSize, type);
         } catch (Exception ex) {
             Log.w(TAG, ex);
             return null;
@@ -91,8 +100,8 @@
         }
     }
 
-    public static Bitmap requestDecode(JobContext jc, FileDescriptor fd,
-            Options options, int targetSize) {
+    public static Bitmap decodeThumbnail(
+            JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) {
         if (options == null) options = new Options();
         jc.setCancelListener(new DecodeCanceller(options));
 
@@ -100,14 +109,40 @@
         BitmapFactory.decodeFileDescriptor(fd, null, options);
         if (jc.isCancelled()) return null;
 
-        options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
-                options.outWidth, options.outHeight, targetSize);
+        int w = options.outWidth;
+        int h = options.outHeight;
+
+        if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+            // We center-crop the original image as it's micro thumbnail. In this case,
+            // we want to make sure the shorter side >= "targetSize".
+            float scale = (float) targetSize / Math.min(w, h);
+            options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+
+            // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding
+            // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here.
+            final int MAX_PIXEL_COUNT = 640000; // 400 x 1600
+            if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) {
+                options.inSampleSize = BitmapUtils.computeSampleSize(
+                        FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h)));
+            }
+        } else {
+            // For screen nail, we only want to keep the longer side >= targetSize.
+            float scale = (float) targetSize / Math.max(w, h);
+            options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+        }
+
         options.inJustDecodeBounds = false;
 
         Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options);
-        // We need to resize down if the decoder does not support inSampleSize.
-        // (For example, GIF images.)
-        result = BitmapUtils.resizeDownIfTooBig(result, targetSize, true);
+        if (result == null) return null;
+
+        // We need to resize down if the decoder does not support inSampleSize
+        // (For example, GIF images)
+        float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL
+                ? Math.min(result.getWidth(), result.getHeight())
+                : Math.max(result.getWidth(), result.getHeight()));
+
+        if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true);
         return ensureGLCompatibleBitmap(result);
     }
 
@@ -118,7 +153,7 @@
      * Note: The returned image may be resized down. However, both width and height must be
      * larger than the <code>targetSize</code>.
      */
-    public static Bitmap requestDecodeIfBigEnough(JobContext jc, byte[] data,
+    public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data,
             Options options, int targetSize) {
         if (options == null) options = new Options();
         jc.setCancelListener(new DecodeCanceller(options));
@@ -136,14 +171,6 @@
                 BitmapFactory.decodeByteArray(data, 0, data.length, options));
     }
 
-    public static Bitmap requestDecode(JobContext jc,
-            FileDescriptor fileDescriptor, Rect paddings, Options options) {
-        if (options == null) options = new Options();
-        jc.setCancelListener(new DecodeCanceller(options));
-        return ensureGLCompatibleBitmap(BitmapFactory.decodeFileDescriptor
-                (fileDescriptor, paddings, options));
-    }
-
     // TODO: This function should not be called directly from
     // DecodeUtils.requestDecode(...), since we don't have the knowledge
     // if the bitmap will be uploaded to GL.
@@ -154,7 +181,7 @@
         return newBitmap;
     }
 
-    public static BitmapRegionDecoder requestCreateBitmapRegionDecoder(
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
             JobContext jc, byte[] bytes, int offset, int length,
             boolean shareable) {
         if (offset < 0 || length <= 0 || offset + length > bytes.length) {
@@ -172,7 +199,7 @@
         }
     }
 
-    public static BitmapRegionDecoder requestCreateBitmapRegionDecoder(
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
             JobContext jc, String filePath, boolean shareable) {
         try {
             return BitmapRegionDecoder.newInstance(filePath, shareable);
@@ -182,7 +209,7 @@
         }
     }
 
-    public static BitmapRegionDecoder requestCreateBitmapRegionDecoder(
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
             JobContext jc, FileDescriptor fd, boolean shareable) {
         try {
             return BitmapRegionDecoder.newInstance(fd, shareable);
@@ -192,7 +219,7 @@
         }
     }
 
-    public static BitmapRegionDecoder requestCreateBitmapRegionDecoder(
+    public static BitmapRegionDecoder createBitmapRegionDecoder(
             JobContext jc, InputStream is, boolean shareable) {
         try {
             return BitmapRegionDecoder.newInstance(is, shareable);
@@ -203,20 +230,4 @@
             return null;
         }
     }
-
-    public static BitmapRegionDecoder requestCreateBitmapRegionDecoder(
-            JobContext jc, Uri uri, ContentResolver resolver,
-            boolean shareable) {
-        ParcelFileDescriptor pfd = null;
-        try {
-            pfd = resolver.openFileDescriptor(uri, "r");
-            return BitmapRegionDecoder.newInstance(
-                    pfd.getFileDescriptor(), shareable);
-        } catch (Throwable t) {
-            Log.w(TAG, t);
-            return null;
-        } finally {
-            Utils.closeSilently(pfd);
-        }
-    }
 }
diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java
index 30ba668..7505dac 100644
--- a/src/com/android/gallery3d/data/DownloadCache.java
+++ b/src/com/android/gallery3d/data/DownloadCache.java
@@ -16,6 +16,12 @@
 
 package com.android.gallery3d.data;
 
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
 import com.android.gallery3d.app.GalleryApp;
 import com.android.gallery3d.common.LruCache;
 import com.android.gallery3d.common.Utils;
@@ -27,17 +33,10 @@
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
 
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-
 import java.io.File;
 import java.net.URL;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.WeakHashMap;
 
 public class DownloadCache {
     private static final String TAG = "DownloadCache";
@@ -78,7 +77,6 @@
 
     private long mTotalBytes = 0;
     private boolean mInitialized = false;
-    private WeakHashMap<Object, Entry> mAssociateMap = new WeakHashMap<Object, Entry>();
 
     public DownloadCache(GalleryApp application, File root, long capacity) {
         mRoot = Utils.checkNotNull(root);
@@ -113,31 +111,6 @@
         return null;
     }
 
-    public Entry lookup(URL url) {
-        if (!mInitialized) initialize();
-        String stringUrl = url.toString();
-
-        // First find in the entry-pool
-        synchronized (mEntryMap) {
-            Entry entry = mEntryMap.get(stringUrl);
-            if (entry != null) {
-                updateLastAccess(entry.mId);
-                return entry;
-            }
-        }
-
-        // Then, find it in database
-        TaskProxy proxy = new TaskProxy();
-        synchronized (mTaskMap) {
-            Entry entry = findEntryInDatabase(stringUrl);
-            if (entry != null) {
-                updateLastAccess(entry.mId);
-                return entry;
-            }
-        }
-        return null;
-    }
-
     public Entry download(JobContext jc, URL url) {
         if (!mInitialized) initialize();
 
@@ -282,10 +255,6 @@
             mId = id;
             this.cacheFile = Utils.checkNotNull(cacheFile);
         }
-
-        public void associateWith(Object object) {
-            mAssociateMap.put(Utils.checkNotNull(object), this);
-        }
     }
 
     private class DownloadTask implements Job<File>, FutureListener<File> {
diff --git a/src/com/android/gallery3d/data/DownloadUtils.java b/src/com/android/gallery3d/data/DownloadUtils.java
index 9632db9..9e35c45 100644
--- a/src/com/android/gallery3d/data/DownloadUtils.java
+++ b/src/com/android/gallery3d/data/DownloadUtils.java
@@ -20,7 +20,6 @@
 import com.android.gallery3d.util.ThreadPool.CancelListener;
 import com.android.gallery3d.util.ThreadPool.JobContext;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -44,22 +43,6 @@
         }
     }
 
-    public static byte[] requestDownload(JobContext jc, URL url) {
-        ByteArrayOutputStream baos = null;
-        try {
-            baos = new ByteArrayOutputStream();
-            if (!download(jc, url, baos)) {
-                return null;
-            }
-            return baos.toByteArray();
-        } catch (Throwable t) {
-            Log.w(TAG, t);
-            return null;
-        } finally {
-            Utils.closeSilently(baos);
-        }
-    }
-
     public static void dump(JobContext jc, InputStream is, OutputStream os)
             throws IOException {
         byte buffer[] = new byte[4096];
diff --git a/src/com/android/gallery3d/data/Exif.java b/src/com/android/gallery3d/data/Exif.java
index 6f314df..ba5862a 100644
--- a/src/com/android/gallery3d/data/Exif.java
+++ b/src/com/android/gallery3d/data/Exif.java
@@ -18,8 +18,8 @@
 
 import android.util.Log;
 
-import java.io.InputStream;
 import java.io.IOException;
+import java.io.InputStream;
 
 public class Exif {
     private static final String TAG = "CameraExif";
diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java
index c5fd131..29af27f 100644
--- a/src/com/android/gallery3d/data/Face.java
+++ b/src/com/android/gallery3d/data/Face.java
@@ -45,22 +45,10 @@
         return mPosition;
     }
 
-    public int getWidth() {
-        return mPosition.right - mPosition.left;
-    }
-
-    public int getHeight() {
-        return mPosition.bottom - mPosition.top;
-    }
-
     public String getName() {
         return mName;
     }
 
-    public String getPersonId() {
-        return mPersonId;
-    }
-
     @Override
     public boolean equals(Object obj) {
         if (obj instanceof Face) {
diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java
index 104ff48..81660c9 100644
--- a/src/com/android/gallery3d/data/ImageCacheRequest.java
+++ b/src/com/android/gallery3d/data/ImageCacheRequest.java
@@ -16,15 +16,15 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.data.ImageCacheService.ImageData;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 abstract class ImageCacheRequest implements Job<Bitmap> {
     private static final String TAG = "ImageCacheRequest";
 
@@ -41,48 +41,56 @@
         mTargetSize = targetSize;
     }
 
+    @Override
     public Bitmap run(JobContext jc) {
         String debugTag = mPath + "," +
                  ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" :
                  (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?");
         ImageCacheService cacheService = mApplication.getImageCacheService();
 
-        ImageData data = cacheService.getImageData(mPath, mType);
+        BytesBuffer buffer = MediaItem.getBytesBufferPool().get();
+        try {
+            boolean found = cacheService.getImageData(mPath, mType, buffer);
+            if (jc.isCancelled()) return null;
+            if (found) {
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+                Bitmap bitmap;
+                if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+                    bitmap = MediaItem.getMicroThumbPool().decode(jc,
+                            buffer.data, buffer.offset, buffer.length, options);
+                } else {
+                    bitmap = MediaItem.getThumbPool().decode(jc,
+                            buffer.data, buffer.offset, buffer.length, options);
+                }
+                if (bitmap == null && !jc.isCancelled()) {
+                    Log.w(TAG, "decode cached failed " + debugTag);
+                }
+                return bitmap;
+            }
+        } finally {
+            MediaItem.getBytesBufferPool().recycle(buffer);
+        }
+        Bitmap bitmap = onDecodeOriginal(jc, mType);
         if (jc.isCancelled()) return null;
 
-        if (data != null) {
-            BitmapFactory.Options options = new BitmapFactory.Options();
-            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
-            Bitmap bitmap = DecodeUtils.requestDecode(jc, data.mData,
-                    data.mOffset, data.mData.length - data.mOffset, options);
-            if (bitmap == null && !jc.isCancelled()) {
-                Log.w(TAG, "decode cached failed " + debugTag);
-            }
-            return bitmap;
-        } else {
-            Bitmap bitmap = onDecodeOriginal(jc, mType);
-            if (jc.isCancelled()) return null;
-
-            if (bitmap == null) {
-                Log.w(TAG, "decode orig failed " + debugTag);
-                return null;
-            }
-
-            if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
-                bitmap = BitmapUtils.resizeDownAndCropCenter(bitmap,
-                        mTargetSize, true);
-            } else {
-                bitmap = BitmapUtils.resizeDownBySideLength(bitmap,
-                        mTargetSize, true);
-            }
-            if (jc.isCancelled()) return null;
-
-            byte[] array = BitmapUtils.compressBitmap(bitmap);
-            if (jc.isCancelled()) return null;
-
-            cacheService.putImageData(mPath, mType, array);
-            return bitmap;
+        if (bitmap == null) {
+            Log.w(TAG, "decode orig failed " + debugTag);
+            return null;
         }
+
+        if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+            bitmap = BitmapUtils.resizeAndCropCenter(bitmap, mTargetSize, true);
+        } else {
+            bitmap = BitmapUtils.resizeDownBySideLength(bitmap, mTargetSize, true);
+        }
+        if (jc.isCancelled()) return null;
+
+        byte[] array = BitmapUtils.compressToBytes(bitmap);
+        if (jc.isCancelled()) return null;
+
+        cacheService.putImageData(mPath, mType, array);
+        return bitmap;
     }
 
     public abstract Bitmap onDecodeOriginal(JobContext jc, int targetSize);
diff --git a/src/com/android/gallery3d/data/ImageCacheService.java b/src/com/android/gallery3d/data/ImageCacheService.java
index 3adce13..0e79313 100644
--- a/src/com/android/gallery3d/data/ImageCacheService.java
+++ b/src/com/android/gallery3d/data/ImageCacheService.java
@@ -16,13 +16,15 @@
 
 package com.android.gallery3d.data;
 
+import android.content.Context;
+
 import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.common.BlobCache.LookupRequest;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
 import com.android.gallery3d.util.CacheManager;
 import com.android.gallery3d.util.GalleryUtils;
 
-import android.content.Context;
-
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
@@ -33,7 +35,7 @@
     private static final String IMAGE_CACHE_FILE = "imgcache";
     private static final int IMAGE_CACHE_MAX_ENTRIES = 5000;
     private static final int IMAGE_CACHE_MAX_BYTES = 200 * 1024 * 1024;
-    private static final int IMAGE_CACHE_VERSION = 3;
+    private static final int IMAGE_CACHE_VERSION = 4;
 
     private BlobCache mCache;
 
@@ -43,32 +45,35 @@
                 IMAGE_CACHE_VERSION);
     }
 
-    public static class ImageData {
-        public ImageData(byte[] data, int offset) {
-            mData = data;
-            mOffset = offset;
-        }
-        public byte[] mData;
-        public int mOffset;
-    }
-
-    public ImageData getImageData(Path path, int type) {
+    /**
+     * Gets the cached image data for the given <code>path</code> and <code>type</code>.
+     *
+     * The image data will be stored in <code>buffer.data</code>, started from
+     * <code>buffer.offset</code> for <code>buffer.length</code> bytes. If the
+     * buffer.data is not big enough, a new byte array will be allocated and returned.
+     *
+     * @return true if the image data is found; false if not found.
+     */
+    public boolean getImageData(Path path, int type, BytesBuffer buffer) {
         byte[] key = makeKey(path, type);
         long cacheKey = Utils.crc64Long(key);
         try {
-            byte[] value = null;
+            LookupRequest request = new LookupRequest();
+            request.key = cacheKey;
+            request.buffer = buffer.data;
             synchronized (mCache) {
-                value = mCache.lookup(cacheKey);
+                if (!mCache.lookup(request)) return false;
             }
-            if (value == null) return null;
-            if (isSameKey(key, value)) {
-                int offset = key.length;
-                return new ImageData(value, offset);
+            if (isSameKey(key, request.buffer)) {
+                buffer.data = request.buffer;
+                buffer.offset = key.length;
+                buffer.length = request.length - buffer.offset;
+                return true;
             }
         } catch (IOException ex) {
             // ignore.
         }
-        return null;
+        return false;
     }
 
     public void putImageData(Path path, int type, byte[] value) {
diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java
index 5bd4398..117dbb6 100644
--- a/src/com/android/gallery3d/data/LocalAlbum.java
+++ b/src/com/android/gallery3d/data/LocalAlbum.java
@@ -16,18 +16,22 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.ContentResolver;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
+import android.provider.MediaStore;
 import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Images.ImageColumns;
 import android.provider.MediaStore.Video;
 import android.provider.MediaStore.Video.VideoColumns;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
 import java.util.ArrayList;
 
 // LocalAlbumSet lists all media items in one bucket on local storage.
@@ -45,7 +49,7 @@
     private final GalleryApp mApplication;
     private final ContentResolver mResolver;
     private final int mBucketId;
-    private final String mBucketName;
+    private final String mName;
     private final boolean mIsImage;
     private final ChangeNotifier mNotifier;
     private final Path mItemPath;
@@ -57,7 +61,7 @@
         mApplication = application;
         mResolver = application.getContentResolver();
         mBucketId = bucketId;
-        mBucketName = name;
+        mName = getLocalizedName(application.getResources(), bucketId, name);
         mIsImage = isImage;
 
         if (isImage) {
@@ -87,6 +91,19 @@
     }
 
     @Override
+    public Uri getContentUri() {
+        if (mIsImage) {
+            return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+                            String.valueOf(mBucketId)).build();
+        } else {
+            return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon()
+                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+                            String.valueOf(mBucketId)).build();
+        }
+    }
+
+    @Override
     public ArrayList<MediaItem> getMediaItem(int start, int count) {
         DataManager dataManager = mApplication.getDataManager();
         Uri uri = mBaseUri.buildUpon()
@@ -221,7 +238,7 @@
 
     @Override
     public String getName() {
-        return mBucketName;
+        return mName;
     }
 
     @Override
@@ -249,4 +266,19 @@
     public boolean isLeafAlbum() {
         return true;
     }
+
+    private static String getLocalizedName(Resources res, int bucketId,
+            String name) {
+        if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
+            return res.getString(R.string.folder_camera);
+        } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
+            return res.getString(R.string.folder_download);
+        } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
+            return res.getString(R.string.folder_imported);
+        } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
+            return res.getString(R.string.folder_screenshot);
+        } else {
+            return name;
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java
index a8c1aa7..07741ef 100644
--- a/src/com/android/gallery3d/data/LocalAlbumSet.java
+++ b/src/com/android/gallery3d/data/LocalAlbumSet.java
@@ -16,29 +16,33 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.MediaSetUtils;
-
 import android.content.ContentResolver;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Handler;
 import android.provider.MediaStore.Files;
 import android.provider.MediaStore.Files.FileColumns;
 import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Images.ImageColumns;
 import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.JobContext;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Comparator;
-import java.util.HashSet;
 
 // LocalAlbumSet lists all image or video albums in the local storage.
 // The path should be "/local/image", "local/video" or "/local/all"
-public class LocalAlbumSet extends MediaSet {
+public class LocalAlbumSet extends MediaSet
+        implements FutureListener<ArrayList<MediaSet>> {
     public static final Path PATH_ALL = Path.fromString("/local/all");
     public static final Path PATH_IMAGE = Path.fromString("/local/image");
     public static final Path PATH_VIDEO = Path.fromString("/local/video");
@@ -95,10 +99,16 @@
     private final ChangeNotifier mNotifierImage;
     private final ChangeNotifier mNotifierVideo;
     private final String mName;
+    private final Handler mHandler;
+    private boolean mIsLoading;
+
+    private Future<ArrayList<MediaSet>> mLoadTask;
+    private ArrayList<MediaSet> mLoadBuffer;
 
     public LocalAlbumSet(Path path, GalleryApp application) {
         super(path, nextVersionNumber());
         mApplication = application;
+        mHandler = new Handler(application.getMainLooper());
         mType = getTypeFromPath(path);
         mNotifierImage = new ChangeNotifier(this, mWatchUriImage, application);
         mNotifierVideo = new ChangeNotifier(this, mWatchUriVideo, application);
@@ -111,10 +121,7 @@
         if (name.length < 2) {
             throw new IllegalArgumentException(path.toString());
         }
-        if ("all".equals(name[1])) return MEDIA_TYPE_ALL;
-        if ("image".equals(name[1])) return MEDIA_TYPE_IMAGE;
-        if ("video".equals(name[1])) return MEDIA_TYPE_VIDEO;
-        throw new IllegalArgumentException(path.toString());
+        return getTypeFromString(name[1]);
     }
 
     @Override
@@ -132,7 +139,16 @@
         return mName;
     }
 
-    private BucketEntry[] loadBucketEntries(Cursor cursor) {
+    private BucketEntry[] loadBucketEntries(JobContext jc) {
+        Uri uri = mBaseUri;
+
+        Log.v("DebugLoadingTime", "start quering media provider");
+        Cursor cursor = mApplication.getContentResolver().query(
+                uri, PROJECTION_BUCKET, BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
+        if (cursor == null) {
+            Log.w(TAG, "cannot open local database: " + uri);
+            return new BucketEntry[0];
+        }
         ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
         int typeBits = 0;
         if ((mType & MEDIA_TYPE_IMAGE) != 0) {
@@ -151,7 +167,9 @@
                         buffer.add(entry);
                     }
                 }
+                if (jc.isCancelled()) return null;
             }
+            Log.v("DebugLoadingTime", "got " + buffer.size() + " buckets");
         } finally {
             cursor.close();
         }
@@ -166,62 +184,59 @@
         return -1;
     }
 
-    @SuppressWarnings("unchecked")
-    protected ArrayList<MediaSet> loadSubMediaSets() {
-        // Note: it will be faster if we only select media_type and bucket_id.
-        //       need to test the performance if that is worth
+    private class AlbumsLoader implements ThreadPool.Job<ArrayList<MediaSet>> {
 
-        Uri uri = mBaseUri;
-        GalleryUtils.assertNotInRenderThread();
-        Cursor cursor = mApplication.getContentResolver().query(
-                uri, PROJECTION_BUCKET, BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
-        if (cursor == null) {
-            Log.w(TAG, "cannot open local database: " + uri);
-            return new ArrayList<MediaSet>();
-        }
-        BucketEntry[] entries = loadBucketEntries(cursor);
-        int offset = 0;
+        @Override
+        @SuppressWarnings("unchecked")
+        public ArrayList<MediaSet> run(JobContext jc) {
+            // Note: it will be faster if we only select media_type and bucket_id.
+            //       need to test the performance if that is worth
+            BucketEntry[] entries = loadBucketEntries(jc);
 
-        // Move camera and download bucket to the front, while keeping the
-        // order of others.
-        int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
-        if (index != -1) {
-            circularShiftRight(entries, offset++, index);
-        }
-        index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
-        if (index != -1) {
-            circularShiftRight(entries, offset++, index);
-        }
+            if (jc.isCancelled()) return null;
 
-        ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
-        DataManager dataManager = mApplication.getDataManager();
-        for (BucketEntry entry : entries) {
-            albums.add(getLocalAlbum(dataManager,
-                    mType, mPath, entry.bucketId, entry.bucketName));
+            int offset = 0;
+            // Move camera and download bucket to the front, while keeping the
+            // order of others.
+            int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
+            if (index != -1) {
+                circularShiftRight(entries, offset++, index);
+            }
+            index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
+            if (index != -1) {
+                circularShiftRight(entries, offset++, index);
+            }
+
+            ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
+            DataManager dataManager = mApplication.getDataManager();
+            for (BucketEntry entry : entries) {
+                MediaSet album = getLocalAlbum(dataManager,
+                        mType, mPath, entry.bucketId, entry.bucketName);
+                albums.add(album);
+            }
+            return albums;
         }
-        for (int i = 0, n = albums.size(); i < n; ++i) {
-            albums.get(i).reload();
-        }
-        return albums;
     }
 
     private MediaSet getLocalAlbum(
             DataManager manager, int type, Path parent, int id, String name) {
-        Path path = parent.getChild(id);
-        MediaObject object = manager.peekMediaObject(path);
-        if (object != null) return (MediaSet) object;
-        switch (type) {
-            case MEDIA_TYPE_IMAGE:
-                return new LocalAlbum(path, mApplication, id, true, name);
-            case MEDIA_TYPE_VIDEO:
-                return new LocalAlbum(path, mApplication, id, false, name);
-            case MEDIA_TYPE_ALL:
-                Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
-                return new LocalMergeAlbum(path, comp, new MediaSet[] {
-                        getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name),
-                        getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)});
+        synchronized (DataManager.LOCK) {
+            Path path = parent.getChild(id);
+            MediaObject object = manager.peekMediaObject(path);
+            if (object != null) return (MediaSet) object;
+            switch (type) {
+                case MEDIA_TYPE_IMAGE:
+                    return new LocalAlbum(path, mApplication, id, true, name);
+                case MEDIA_TYPE_VIDEO:
+                    return new LocalAlbum(path, mApplication, id, false, name);
+                case MEDIA_TYPE_ALL:
+                    Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
+                    return new LocalMergeAlbum(path, comp, new MediaSet[] {
+                            getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name),
+                            getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}, id);
+            }
+            throw new IllegalArgumentException(String.valueOf(type));
         }
-        throw new IllegalArgumentException(String.valueOf(type));
     }
 
     public static String getBucketName(ContentResolver resolver, int bucketId) {
@@ -247,15 +262,46 @@
     }
 
     @Override
-    public long reload() {
+    public synchronized boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Override
+    // synchronized on this function for
+    //   1. Prevent calling reload() concurrently.
+    //   2. Prevent calling onFutureDone() and reload() concurrently
+    public synchronized long reload() {
         // "|" is used instead of "||" because we want to clear both flags.
         if (mNotifierImage.isDirty() | mNotifierVideo.isDirty()) {
+            if (mLoadTask != null) mLoadTask.cancel();
+            mIsLoading = true;
+            mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this);
+        }
+        if (mLoadBuffer != null) {
+            mAlbums = mLoadBuffer;
+            mLoadBuffer = null;
+            for (MediaSet album : mAlbums) {
+                album.reload();
+            }
             mDataVersion = nextVersionNumber();
-            mAlbums = loadSubMediaSets();
         }
         return mDataVersion;
     }
 
+    @Override
+    public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) {
+        if (mLoadTask != future) return; // ignore, wait for the latest task
+        mLoadBuffer = future.get();
+        mIsLoading = false;
+        if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>();
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                notifyContentChanged();
+            }
+        });
+    }
+
     // For debug only. Fake there is a ContentObserver.onChange() event.
     void fakeChange() {
         mNotifierImage.fakeChange();
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index fa3ece3..aa27c6f 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -16,13 +16,6 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-import com.android.gallery3d.util.UpdateHelper;
-
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.Cursor;
@@ -35,6 +28,13 @@
 import android.provider.MediaStore.Images.ImageColumns;
 import android.util.Log;
 
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
 import java.io.File;
 import java.io.IOException;
 
@@ -57,7 +57,7 @@
     private static final int INDEX_DATA = 8;
     private static final int INDEX_ORIENTATION = 9;
     private static final int INDEX_BUCKET_ID = 10;
-    private static final int INDEX_SIZE_ID = 11;
+    private static final int INDEX_SIZE = 11;
     private static final int INDEX_WIDTH = 12;
     private static final int INDEX_HEIGHT = 13;
 
@@ -74,16 +74,13 @@
             ImageColumns.ORIENTATION,   // 9
             ImageColumns.BUCKET_ID,     // 10
             ImageColumns.SIZE,          // 11
-            // These should be changed to proper names after they are made public.
-            "width", // ImageColumns.WIDTH,         // 12
-            "height", // ImageColumns.HEIGHT         // 13
+            ImageColumns.WIDTH,         // 12
+            ImageColumns.HEIGHT         // 13
     };
 
     private final GalleryApp mApplication;
 
     public int rotation;
-    public int width;
-    public int height;
 
     public LocalImage(Path path, GalleryApp application, Cursor cursor) {
         super(path, nextVersionNumber());
@@ -121,7 +118,7 @@
         filePath = cursor.getString(INDEX_DATA);
         rotation = cursor.getInt(INDEX_ORIENTATION);
         bucketId = cursor.getInt(INDEX_BUCKET_ID);
-        fileSize = cursor.getLong(INDEX_SIZE_ID);
+        fileSize = cursor.getLong(INDEX_SIZE);
         width = cursor.getInt(INDEX_WIDTH);
         height = cursor.getInt(INDEX_HEIGHT);
     }
@@ -143,7 +140,7 @@
         filePath = uh.update(filePath, cursor.getString(INDEX_DATA));
         rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION));
         bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
-        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID));
+        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
         width = uh.update(width, cursor.getInt(INDEX_WIDTH));
         height = uh.update(height, cursor.getInt(INDEX_HEIGHT));
         return uh.isUpdated();
@@ -159,14 +156,15 @@
 
         LocalImageRequest(GalleryApp application, Path path, int type,
                 String localFilePath) {
-            super(application, path, type, getTargetSize(type));
+            super(application, path, type, MediaItem.getTargetSize(type));
             mLocalFilePath = localFilePath;
         }
 
         @Override
-        public Bitmap onDecodeOriginal(JobContext jc, int type) {
+        public Bitmap onDecodeOriginal(JobContext jc, final int type) {
             BitmapFactory.Options options = new BitmapFactory.Options();
             options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+            int targetSize = MediaItem.getTargetSize(type);
 
             // try to decode from JPEG EXIF
             if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
@@ -181,25 +179,13 @@
                     Log.w(TAG, "fail to get exif thumb", t);
                 }
                 if (thumbData != null) {
-                    Bitmap bitmap = DecodeUtils.requestDecodeIfBigEnough(
-                            jc, thumbData, options, getTargetSize(type));
+                    Bitmap bitmap = DecodeUtils.decodeIfBigEnough(
+                            jc, thumbData, options, targetSize);
                     if (bitmap != null) return bitmap;
                 }
             }
-            return DecodeUtils.requestDecode(
-                    jc, mLocalFilePath, options, getTargetSize(type));
-        }
-    }
 
-    static int getTargetSize(int type) {
-        switch (type) {
-            case TYPE_THUMBNAIL:
-                return THUMBNAIL_TARGET_SIZE;
-            case TYPE_MICROTHUMBNAIL:
-                return MICROTHUMBNAIL_TARGET_SIZE;
-            default:
-                throw new RuntimeException(
-                    "should only request thumb/microthumb from cache");
+            return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type);
         }
     }
 
@@ -217,8 +203,7 @@
         }
 
         public BitmapRegionDecoder run(JobContext jc) {
-            return DecodeUtils.requestCreateBitmapRegionDecoder(
-                    jc, mLocalFilePath, false);
+            return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false);
         }
     }
 
diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java
index a76fedf..7a54e80 100644
--- a/src/com/android/gallery3d/data/LocalMediaItem.java
+++ b/src/com/android/gallery3d/data/LocalMediaItem.java
@@ -16,10 +16,10 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.database.Cursor;
 
+import com.android.gallery3d.util.GalleryUtils;
+
 import java.text.DateFormat;
 import java.util.Date;
 
@@ -44,6 +44,8 @@
     public long dateModifiedInSec;
     public String filePath;
     public int bucketId;
+    public int width;
+    public int height;
 
     public LocalMediaItem(Path path, long version) {
         super(path, version);
diff --git a/src/com/android/gallery3d/data/LocalMergeAlbum.java b/src/com/android/gallery3d/data/LocalMergeAlbum.java
index bb796d5..1e34e78 100644
--- a/src/com/android/gallery3d/data/LocalMergeAlbum.java
+++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java
@@ -16,6 +16,9 @@
 
 package com.android.gallery3d.data;
 
+import android.net.Uri;
+import android.provider.MediaStore;
+
 import java.lang.ref.SoftReference;
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -38,16 +41,18 @@
     private String mName;
     private FetchCache[] mFetcher;
     private int mSupportedOperation;
+    private int mBucketId;
 
     // mIndex maps global position to the position of each underlying media sets.
     private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>();
 
     public LocalMergeAlbum(
-            Path path, Comparator<MediaItem> comparator, MediaSet[] sources) {
+            Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId) {
         super(path, INVALID_DATA_VERSION);
         mComparator = comparator;
         mSources = sources;
         mName = sources.length == 0 ? "" : sources[0].getName();
+        mBucketId = bucketId;
         for (MediaSet set : mSources) {
             set.addContentListener(this);
         }
@@ -76,6 +81,12 @@
     }
 
     @Override
+    public Uri getContentUri() {
+        return MediaStore.Files.getContentUri("external").buildUpon().appendQueryParameter(
+                LocalSource.KEY_BUCKET_ID, String.valueOf(mBucketId)).build();
+    }
+
+    @Override
     public String getName() {
         return mName;
     }
diff --git a/src/com/android/gallery3d/data/LocalSource.java b/src/com/android/gallery3d/data/LocalSource.java
index 9bb561b..6735b16 100644
--- a/src/com/android/gallery3d/data/LocalSource.java
+++ b/src/com/android/gallery3d/data/LocalSource.java
@@ -75,6 +75,8 @@
                 "external/images/media", LOCAL_IMAGE_ALBUM);
         mUriMatcher.addURI(MediaStore.AUTHORITY,
                 "external/video/media", LOCAL_VIDEO_ALBUM);
+        mUriMatcher.addURI(MediaStore.AUTHORITY,
+                "external/file", LOCAL_ALL_ALBUM);
     }
 
     @Override
@@ -98,7 +100,7 @@
                         LocalAlbumSet.PATH_VIDEO.getChild(bucketId));
                 Comparator<MediaItem> comp = DataManager.sDateTakenComparator;
                 return new LocalMergeAlbum(
-                        path, comp, new MediaSet[] {imageSet, videoSet});
+                        path, comp, new MediaSet[] {imageSet, videoSet}, bucketId);
             }
             case LOCAL_IMAGE_ITEM:
                 return new LocalImage(path, mApplication, mMatcher.getIntVar(0));
@@ -122,6 +124,7 @@
     }
 
     // The media type bit passed by the intent
+    private static final int MEDIA_TYPE_ALL = 0;
     private static final int MEDIA_TYPE_IMAGE = 1;
     private static final int MEDIA_TYPE_VIDEO = 4;
 
@@ -148,7 +151,7 @@
     }
 
     @Override
-    public Path findPathByUri(Uri uri) {
+    public Path findPathByUri(Uri uri, String type) {
         try {
             switch (mUriMatcher.match(uri)) {
                 case LOCAL_IMAGE_ITEM: {
@@ -165,6 +168,9 @@
                 case LOCAL_VIDEO_ALBUM: {
                     return getAlbumPath(uri, MEDIA_TYPE_VIDEO);
                 }
+                case LOCAL_ALL_ALBUM: {
+                    return getAlbumPath(uri, MEDIA_TYPE_ALL);
+                }
             }
         } catch (NumberFormatException e) {
             Log.w(TAG, "uri: " + uri.toString(), e);
diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java
index ca379c3..4e888a5 100644
--- a/src/com/android/gallery3d/data/LocalVideo.java
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -16,30 +16,24 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.util.UpdateHelper;
-import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.content.ContentResolver;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
-import android.graphics.Canvas;
-import android.graphics.Paint;
 import android.net.Uri;
 import android.provider.MediaStore.Video;
 import android.provider.MediaStore.Video.VideoColumns;
 
-import java.io.File;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
 
 // LocalVideo represents a video in the local storage.
 public class LocalVideo extends LocalMediaItem {
-
+    private static final String TAG = "LocalVideo";
     static final Path ITEM_PATH = Path.fromString("/local/video/item");
 
     // Must preserve order between these indices and the order of the terms in
@@ -55,7 +49,8 @@
     private static final int INDEX_DATA = 8;
     private static final int INDEX_DURATION = 9;
     private static final int INDEX_BUCKET_ID = 10;
-    private static final int INDEX_SIZE_ID = 11;
+    private static final int INDEX_SIZE = 11;
+    private static final int INDEX_RESOLUTION = 12;
 
     static final String[] PROJECTION = new String[] {
             VideoColumns._ID,
@@ -69,11 +64,11 @@
             VideoColumns.DATA,
             VideoColumns.DURATION,
             VideoColumns.BUCKET_ID,
-            VideoColumns.SIZE
+            VideoColumns.SIZE,
+            VideoColumns.RESOLUTION,
     };
 
     private final GalleryApp mApplication;
-    private static Bitmap sOverlay;
 
     public int durationInSec;
 
@@ -113,7 +108,22 @@
         filePath = cursor.getString(INDEX_DATA);
         durationInSec = cursor.getInt(INDEX_DURATION) / 1000;
         bucketId = cursor.getInt(INDEX_BUCKET_ID);
-        fileSize = cursor.getLong(INDEX_SIZE_ID);
+        fileSize = cursor.getLong(INDEX_SIZE);
+        parseResolution(cursor.getString(INDEX_RESOLUTION));
+    }
+
+    private void parseResolution(String resolution) {
+        if (resolution == null) return;
+        int m = resolution.indexOf('x');
+        if (m == -1) return;
+        try {
+            int w = Integer.parseInt(resolution.substring(0, m));
+            int h = Integer.parseInt(resolution.substring(m + 1));
+            width = w;
+            height = h;
+        } catch (Throwable t) {
+            Log.w(TAG, t);
+        }
     }
 
     @Override
@@ -134,7 +144,7 @@
         durationInSec = uh.update(
                 durationInSec, cursor.getInt(INDEX_DURATION) / 1000);
         bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
-        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID));
+        fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
         return uh.isUpdated();
     }
 
@@ -148,7 +158,7 @@
 
         LocalVideoRequest(GalleryApp application, Path path, int type,
                 String localFilePath) {
-            super(application, path, type, LocalImage.getTargetSize(type));
+            super(application, path, type, MediaItem.getTargetSize(type));
             mLocalFilePath = localFilePath;
         }
 
@@ -213,11 +223,11 @@
 
     @Override
     public int getWidth() {
-        return 0;
+        return width;
     }
 
     @Override
     public int getHeight() {
-        return 0;
+        return height;
     }
 }
diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java
index 788060c..b36bd63 100644
--- a/src/com/android/gallery3d/data/LocationClustering.java
+++ b/src/com/android/gallery3d/data/LocationClustering.java
@@ -16,15 +16,16 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.util.ReverseGeocoder;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
+import android.util.FloatMath;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ReverseGeocoder;
+
 import java.util.ArrayList;
 
 class LocationClustering extends Clustering {
@@ -294,7 +295,7 @@
             }
 
             // step 5: calculate the final score
-            float score = totalDistance * (float) Math.sqrt(realK);
+            float score = totalDistance * FloatMath.sqrt(realK);
 
             if (score < bestScore) {
                 bestScore = score;
diff --git a/src/com/android/gallery3d/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java
index 1b56ac4..9663dd0 100644
--- a/src/com/android/gallery3d/data/MediaDetails.java
+++ b/src/com/android/gallery3d/data/MediaDetails.java
@@ -16,15 +16,15 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-
 import android.media.ExifInterface;
 
+import com.android.gallery3d.R;
+
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.TreeMap;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 
 public class MediaDetails implements Iterable<Entry<Integer, Object>> {
     @SuppressWarnings("unused")
@@ -73,22 +73,6 @@
         public boolean isFlashFired() {
             return (mState & FLASH_FIRED_MASK) != 0;
         }
-
-        public int getFlashReturn() {
-            return (mState & FLASH_RETURN_MASK) >> 1;
-        }
-
-        public int getFlashMode() {
-            return (mState & FLASH_MODE_MASK) >> 3;
-        }
-
-        public boolean isFlashPresent() {
-            return (mState & FLASH_FUNCTION_MASK) != 0;
-        }
-
-        public boolean isRedEyeModePresent() {
-            return (mState & FLASH_RED_EYE_MASK) != 0;
-        }
     }
 
     public void addDetail(int index, Object value) {
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
index 1361232..f0f1af4 100644
--- a/src/com/android/gallery3d/data/MediaItem.java
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -16,11 +16,12 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.util.ThreadPool.Job;
-
 import android.graphics.Bitmap;
 import android.graphics.BitmapRegionDecoder;
 
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+
 // MediaItem represents an image or a video item.
 public abstract class MediaItem extends MediaObject {
     // NOTE: These type numbers are stored in the image cache, so it should not
@@ -38,6 +39,15 @@
 
     public static final String MIME_TYPE_JPEG = "image/jpeg";
 
+    private static final int BYTESBUFFE_POOL_SIZE = 4;
+    private static final int BYTESBUFFER_SIZE = 200 * 1024;
+
+    private static final BitmapPool sMicroThumbPool = new BitmapPool(
+            MICROTHUMBNAIL_TARGET_SIZE, MICROTHUMBNAIL_TARGET_SIZE, 16);
+    private static final BitmapPool sThumbPool = new BitmapPool(4);
+    private static final BytesBufferPool sMicroThumbBufferPool =
+            new BytesBufferPool(BYTESBUFFE_POOL_SIZE, BYTESBUFFER_SIZE);
+
     // TODO: fix default value for latlng and change this.
     public static final double INVALID_LATLNG = 0f;
 
@@ -89,4 +99,34 @@
     // Returns 0, 0 if the information is not available.
     public abstract int getWidth();
     public abstract int getHeight();
+
+    // This is an alternative for requestImage() in PhotoPage. If this
+    // is implemented, you don't need to implement requestImage().
+    public ScreenNail getScreenNail() {
+        return null;
+    }
+
+    public static int getTargetSize(int type) {
+        switch (type) {
+            case TYPE_THUMBNAIL:
+                return THUMBNAIL_TARGET_SIZE;
+            case TYPE_MICROTHUMBNAIL:
+                return MICROTHUMBNAIL_TARGET_SIZE;
+            default:
+                throw new RuntimeException(
+                    "should only request thumb/microthumb from cache");
+        }
+    }
+
+    public static BitmapPool getMicroThumbPool() {
+        return sMicroThumbPool;
+    }
+
+    public static BitmapPool getThumbPool() {
+        return sThumbPool;
+    }
+
+    public static BytesBufferPool getBytesBufferPool() {
+        return sMicroThumbBufferPool;
+    }
 }
diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java
index d0f1672..f78aa7a 100644
--- a/src/com/android/gallery3d/data/MediaObject.java
+++ b/src/com/android/gallery3d/data/MediaObject.java
@@ -44,6 +44,10 @@
     public static final int MEDIA_TYPE_VIDEO = 4;
     public static final int MEDIA_TYPE_ALL = MEDIA_TYPE_IMAGE | MEDIA_TYPE_VIDEO;
 
+    public static final String MEDIA_TYPE_IMAGE_STRING = "image";
+    public static final String MEDIA_TYPE_VIDEO_STRING = "video";
+    public static final String MEDIA_TYPE_ALL_STRING = "all";
+
     // These are flags for cache() and return values for getCacheFlag():
     public static final int CACHE_FLAG_NO = 0;
     public static final int CACHE_FLAG_SCREENNAIL = 1;
@@ -127,4 +131,20 @@
     public static synchronized long nextVersionNumber() {
         return ++MediaObject.sVersionSerial;
     }
+
+    public static int getTypeFromString(String s) {
+        if (MEDIA_TYPE_ALL_STRING.equals(s)) return MediaObject.MEDIA_TYPE_ALL;
+        if (MEDIA_TYPE_IMAGE_STRING.equals(s)) return MediaObject.MEDIA_TYPE_IMAGE;
+        if (MEDIA_TYPE_VIDEO_STRING.equals(s)) return MediaObject.MEDIA_TYPE_VIDEO;
+        throw new IllegalArgumentException(s);
+    }
+
+    public static String getTypeString(int type) {
+        switch (type) {
+            case MEDIA_TYPE_IMAGE: return MEDIA_TYPE_IMAGE_STRING;
+            case MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO_STRING;
+            case MEDIA_TYPE_ALL: return MEDIA_TYPE_ALL_STRING;
+        }
+        throw new IllegalArgumentException();
+    }
 }
diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java
index ff9b8c3..67525b1 100644
--- a/src/com/android/gallery3d/data/MediaSet.java
+++ b/src/com/android/gallery3d/data/MediaSet.java
@@ -94,6 +94,14 @@
         return false;
     }
 
+    /**
+     * Method {@link #reload()} may process the loading task in background, this method tells
+     * its client whether the loading is still in process or not.
+     */
+    public boolean isLoading() {
+        return false;
+    }
+
     public int getTotalMediaItemCount() {
         int total = getMediaItemCount();
         for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java
index ae98e0f..9f7561e 100644
--- a/src/com/android/gallery3d/data/MediaSource.java
+++ b/src/com/android/gallery3d/data/MediaSource.java
@@ -16,10 +16,10 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.data.MediaSet.ItemConsumer;
-
 import android.net.Uri;
 
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
 import java.util.ArrayList;
 
 public abstract class MediaSource {
@@ -34,7 +34,7 @@
         return mPrefix;
     }
 
-    public Path findPathByUri(Uri uri) {
+    public Path findPathByUri(Uri uri, String type) {
         return null;
     }
 
diff --git a/src/com/android/gallery3d/data/MtpClient.java b/src/com/android/gallery3d/data/MtpClient.java
index 6991c16..2d58df2 100644
--- a/src/com/android/gallery3d/data/MtpClient.java
+++ b/src/com/android/gallery3d/data/MtpClient.java
@@ -27,13 +27,10 @@
 import android.hardware.usb.UsbInterface;
 import android.hardware.usb.UsbManager;
 import android.mtp.MtpDevice;
-import android.mtp.MtpDeviceInfo;
 import android.mtp.MtpObjectInfo;
 import android.mtp.MtpStorageInfo;
-import android.os.ParcelFileDescriptor;
 import android.util.Log;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
diff --git a/src/com/android/gallery3d/data/MtpContext.java b/src/com/android/gallery3d/data/MtpContext.java
index 6528494..0749020 100644
--- a/src/com/android/gallery3d/data/MtpContext.java
+++ b/src/com/android/gallery3d/data/MtpContext.java
@@ -1,10 +1,6 @@
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.Context;
-import android.hardware.usb.UsbDevice;
 import android.media.MediaScannerConnection;
 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
 import android.mtp.MtpObjectInfo;
@@ -13,6 +9,10 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.GalleryUtils;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
@@ -20,8 +20,6 @@
 public class MtpContext implements MtpClient.Listener {
     private static final String TAG = "MtpContext";
 
-    public static final String NAME_IMPORTED_FOLDER = "Imported";
-
     private ScannerClient mScannerClient;
     private Context mContext;
     private MtpClient mClient;
@@ -105,7 +103,7 @@
     public boolean copyFile(String deviceName, MtpObjectInfo objInfo) {
         if (GalleryUtils.hasSpaceForSize(objInfo.getCompressedSize())) {
             File dest = Environment.getExternalStorageDirectory();
-            dest = new File(dest, NAME_IMPORTED_FOLDER);
+            dest = new File(dest, BucketNames.IMPORTED);
             dest.mkdirs();
             String destPath = new File(dest, objInfo.getName()).getAbsolutePath();
             int objectId = objInfo.getObjectHandle();
diff --git a/src/com/android/gallery3d/data/MtpDevice.java b/src/com/android/gallery3d/data/MtpDevice.java
index e654583..f43ae2b 100644
--- a/src/com/android/gallery3d/data/MtpDevice.java
+++ b/src/com/android/gallery3d/data/MtpDevice.java
@@ -16,8 +16,6 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-
 import android.hardware.usb.UsbDevice;
 import android.mtp.MtpConstants;
 import android.mtp.MtpObjectInfo;
@@ -25,6 +23,8 @@
 import android.net.Uri;
 import android.util.Log;
 
+import com.android.gallery3d.app.GalleryApp;
+
 import java.util.ArrayList;
 import java.util.List;
 
diff --git a/src/com/android/gallery3d/data/MtpDeviceSet.java b/src/com/android/gallery3d/data/MtpDeviceSet.java
index 6521623..1f26511 100644
--- a/src/com/android/gallery3d/data/MtpDeviceSet.java
+++ b/src/com/android/gallery3d/data/MtpDeviceSet.java
@@ -16,27 +16,38 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.util.MediaSetUtils;
-
 import android.mtp.MtpDeviceInfo;
 import android.net.Uri;
+import android.os.Handler;
 import android.util.Log;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 // MtpDeviceSet -- MtpDevice -- MtpImage
-public class MtpDeviceSet extends MediaSet {
+public class MtpDeviceSet extends MediaSet
+        implements FutureListener<ArrayList<MediaSet>> {
     private static final String TAG = "MtpDeviceSet";
 
     private GalleryApp mApplication;
-    private final ArrayList<MediaSet> mDeviceSet = new ArrayList<MediaSet>();
     private final ChangeNotifier mNotifier;
     private final MtpContext mMtpContext;
     private final String mName;
+    private final Handler mHandler;
+
+    private Future<ArrayList<MediaSet>> mLoadTask;
+    private ArrayList<MediaSet> mDeviceSet = new ArrayList<MediaSet>();
+    private ArrayList<MediaSet> mLoadBuffer;
+    private boolean mIsLoading;
 
     public MtpDeviceSet(Path path, GalleryApp application, MtpContext mtpContext) {
         super(path, nextVersionNumber());
@@ -44,28 +55,32 @@
         mNotifier = new ChangeNotifier(this, Uri.parse("mtp://"), application);
         mMtpContext = mtpContext;
         mName = application.getResources().getString(R.string.set_label_mtp_devices);
+        mHandler = new Handler(mApplication.getMainLooper());
     }
 
-    private void loadDevices() {
-        DataManager dataManager = mApplication.getDataManager();
-        // Enumerate all devices
-        mDeviceSet.clear();
-        List<android.mtp.MtpDevice> devices = mMtpContext.getMtpClient().getDeviceList();
-        Log.v(TAG, "loadDevices: " + devices + ", size=" + devices.size());
-        for (android.mtp.MtpDevice mtpDevice : devices) {
-            int deviceId = mtpDevice.getDeviceId();
-            Path childPath = mPath.getChild(deviceId);
-            MtpDevice device = (MtpDevice) dataManager.peekMediaObject(childPath);
-            if (device == null) {
-                device = new MtpDevice(childPath, mApplication, deviceId, mMtpContext);
-            }
-            Log.d(TAG, "add device " + device);
-            mDeviceSet.add(device);
-        }
+    private class DevicesLoader implements Job<ArrayList<MediaSet>> {
+        @Override
+        public ArrayList<MediaSet> run(JobContext jc) {
+            DataManager dataManager = mApplication.getDataManager();
+            ArrayList<MediaSet> result = new ArrayList<MediaSet>();
 
-        Collections.sort(mDeviceSet, MediaSetUtils.NAME_COMPARATOR);
-        for (int i = 0, n = mDeviceSet.size(); i < n; i++) {
-            mDeviceSet.get(i).reload();
+            // Enumerate all devices
+            List<android.mtp.MtpDevice> devices = mMtpContext.getMtpClient().getDeviceList();
+            Log.v(TAG, "loadDevices: " + devices + ", size=" + devices.size());
+            for (android.mtp.MtpDevice mtpDevice : devices) {
+                synchronized (DataManager.LOCK) {
+                    int deviceId = mtpDevice.getDeviceId();
+                    Path childPath = mPath.getChild(deviceId);
+                    MtpDevice device = (MtpDevice) dataManager.peekMediaObject(childPath);
+                    if (device == null) {
+                        device = new MtpDevice(childPath, mApplication, deviceId, mMtpContext);
+                    }
+                    Log.d(TAG, "add device " + device);
+                    result.add(device);
+                }
+            }
+            Collections.sort(result, MediaSetUtils.NAME_COMPARATOR);
+            return result;
         }
     }
 
@@ -99,11 +114,40 @@
     }
 
     @Override
-    public long reload() {
+    public synchronized boolean isLoading() {
+        return mIsLoading;
+    }
+
+    @Override
+    public synchronized long reload() {
         if (mNotifier.isDirty()) {
+            if (mLoadTask != null) mLoadTask.cancel();
+            mIsLoading = true;
+            mLoadTask = mApplication.getThreadPool().submit(new DevicesLoader(), this);
+        }
+        if (mLoadBuffer != null) {
+            mDeviceSet = mLoadBuffer;
+            mLoadBuffer = null;
+            for (MediaSet device : mDeviceSet) {
+                device.reload();
+            }
             mDataVersion = nextVersionNumber();
-            loadDevices();
         }
         return mDataVersion;
     }
+
+    @Override
+    public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) {
+        if (future != mLoadTask) return;
+        mLoadBuffer = future.get();
+        mIsLoading = false;
+        if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>();
+
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                notifyContentChanged();
+            }
+        });
+    }
 }
diff --git a/src/com/android/gallery3d/data/MtpImage.java b/src/com/android/gallery3d/data/MtpImage.java
index 211b2f2..96b4d9f 100644
--- a/src/com/android/gallery3d/data/MtpImage.java
+++ b/src/com/android/gallery3d/data/MtpImage.java
@@ -16,12 +16,6 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.provider.GalleryProvider;
-import com.android.gallery3d.util.ThreadPool;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapRegionDecoder;
@@ -30,6 +24,12 @@
 import android.net.Uri;
 import android.util.Log;
 
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.provider.GalleryProvider;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 import java.text.DateFormat;
 import java.util.Date;
 
@@ -84,7 +84,7 @@
                     Log.w(TAG, "decoding thumbnail failed");
                     return null;
                 }
-                return DecodeUtils.requestDecode(jc, thumbnail, null);
+                return DecodeUtils.decode(jc, thumbnail, null);
             }
         };
     }
@@ -95,7 +95,7 @@
             public BitmapRegionDecoder run(JobContext jc) {
                 byte[] bytes = mMtpContext.getMtpClient().getObject(
                         UsbDevice.getDeviceName(mDeviceId), mObjectId, mObjectSize);
-                return DecodeUtils.requestCreateBitmapRegionDecoder(
+                return DecodeUtils.createBitmapRegionDecoder(
                         jc, bytes, 0, bytes.length, false);
             }
         };
diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java
index 3de1c7c..dfb9131 100644
--- a/src/com/android/gallery3d/data/Path.java
+++ b/src/com/android/gallery3d/data/Path.java
@@ -175,13 +175,20 @@
     }
 
     public String getPrefix() {
+        if (this == sRoot) return "";
+        return getPrefixPath().mSegment;
+    }
+
+    public Path getPrefixPath() {
         synchronized (Path.class) {
             Path current = this;
-            if (current == sRoot) return "";
+            if (current == sRoot) {
+                throw new IllegalStateException();
+            }
             while (current.mParent != sRoot) {
                 current = current.mParent;
             }
-            return current.mSegment;
+            return current;
         }
     }
 
@@ -190,15 +197,6 @@
         return mSegment;
     }
 
-    public String getSuffix(int level) {
-        // We don't need lock because mSegment and mParent are final.
-        Path p = this;
-        while (level-- != 0) {
-            p = p.mParent;
-        }
-        return p.mSegment;
-    }
-
     // Below are for testing/debugging only
     static void clearAll() {
         synchronized (Path.class) {
diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java
index 7e24b33..2dbe49d 100644
--- a/src/com/android/gallery3d/data/SizeClustering.java
+++ b/src/com/android/gallery3d/data/SizeClustering.java
@@ -16,11 +16,11 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-
 import android.content.Context;
 import android.content.res.Resources;
 
+import com.android.gallery3d.R;
+
 import java.util.ArrayList;
 
 public class SizeClustering extends Clustering {
diff --git a/src/com/android/gallery3d/data/SnailAlbum.java b/src/com/android/gallery3d/data/SnailAlbum.java
new file mode 100644
index 0000000..cd4f803
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailAlbum.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// This is a simple MediaSet which contains only one MediaItem -- a SnailItem.
+public class SnailAlbum extends MediaSet {
+    private static final String TAG = "SnailAlbum";
+    private SnailItem mItem;
+    private AtomicBoolean mDirty = new AtomicBoolean(false);
+
+    public SnailAlbum(Path path, MediaItem item) {
+        super(path, nextVersionNumber());
+        mItem = (SnailItem) item;
+    }
+
+    @Override
+    public int getMediaItemCount() {
+        return 1;
+    }
+
+    @Override
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+
+        // If [start, start+count) contains the index 0, return the item.
+        if (start <= 0 && start + count > 0) {
+            result.add(mItem);
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean isLeafAlbum() {
+        return true;
+    }
+
+    @Override
+    public String getName() {
+        return "SnailAlbum";
+    }
+
+    @Override
+    public long reload() {
+        if (mDirty.compareAndSet(true, false)) {
+            mItem.updateVersion();
+            mDataVersion = nextVersionNumber();
+        }
+        return mDataVersion;
+    }
+
+    public void notifyChange() {
+        mDirty.set(true);
+        notifyContentChanged();
+    }
+}
diff --git a/src/com/android/gallery3d/data/SnailItem.java b/src/com/android/gallery3d/data/SnailItem.java
new file mode 100644
index 0000000..fdb1b0d
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailItem.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+// SnailItem is a MediaItem which can provide a ScreenNail. This is
+// used so we can show an foreign component (like an
+// android.view.View) instead of a Bitmap.
+public class SnailItem extends MediaItem {
+    private static final String TAG = "SnailItem";
+    private ScreenNail mScreenNail;
+
+    public SnailItem(Path path) {
+        super(path, nextVersionNumber());
+    }
+
+    @Override
+    public Job<Bitmap> requestImage(int type) {
+        // nothing to return
+        return new Job<Bitmap>() {
+            public Bitmap run(JobContext jc) {
+                return null;
+            }
+        };
+    }
+
+    @Override
+    public Job<BitmapRegionDecoder> requestLargeImage() {
+        // nothing to return
+        return new Job<BitmapRegionDecoder>() {
+            public BitmapRegionDecoder run(JobContext jc) {
+                return null;
+            }
+        };
+    }
+
+    // We do not provide requestImage or requestLargeImage, instead we
+    // provide a ScreenNail.
+    @Override
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
+    }
+
+    @Override
+    public String getMimeType() {
+        return "";
+    }
+
+    // Returns width and height of the media item.
+    // Returns 0, 0 if the information is not available.
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    //  Extra methods for SnailItem
+    //////////////////////////////////////////////////////////////////////////
+
+    public void setScreenNail(ScreenNail screenNail) {
+        mScreenNail = screenNail;
+    }
+
+    public void updateVersion() {
+        mDataVersion = nextVersionNumber();
+    }
+}
diff --git a/src/com/android/gallery3d/data/SnailSource.java b/src/com/android/gallery3d/data/SnailSource.java
new file mode 100644
index 0000000..5f663d2
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailSource.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.data;
+
+import com.android.gallery3d.app.GalleryApp;
+
+public class SnailSource extends MediaSource {
+    private static final String TAG = "SnailSource";
+    private static final int SNAIL_ALBUM = 0;
+    private static final int SNAIL_ITEM = 1;
+
+    private GalleryApp mApplication;
+    private PathMatcher mMatcher;
+    private static int sNextId;
+
+    public SnailSource(GalleryApp application) {
+        super("snail");
+        mApplication = application;
+        mMatcher = new PathMatcher();
+        mMatcher.add("/snail/set/*", SNAIL_ALBUM);
+        mMatcher.add("/snail/item/*", SNAIL_ITEM);
+    }
+
+    // The only path we accept is "/snail/set/id" and "/snail/item/id"
+    @Override
+    public MediaObject createMediaObject(Path path) {
+        DataManager dataManager = mApplication.getDataManager();
+        switch (mMatcher.match(path)) {
+            case SNAIL_ALBUM:
+                String itemPath = "/snail/item/" + mMatcher.getVar(0);
+                MediaItem item =
+                        (MediaItem) dataManager.getMediaObject(itemPath);
+                return new SnailAlbum(path, item);
+            case SNAIL_ITEM: {
+                int id = mMatcher.getIntVar(0);
+                return new SnailItem(path);
+            }
+        }
+        return null;
+    }
+
+    // Registers a new SnailAlbum containing a SnailItem and returns the id of
+    // them. You can obtain the Path of the SnailAlbum and SnailItem associated
+    // with the id by getSetPath and getItemPath().
+    public static synchronized int newId() {
+        return sNextId++;
+    }
+
+    public static Path getSetPath(int id) {
+        return Path.fromString("/snail/set").getChild(id);
+    }
+
+    public static Path getItemPath(int id) {
+        return Path.fromString("/snail/item").getChild(id);
+    }
+}
diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java
index c873051..849f885 100644
--- a/src/com/android/gallery3d/data/TagClustering.java
+++ b/src/com/android/gallery3d/data/TagClustering.java
@@ -16,10 +16,10 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.R;
-
 import android.content.Context;
 
+import com.android.gallery3d.R;
+
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.TreeMap;
diff --git a/src/com/android/gallery3d/data/TimeClustering.java b/src/com/android/gallery3d/data/TimeClustering.java
index 1ccf14c..9a0ae1d 100644
--- a/src/com/android/gallery3d/data/TimeClustering.java
+++ b/src/com/android/gallery3d/data/TimeClustering.java
@@ -16,13 +16,13 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.Context;
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
 
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
index 8f91cc0..b5bfe96 100644
--- a/src/com/android/gallery3d/data/UriImage.java
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -16,13 +16,6 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.ThreadPool.CancelListener;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.content.ContentResolver;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
@@ -30,7 +23,13 @@
 import android.graphics.BitmapRegionDecoder;
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
-import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
 
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -58,22 +57,11 @@
 
     private GalleryApp mApplication;
 
-    public UriImage(GalleryApp application, Path path, Uri uri) {
+    public UriImage(GalleryApp application, Path path, Uri uri, String contentType) {
         super(path, nextVersionNumber());
         mUri = uri;
         mApplication = Utils.checkNotNull(application);
-        mContentType = getMimeType(uri);
-    }
-
-    private String getMimeType(Uri uri) {
-        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
-            String extension =
-                    MimeTypeMap.getFileExtensionFromUrl(uri.toString());
-            String type = MimeTypeMap.getSingleton()
-                    .getMimeTypeFromExtension(extension.toLowerCase());
-            if (type != null) return type;
-        }
-        return mApplication.getContentResolver().getType(uri);
+        mContentType = contentType;
     }
 
     @Override
@@ -180,7 +168,7 @@
     private class RegionDecoderJob implements Job<BitmapRegionDecoder> {
         public BitmapRegionDecoder run(JobContext jc) {
             if (!prepareInputFile(jc)) return null;
-            BitmapRegionDecoder decoder = DecodeUtils.requestCreateBitmapRegionDecoder(
+            BitmapRegionDecoder decoder = DecodeUtils.createBitmapRegionDecoder(
                     jc, mFileDescriptor.getFileDescriptor(), false);
             mWidth = decoder.getWidth();
             mHeight = decoder.getHeight();
@@ -195,25 +183,24 @@
             mType = type;
         }
 
+        @Override
         public Bitmap run(JobContext jc) {
             if (!prepareInputFile(jc)) return null;
-            int targetSize = LocalImage.getTargetSize(mType);
+            int targetSize = MediaItem.getTargetSize(mType);
             Options options = new Options();
             options.inPreferredConfig = Config.ARGB_8888;
-            Bitmap bitmap = DecodeUtils.requestDecode(jc,
-                    mFileDescriptor.getFileDescriptor(), options, targetSize);
+            Bitmap bitmap = DecodeUtils.decodeThumbnail(jc,
+                    mFileDescriptor.getFileDescriptor(), options, targetSize, mType);
+
             if (jc.isCancelled() || bitmap == null) {
                 return null;
             }
 
             if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
-                bitmap = BitmapUtils.resizeDownAndCropCenter(bitmap,
-                        targetSize, true);
+                bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
             } else {
-                bitmap = BitmapUtils.resizeDownBySideLength(bitmap,
-                        targetSize, true);
+                bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
             }
-
             return bitmap;
         }
     }
diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java
index ac62b93..d37c51d 100644
--- a/src/com/android/gallery3d/data/UriSource.java
+++ b/src/com/android/gallery3d/data/UriSource.java
@@ -16,9 +16,11 @@
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.GalleryApp;
-
+import android.content.ContentResolver;
 import android.net.Uri;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.app.GalleryApp;
 
 import java.net.URLDecoder;
 import java.net.URLEncoder;
@@ -26,6 +28,8 @@
 class UriSource extends MediaSource {
     @SuppressWarnings("unused")
     private static final String TAG = "UriSource";
+    private static final String IMAGE_TYPE_PREFIX = "image/";
+    private static final String IMAGE_TYPE_ANY = "image/*";
 
     private GalleryApp mApplication;
 
@@ -37,22 +41,44 @@
     @Override
     public MediaObject createMediaObject(Path path) {
         String segment[] = path.split();
-        if (segment.length != 2) {
+        if (segment.length != 3) {
             throw new RuntimeException("bad path: " + path);
         }
+        String uri = URLDecoder.decode(segment[1]);
+        String type = URLDecoder.decode(segment[2]);
+        return new UriImage(mApplication, path, Uri.parse(uri), type);
+    }
 
-        String decoded = URLDecoder.decode(segment[1]);
-        return new UriImage(mApplication, path, Uri.parse(decoded));
+    private String getMimeType(Uri uri) {
+        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+            String extension =
+                    MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+            String type = MimeTypeMap.getSingleton()
+                    .getMimeTypeFromExtension(extension.toLowerCase());
+            if (type != null) return type;
+        }
+        // Assume the type is image if the type cannot be resolved
+        // This could happen for "http" URI.
+        String type = mApplication.getContentResolver().getType(uri);
+        if (type == null) type = "image/*";
+        return type;
     }
 
     @Override
-    public Path findPathByUri(Uri uri) {
-        String type = mApplication.getContentResolver().getType(uri);
-        // Assume the type is image if the type cannot be resolved
-        // This could happen for "http" URI.
-        if (type == null || type.startsWith("image/")) {
-            return Path.fromString("/uri/" + URLEncoder.encode(uri.toString()));
+    public Path findPathByUri(Uri uri, String type) {
+        String mimeType = getMimeType(uri);
+
+        // Try to find a most specific type but it has to be started with "image/"
+        if ((type == null) || (IMAGE_TYPE_ANY.equals(type)
+                && mimeType.startsWith(IMAGE_TYPE_PREFIX))) {
+            type = mimeType;
         }
+
+        if (type.startsWith(IMAGE_TYPE_PREFIX)) {
+            return Path.fromString("/uri/" + URLEncoder.encode(uri.toString())
+                    + "/" +URLEncoder.encode(type));
+        }
+        // We have no clues that it is an image
         return null;
     }
 }
diff --git a/src/com/android/gallery3d/gadget/LocalPhotoSource.java b/src/com/android/gallery3d/gadget/LocalPhotoSource.java
index ad77de5..6ec41f6 100644
--- a/src/com/android/gallery3d/gadget/LocalPhotoSource.java
+++ b/src/com/android/gallery3d/gadget/LocalPhotoSource.java
@@ -16,14 +16,6 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.ContentListener;
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -34,6 +26,14 @@
 import android.os.Handler;
 import android.provider.MediaStore.Images.Media;
 
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.GalleryUtils;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
diff --git a/src/com/android/gallery3d/gadget/MediaSetSource.java b/src/com/android/gallery3d/gadget/MediaSetSource.java
index c1687e0..786092b 100644
--- a/src/com/android/gallery3d/gadget/MediaSetSource.java
+++ b/src/com/android/gallery3d/gadget/MediaSetSource.java
@@ -16,16 +16,16 @@
 
 package com.android.gallery3d.gadget;
 
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.ContentListener;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
 
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Binder;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 
diff --git a/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
index 814ede2..c18652d 100644
--- a/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
+++ b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
@@ -16,9 +16,6 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry;
-
 import android.app.PendingIntent;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProvider;
@@ -30,6 +27,9 @@
 import android.util.Log;
 import android.widget.RemoteViews;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry;
+
 public class PhotoAppWidgetProvider extends AppWidgetProvider {
 
     private static final String TAG = "WidgetProvider";
diff --git a/src/com/android/gallery3d/gadget/WidgetClickHandler.java b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
index 075644d..35f9bec 100644
--- a/src/com/android/gallery3d/gadget/WidgetClickHandler.java
+++ b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
@@ -16,9 +16,6 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.Gallery;
-
 import android.app.Activity;
 import android.content.Intent;
 import android.content.res.AssetFileDescriptor;
@@ -27,6 +24,10 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.PhotoPage;
+
 public class WidgetClickHandler extends Activity {
     private static final String TAG = "PhotoAppWidgetClickHandler";
 
@@ -46,14 +47,19 @@
     @Override
     protected void onCreate(Bundle savedState) {
         super.onCreate(savedState);
-        Intent intent = getIntent();
-        if (isValidDataUri(intent.getData())) {
-            startActivity(new Intent(Intent.ACTION_VIEW, intent.getData()));
+        Uri uri = getIntent().getData();
+        Intent intent;
+        if (isValidDataUri(uri)) {
+            intent = new Intent(Intent.ACTION_VIEW, uri);
+            intent.putExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
         } else {
             Toast.makeText(this,
                     R.string.no_such_item, Toast.LENGTH_LONG).show();
-            startActivity(new Intent(this, Gallery.class));
+            intent = new Intent(this, Gallery.class);
         }
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
+                Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+        startActivity(intent);
         finish();
     }
 }
diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java
index a871e24..5717657 100644
--- a/src/com/android/gallery3d/gadget/WidgetConfigure.java
+++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java
@@ -16,11 +16,6 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.AlbumPicker;
-import com.android.gallery3d.app.CropImage;
-import com.android.gallery3d.app.DialogPicker;
-
 import android.app.Activity;
 import android.appwidget.AppWidgetManager;
 import android.content.Intent;
@@ -30,6 +25,11 @@
 import android.os.Bundle;
 import android.widget.RemoteViews;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AlbumPicker;
+import com.android.gallery3d.app.CropImage;
+import com.android.gallery3d.app.DialogPicker;
+
 public class WidgetConfigure extends Activity {
     @SuppressWarnings("unused")
     private static final String TAG = "WidgetConfigure";
diff --git a/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java
index 1648784..b8ef7a7 100644
--- a/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java
+++ b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java
@@ -16,8 +16,6 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.common.Utils;
-
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
@@ -28,6 +26,8 @@
 import android.net.Uri;
 import android.util.Log;
 
+import com.android.gallery3d.common.Utils;
+
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 
diff --git a/src/com/android/gallery3d/gadget/WidgetService.java b/src/com/android/gallery3d/gadget/WidgetService.java
index a61831c..eba7403 100644
--- a/src/com/android/gallery3d/gadget/WidgetService.java
+++ b/src/com/android/gallery3d/gadget/WidgetService.java
@@ -16,13 +16,6 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.data.ContentListener;
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.data.Path;
-
 import android.appwidget.AppWidgetManager;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -30,6 +23,13 @@
 import android.widget.RemoteViews;
 import android.widget.RemoteViewsService;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
 public class WidgetService extends RemoteViewsService {
 
     @SuppressWarnings("unused")
diff --git a/src/com/android/gallery3d/gadget/WidgetSource.java b/src/com/android/gallery3d/gadget/WidgetSource.java
index 8b8eb79..92874c7 100644
--- a/src/com/android/gallery3d/gadget/WidgetSource.java
+++ b/src/com/android/gallery3d/gadget/WidgetSource.java
@@ -16,11 +16,11 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.data.ContentListener;
-
 import android.graphics.Bitmap;
 import android.net.Uri;
 
+import com.android.gallery3d.data.ContentListener;
+
 public interface WidgetSource {
     public int size();
     public Bitmap getImage(int index);
diff --git a/src/com/android/gallery3d/gadget/WidgetTypeChooser.java b/src/com/android/gallery3d/gadget/WidgetTypeChooser.java
index c4bca60..1694f1c 100644
--- a/src/com/android/gallery3d/gadget/WidgetTypeChooser.java
+++ b/src/com/android/gallery3d/gadget/WidgetTypeChooser.java
@@ -16,8 +16,6 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.R;
-
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
@@ -27,6 +25,8 @@
 import android.widget.RadioGroup;
 import android.widget.RadioGroup.OnCheckedChangeListener;
 
+import com.android.gallery3d.R;
+
 public class WidgetTypeChooser extends Activity {
 
     private OnCheckedChangeListener mListener = new OnCheckedChangeListener() {
diff --git a/src/com/android/gallery3d/gadget/WidgetUtils.java b/src/com/android/gallery3d/gadget/WidgetUtils.java
index b194c7d..c20c186 100644
--- a/src/com/android/gallery3d/gadget/WidgetUtils.java
+++ b/src/com/android/gallery3d/gadget/WidgetUtils.java
@@ -16,18 +16,18 @@
 
 package com.android.gallery3d.gadget;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.util.ThreadPool;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
 import android.graphics.Canvas;
 import android.graphics.Paint;
-import android.graphics.Bitmap.Config;
 import android.util.Log;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool;
+
 public class WidgetUtils {
 
     private static final String TAG = "WidgetUtils";
diff --git a/src/com/android/gallery3d/photoeditor/ActionBar.java b/src/com/android/gallery3d/photoeditor/ActionBar.java
index 814ee8e..94379f7 100644
--- a/src/com/android/gallery3d/photoeditor/ActionBar.java
+++ b/src/com/android/gallery3d/photoeditor/ActionBar.java
@@ -92,10 +92,6 @@
         findViewById(R.id.action_bar_back).performClick();
     }
 
-    public void clickSave() {
-        findViewById(R.id.save_button).performClick();
-    }
-
     public boolean canSave() {
         return findViewById(R.id.save_button).isEnabled();
     }
diff --git a/src/com/android/gallery3d/photoeditor/BitmapUtils.java b/src/com/android/gallery3d/photoeditor/BitmapUtils.java
index a9fa758..9b41143 100644
--- a/src/com/android/gallery3d/photoeditor/BitmapUtils.java
+++ b/src/com/android/gallery3d/photoeditor/BitmapUtils.java
@@ -21,11 +21,8 @@
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.CompressFormat;
 import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
 import android.graphics.Matrix;
-import android.graphics.Paint;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.net.Uri;
 import android.provider.MediaStore.Images.ImageColumns;
 import android.util.Log;
@@ -57,40 +54,8 @@
         this.context = context;
     }
 
-    /**
-     * Creates a mutable bitmap from subset of source bitmap, transformed by the optional matrix.
-     */
-    private static Bitmap createBitmap(
-            Bitmap source, int x, int y, int width, int height, Matrix m) {
-        // Re-implement Bitmap createBitmap() to always return a mutable bitmap.
-        Canvas canvas = new Canvas();
-
-        Bitmap bitmap;
-        Paint paint;
-        if ((m == null) || m.isIdentity()) {
-            bitmap = Bitmap.createBitmap(width, height, source.getConfig());
-            paint = null;
-        } else {
-            RectF rect = new RectF(0, 0, width, height);
-            m.mapRect(rect);
-            bitmap = Bitmap.createBitmap(
-                    Math.round(rect.width()), Math.round(rect.height()), source.getConfig());
-
-            canvas.translate(-rect.left, -rect.top);
-            canvas.concat(m);
-
-            paint = new Paint(Paint.FILTER_BITMAP_FLAG);
-            if (!m.rectStaysRect()) {
-                paint.setAntiAlias(true);
-            }
-        }
-        bitmap.setDensity(source.getDensity());
-        canvas.setBitmap(bitmap);
-
-        Rect srcBounds = new Rect(x, y, x + width, y + height);
-        RectF dstBounds = new RectF(0, 0, width, height);
-        canvas.drawBitmap(source, srcBounds, dstBounds, paint);
-        return bitmap;
+    private static Bitmap createBitmap(Bitmap source, Matrix m) {
+        return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), m, true);
     }
 
     private void closeStream(Closeable stream) {
@@ -143,7 +108,7 @@
     }
 
     /**
-     * Decodes bitmap (maybe immutable) that keeps aspect-ratio and spans most within the bounds.
+     * Decodes bitmap that keeps aspect-ratio and spans most within the bounds.
      */
     private Bitmap decodeBitmap(Uri uri, int width, int height) {
         InputStream is = null;
@@ -184,8 +149,7 @@
             if (scale < 1) {
                 Matrix m = new Matrix();
                 m.setScale(scale, scale);
-                Bitmap transformed = createBitmap(
-                        bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m);
+                Bitmap transformed = createBitmap(bitmap, m);
                 bitmap.recycle();
                 return transformed;
             }
@@ -194,7 +158,7 @@
     }
 
     /**
-     * Gets decoded bitmap that keeps orientation as well.
+     * Gets decoded bitmap (maybe immutable) that keeps orientation as well.
      */
     public Bitmap getBitmap(Uri uri, int width, int height) {
         Bitmap bitmap = decodeBitmap(uri, width, height);
@@ -205,8 +169,7 @@
             if (orientation != 0) {
                 Matrix m = new Matrix();
                 m.setRotate(orientation);
-                Bitmap transformed = createBitmap(
-                        bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m);
+                Bitmap transformed = createBitmap(bitmap, m);
                 bitmap.recycle();
                 return transformed;
             }
@@ -218,15 +181,13 @@
      * Saves the bitmap by given directory, filename, and format; if the directory is given null,
      * then saves it under the cache directory.
      */
-    public File saveBitmap(
-            Bitmap bitmap, String directory, String filename, CompressFormat format) {
+    public File saveBitmap(Bitmap bitmap, File directory, String filename, CompressFormat format) {
 
         if (directory == null) {
-            directory = context.getCacheDir().getAbsolutePath();
+            directory = context.getCacheDir();
         } else {
             // Check if the given directory exists or try to create it.
-            File file = new File(directory);
-            if (!file.isDirectory() && !file.mkdirs()) {
+            if (!directory.isDirectory() && !directory.mkdirs()) {
                 return null;
             }
         }
diff --git a/src/com/android/gallery3d/photoeditor/EffectsBar.java b/src/com/android/gallery3d/photoeditor/EffectsBar.java
index 4075404..fad0b90 100644
--- a/src/com/android/gallery3d/photoeditor/EffectsBar.java
+++ b/src/com/android/gallery3d/photoeditor/EffectsBar.java
@@ -22,11 +22,9 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.photoeditor.actions.EffectAction;
-import com.android.gallery3d.photoeditor.actions.EffectToolFactory;
 
 /**
  * Effects bar that contains all effects and shows them in categorized views.
@@ -37,7 +35,6 @@
     private FilterStack filterStack;
     private EffectsMenu effectsMenu;
     private View effectsGallery;
-    private ViewGroup effectToolPanel;
     private EffectAction activeEffect;
 
     public EffectsBar(Context context, AttributeSet attrs) {
@@ -75,49 +72,40 @@
         ViewGroup scrollView = (ViewGroup) effectsGallery.findViewById(R.id.scroll_view);
         ViewGroup effects = (ViewGroup) inflater.inflate(effectsId, scrollView, false);
         for (int i = 0; i < effects.getChildCount(); i++) {
-            setupEffectListener((EffectAction) effects.getChildAt(i));
+            setupEffect((EffectAction) effects.getChildAt(i));
         }
         scrollView.addView(effects);
         scrollView.scrollTo(0, 0);
         addView(effectsGallery, 0);
     }
 
-    private void setupEffectListener(final EffectAction effect) {
-        effect.setListener(new EffectAction.Listener() {
+    private void setupEffect(final EffectAction effect) {
+        effect.setOnClickListener(new View.OnClickListener() {
 
             @Override
-            public void onClick() {
+            public void onClick(View v) {
                 if (isEnabled()) {
                     // Set the clicked effect active before exiting effects-gallery.
                     activeEffect = effect;
                     exitEffectsGallery();
-                    // Create effect tool panel first before the factory could create tools within.
-                    createEffectToolPanel();
-                    activeEffect.begin(filterStack,
-                            new EffectToolFactory(effectToolPanel, inflater));
+                    EffectAction.ActionListener listener = new EffectAction.ActionListener() {
+
+                        @Override
+                        public void onOk() {
+                            exit(null);
+                        }
+                    };
+                    activeEffect.begin(getRootView(), filterStack, listener);
                 }
             }
-
-            @Override
-            public void onDone() {
-                exit(null);
-            }
         });
     }
 
-    private void createEffectToolPanel() {
-        effectToolPanel = (ViewGroup) inflater.inflate(
-                R.layout.photoeditor_effect_tool_panel, this, false);
-        ((TextView) effectToolPanel.findViewById(R.id.effect_label)).setText(activeEffect.name());
-        addView(effectToolPanel, 0);
-    }
-
     private boolean exitEffectsGallery() {
         if (effectsGallery != null) {
             if (activeEffect != null) {
                 // Detach the active effect to prevent it stopping effects-gallery from gc.
-                ViewGroup scrollView = (ViewGroup) effectsGallery.findViewById(R.id.scroll_view);
-                ((ViewGroup) scrollView.getChildAt(0)).removeView(activeEffect);
+                ((ViewGroup) activeEffect.getParent()).removeView(activeEffect);
             }
             removeView(effectsGallery);
             effectsGallery = null;
@@ -128,18 +116,13 @@
 
     private boolean exitActiveEffect(final Runnable runnableOnDone) {
         if (activeEffect != null) {
-            SpinnerProgressDialog.showDialog();
+            final Toolbar toolbar = (Toolbar) getRootView().findViewById(R.id.toolbar);
+            toolbar.showSpinner();
             activeEffect.end(new Runnable() {
 
                 @Override
                 public void run() {
-                    SpinnerProgressDialog.dismissDialog();
-                    View fullscreenTool = getRootView().findViewById(R.id.fullscreen_effect_tool);
-                    if (fullscreenTool != null) {
-                        ((ViewGroup) fullscreenTool.getParent()).removeView(fullscreenTool);
-                    }
-                    removeView(effectToolPanel);
-                    effectToolPanel = null;
+                    toolbar.dismissSpinner();
                     activeEffect = null;
                     if (runnableOnDone != null) {
                         runnableOnDone.run();
diff --git a/src/com/android/gallery3d/photoeditor/EffectsMenu.java b/src/com/android/gallery3d/photoeditor/EffectsMenu.java
index 71614b1..6578849 100644
--- a/src/com/android/gallery3d/photoeditor/EffectsMenu.java
+++ b/src/com/android/gallery3d/photoeditor/EffectsMenu.java
@@ -51,13 +51,13 @@
     }
 
     public void setOnToggleListener(OnToggleListener listener) {
-        setToggleRunnalbe(listener, R.id.exposure_button, R.layout.photoeditor_effects_exposure);
-        setToggleRunnalbe(listener, R.id.artistic_button, R.layout.photoeditor_effects_artistic);
-        setToggleRunnalbe(listener, R.id.color_button, R.layout.photoeditor_effects_color);
-        setToggleRunnalbe(listener, R.id.fix_button, R.layout.photoeditor_effects_fix);
+        setToggleRunnable(listener, R.id.exposure_button, R.layout.photoeditor_effects_exposure);
+        setToggleRunnable(listener, R.id.artistic_button, R.layout.photoeditor_effects_artistic);
+        setToggleRunnable(listener, R.id.color_button, R.layout.photoeditor_effects_color);
+        setToggleRunnable(listener, R.id.fix_button, R.layout.photoeditor_effects_fix);
     }
 
-    private void setToggleRunnalbe(final OnToggleListener listener, final int toggleId,
+    private void setToggleRunnable(final OnToggleListener listener, final int toggleId,
             final int effectsId) {
         setClickRunnable(toggleId, new Runnable() {
 
diff --git a/src/com/android/gallery3d/photoeditor/FilterStack.java b/src/com/android/gallery3d/photoeditor/FilterStack.java
index fe6fb10..273c295 100644
--- a/src/com/android/gallery3d/photoeditor/FilterStack.java
+++ b/src/com/android/gallery3d/photoeditor/FilterStack.java
@@ -118,6 +118,7 @@
                 reallocateBuffer(out);
             }
             appliedStack.get(filterIndex).process(input, buffers[out]);
+            nativeEglSetFenceAndWait();
             return buffers[out];
         }
         return null;
@@ -275,4 +276,10 @@
         photoView.onResume();
         paused = false;
     }
+
+    static {
+        System.loadLibrary("jni_eglfence");
+    }
+
+    private native void nativeEglSetFenceAndWait();
 }
diff --git a/src/com/android/gallery3d/photoeditor/ImageActionButton.java b/src/com/android/gallery3d/photoeditor/ImageActionButton.java
new file mode 100644
index 0000000..a919ac6
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/ImageActionButton.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageButton;
+
+/**
+ * Image buttons used in Action-bar and Effects-menu that can be grayed out when set disabled.
+ * (Text buttons are automatically grayed out when disabled; however, image buttons are not.)
+ */
+public class ImageActionButton extends ImageButton {
+
+    private static final float ENABLED_ALPHA = 1;
+    private static final float DISABLED_ALPHA = 0.28f;
+
+    public ImageActionButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        setAlpha(enabled ? ENABLED_ALPHA : DISABLED_ALPHA);
+    }
+}
diff --git a/src/com/android/gallery3d/photoeditor/PhotoEditor.java b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
index 8f3990b..27aaed2 100644
--- a/src/com/android/gallery3d/photoeditor/PhotoEditor.java
+++ b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
@@ -21,7 +21,6 @@
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
-import android.view.ViewGroup;
 
 import com.android.gallery3d.R;
 
@@ -37,18 +36,19 @@
     private FilterStack filterStack;
     private ActionBar actionBar;
     private EffectsBar effectsBar;
+    private Toolbar toolbar;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.photoeditor_main);
-        SpinnerProgressDialog.initialize((ViewGroup) findViewById(R.id.toolbar));
 
         Intent intent = getIntent();
         if (Intent.ACTION_EDIT.equalsIgnoreCase(intent.getAction())) {
             sourceUri = intent.getData();
         }
 
+        toolbar = (Toolbar) findViewById(R.id.toolbar);
         actionBar = (ActionBar) findViewById(R.id.action_bar);
         filterStack = new FilterStack((PhotoView) findViewById(R.id.photo_view),
                 new FilterStack.StackListener() {
@@ -76,7 +76,7 @@
     }
 
     private void openPhoto() {
-        SpinnerProgressDialog.showDialog();
+        toolbar.showSpinner();
         LoadScreennailTask.Callback callback = new LoadScreennailTask.Callback() {
 
             @Override
@@ -85,7 +85,7 @@
 
                     @Override
                     public void onDone() {
-                        SpinnerProgressDialog.dismissDialog();
+                        toolbar.dismissSpinner();
                         effectsBar.setEnabled(result != null);
                     }
                 });
@@ -103,12 +103,12 @@
 
                     @Override
                     public void run() {
-                        SpinnerProgressDialog.showDialog();
+                        toolbar.showSpinner();
                         OnDoneCallback callback = new OnDoneCallback() {
 
                             @Override
                             public void onDone() {
-                                SpinnerProgressDialog.dismissDialog();
+                                toolbar.dismissSpinner();
                             }
                         };
                         if (undo) {
@@ -131,7 +131,7 @@
 
                     @Override
                     public void run() {
-                        SpinnerProgressDialog.showDialog();
+                        toolbar.showSpinner();
                         filterStack.getOutputBitmap(new OnDoneBitmapCallback() {
 
                             @Override
@@ -140,7 +140,7 @@
 
                                     @Override
                                     public void onComplete(Uri result) {
-                                        SpinnerProgressDialog.dismissDialog();
+                                        toolbar.dismissSpinner();
                                         saveUri = result;
                                         actionBar.updateSave(saveUri == null);
                                     }
@@ -183,22 +183,18 @@
             public void run() {
                 // Exit effects or go back to the previous activity on pressing back button.
                 if (!effectsBar.exit(null)) {
-                    // Pop-up a dialog to save unsaved photo.
+                    // Pop-up a dialog if there are unsaved changes.
                     if (actionBar.canSave()) {
-                        new YesNoCancelDialogBuilder(PhotoEditor.this, new Runnable() {
+                        new YesCancelDialogBuilder(PhotoEditor.this, new Runnable() {
 
                             @Override
                             public void run() {
-                                actionBar.clickSave();
-                            }
-                        }, new Runnable() {
-
-                            @Override
-                            public void run() {
+                                // Discard unsaved photo for the result.
                                 finish();
                             }
-                        }, R.string.save_photo).show();
+                        }, R.string.discard_unsaved_photo).show();
                     } else {
+                        setResult(RESULT_OK, new Intent().setData(saveUri));
                         finish();
                     }
                 }
@@ -223,7 +219,7 @@
         super.onPause();
         filterStack.onPause();
         // Dismiss any running progress dialog as all operations are paused.
-        SpinnerProgressDialog.dismissDialog();
+        toolbar.dismissSpinner();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/RendererUtils.java b/src/com/android/gallery3d/photoeditor/RendererUtils.java
index b92907d..3edcff5 100644
--- a/src/com/android/gallery3d/photoeditor/RendererUtils.java
+++ b/src/com/android/gallery3d/photoeditor/RendererUtils.java
@@ -19,6 +19,7 @@
 import android.graphics.Bitmap;
 import android.opengl.GLES20;
 import android.opengl.GLUtils;
+import android.util.FloatMath;
 
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -153,8 +154,8 @@
     public static void setRenderToRotate(RenderContext context, int srcWidth, int srcHeight,
             int dstWidth, int dstHeight, float degrees) {
         float radian = -degrees * DEGREE_TO_RADIAN;
-        float cosTheta = (float) Math.cos(radian);
-        float sinTheta = (float) Math.sin(radian);
+        float cosTheta = FloatMath.cos(radian);
+        float sinTheta = FloatMath.sin(radian);
         float cosWidth = cosTheta * srcWidth;
         float sinWidth = sinTheta * srcWidth;
         float cosHeight = cosTheta * srcHeight;
@@ -205,8 +206,8 @@
         System.arraycopy(base, 0, vertices, 0, vertices.length);
         if (horizontalDegrees % 180f != 0) {
             float radian = (horizontalDegrees - horizontalRounds * 180) * DEGREE_TO_RADIAN;
-            float cosTheta = (float) Math.cos(radian);
-            float sinTheta = (float) Math.sin(radian);
+            float cosTheta = FloatMath.cos(radian);
+            float sinTheta = FloatMath.sin(radian);
 
             float scale = length / (length + sinTheta * base[0]);
             vertices[0] = cosTheta * base[0] * scale;
@@ -223,8 +224,8 @@
 
         if (verticalDegrees % 180f != 0) {
             float radian = (verticalDegrees - verticalRounds * 180) * DEGREE_TO_RADIAN;
-            float cosTheta = (float) Math.cos(radian);
-            float sinTheta = (float) Math.sin(radian);
+            float cosTheta = FloatMath.cos(radian);
+            float sinTheta = FloatMath.sin(radian);
 
             float scale = length / (length + sinTheta * base[1]);
             vertices[0] = base[0] * scale;
diff --git a/src/com/android/gallery3d/photoeditor/RestorableView.java b/src/com/android/gallery3d/photoeditor/RestorableView.java
index fc98741..705b412 100644
--- a/src/com/android/gallery3d/photoeditor/RestorableView.java
+++ b/src/com/android/gallery3d/photoeditor/RestorableView.java
@@ -32,9 +32,6 @@
  */
 public abstract class RestorableView extends FrameLayout {
 
-    private static final float ENABLED_ALPHA = 1;
-    private static final float DISABLED_ALPHA = 0.47f;
-
     private final HashMap<Integer, Runnable> clickRunnables = new HashMap<Integer, Runnable>();
     private final HashSet<Integer> changedViews = new HashSet<Integer>();
     private final LayoutInflater inflater;
@@ -92,9 +89,7 @@
     }
 
     public void setViewEnabled(int id, boolean enabled) {
-        View view = findViewById(id);
-        view.setEnabled(enabled);
-        view.setAlpha(enabled ? ENABLED_ALPHA : DISABLED_ALPHA);
+        findViewById(id).setEnabled(enabled);
         // Track views whose enabled status has been updated.
         changedViews.add(id);
     }
diff --git a/src/com/android/gallery3d/photoeditor/SaveCopyTask.java b/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
index b7d5626..1b59292 100644
--- a/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
+++ b/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
@@ -30,6 +30,7 @@
 import android.widget.Toast;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.util.BucketNames;
 
 import java.io.File;
 import java.sql.Date;
@@ -48,29 +49,24 @@
         void onComplete(Uri result);
     }
 
-    private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
-    private static final int INDEX_DATE_TAKEN = 0;
-    private static final int INDEX_LATITUDE = 1;
-    private static final int INDEX_LONGITUDE = 2;
+    private interface ContentResolverQueryCallback {
 
-    private static final String[] IMAGE_PROJECTION = new String[] {
-        ImageColumns.DATE_TAKEN,
-        ImageColumns.LATITUDE,
-        ImageColumns.LONGITUDE,
-    };
+        void onCursorResult(Cursor cursor);
+    }
+
+    private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
 
     private final Context context;
     private final Uri sourceUri;
     private final Callback callback;
-    private final String albumName;
     private final String saveFileName;
+    private String saveFolderName;
 
     public SaveCopyTask(Context context, Uri sourceUri, Callback callback) {
         this.context = context;
         this.sourceUri = sourceUri;
         this.callback = callback;
 
-        albumName = context.getString(R.string.edited_photo_bucket_name);
         saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(
                 new Date(System.currentTimeMillis()));
     }
@@ -84,8 +80,19 @@
         if (params[0] == null) {
             return null;
         }
+        // Use the default save directory if the source directory cannot be saved.
+        File saveDirectory = getSaveDirectory();
+        if ((saveDirectory == null) || !saveDirectory.canWrite()) {
+            saveDirectory = new File(Environment.getExternalStorageDirectory(),
+                    BucketNames.DOWNLOAD);
+            saveFolderName = context.getString(R.string.folder_download);
+        } else {
+            saveFolderName = saveDirectory.getName();
+        }
+
         Bitmap bitmap = params[0];
-        File file = save(bitmap);
+        File file = new BitmapUtils(context).saveBitmap(
+                bitmap, saveDirectory, saveFileName, Bitmap.CompressFormat.JPEG);
         Uri uri = (file != null) ? insertContent(file) : null;
         bitmap.recycle();
         return uri;
@@ -94,7 +101,7 @@
     @Override
     protected void onPostExecute(Uri result) {
         String message = (result == null) ? context.getString(R.string.saving_failure)
-                : context.getString(R.string.photo_saved, albumName);
+                : context.getString(R.string.photo_saved, saveFolderName);
         Toast toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
         toast.setGravity(Gravity.CENTER, 0, 0);
         toast.show();
@@ -102,10 +109,33 @@
         callback.onComplete(result);
     }
 
-    private File save(Bitmap bitmap) {
-        String directory = Environment.getExternalStorageDirectory().toString() + "/" + albumName;
-        return new BitmapUtils(context).saveBitmap(
-                bitmap, directory, saveFileName, Bitmap.CompressFormat.JPEG);
+    private void querySource(String[] projection, ContentResolverQueryCallback callback) {
+        ContentResolver contentResolver = context.getContentResolver();
+        Cursor cursor = null;
+        try {
+            cursor = contentResolver.query(sourceUri, projection, null, null, null);
+            if ((cursor != null) && cursor.moveToNext()) {
+                callback.onCursorResult(cursor);
+            }
+        } catch (Exception e) {
+            // Ignore error for lacking the data column from the source.
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    private File getSaveDirectory() {
+        final File[] dir = new File[1];
+        querySource(new String[] { ImageColumns.DATA }, new ContentResolverQueryCallback () {
+
+            @Override
+            public void onCursorResult(Cursor cursor) {
+                dir[0] = new File(cursor.getString(0)).getParentFile();
+            }
+        });
+        return dir[0];
     }
 
     /**
@@ -113,43 +143,39 @@
      */
     private Uri insertContent(File file) {
         long now = System.currentTimeMillis() / 1000;
-        long dateTaken = now;
-        double latitude = 0f;
-        double longitude = 0f;
 
-        ContentResolver contentResolver = context.getContentResolver();
-        Cursor cursor = null;
-        try {
-            cursor = contentResolver.query(sourceUri, IMAGE_PROJECTION, null, null, null);
-            if ((cursor != null) && cursor.moveToNext()) {
-                dateTaken = cursor.getLong(INDEX_DATE_TAKEN);
-                latitude = cursor.getDouble(INDEX_LATITUDE);
-                longitude = cursor.getDouble(INDEX_LONGITUDE);
-            }
-        } catch (Exception e) {
-            // Ignore error for lacking property columns from the source.
-        } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
-        }
-
-        ContentValues values = new ContentValues();
+        final ContentValues values = new ContentValues();
         values.put(Images.Media.TITLE, saveFileName);
         values.put(Images.Media.DISPLAY_NAME, file.getName());
         values.put(Images.Media.MIME_TYPE, "image/jpeg");
-        values.put(Images.Media.DATE_TAKEN, dateTaken);
+        values.put(Images.Media.DATE_TAKEN, now);
         values.put(Images.Media.DATE_MODIFIED, now);
         values.put(Images.Media.DATE_ADDED, now);
         values.put(Images.Media.ORIENTATION, 0);
         values.put(Images.Media.DATA, file.getAbsolutePath());
         values.put(Images.Media.SIZE, file.length());
 
-        // TODO: Change || to && after the default location issue is fixed.
-        if ((latitude != 0f) || (longitude != 0f)) {
-            values.put(Images.Media.LATITUDE, latitude);
-            values.put(Images.Media.LONGITUDE, longitude);
-        }
-        return contentResolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
+        String[] projection = new String[] {
+            ImageColumns.DATE_TAKEN,
+            ImageColumns.LATITUDE,
+            ImageColumns.LONGITUDE,
+        };
+        querySource(projection, new ContentResolverQueryCallback() {
+
+            @Override
+            public void onCursorResult(Cursor cursor) {
+                values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
+
+                double latitude = cursor.getDouble(1);
+                double longitude = cursor.getDouble(2);
+                // TODO: Change || to && after the default location issue is fixed.
+                if ((latitude != 0f) || (longitude != 0f)) {
+                    values.put(Images.Media.LATITUDE, latitude);
+                    values.put(Images.Media.LONGITUDE, longitude);
+                }
+            }
+        });
+
+        return context.getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
index 065075e..5bc544f 100644
--- a/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
+++ b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
@@ -17,69 +17,69 @@
 package com.android.gallery3d.photoeditor;
 
 import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.ProgressBar;
 
 import com.android.gallery3d.R;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Spinner model progress dialog that disables all tools for user interaction after it shows up and
- * and re-enables them after it dismisses; this class along with all its methods should be accessed
- * in only UI thread and allows only one instance at a time.
+ * and re-enables them after it dismisses.
  */
 public class SpinnerProgressDialog extends Dialog {
 
-    private static ViewGroup toolbar;
-    private static SpinnerProgressDialog dialog;
-    private final ArrayList<View> enabledTools = new ArrayList<View>();
+    /**
+     * Listener of touch events.
+     */
+    public interface OnTouchListener {
 
-    public static void initialize(ViewGroup toolbar) {
-        SpinnerProgressDialog.toolbar = toolbar;
+        public boolean onTouch(DialogInterface dialog, MotionEvent event);
     }
 
-    public static void showDialog() {
-        // There should be only one progress dialog running at a time.
-        if (dialog == null) {
-            dialog = new SpinnerProgressDialog();
-            dialog.setCancelable(false);
-            dialog.show();
-            // Disable enabled tools when showing spinner progress dialog.
-            for (int i = 0; i < toolbar.getChildCount(); i++) {
-                View view = toolbar.getChildAt(i);
-                if (view.isEnabled()) {
-                    dialog.enabledTools.add(view);
-                    view.setEnabled(false);
-                }
-            }
-        }
-    }
+    private final List<View> enabledTools = new ArrayList<View>();
+    private final OnTouchListener listener;
 
-    public static void dismissDialog() {
-        if (dialog != null) {
-            dialog.dismiss();
-            // Enable tools that were disabled by this spinner progress dialog.
-            for (View view : dialog.enabledTools) {
-                view.setEnabled(true);
-            }
-            dialog = null;
-        }
-    }
-
-    private SpinnerProgressDialog() {
-        super(toolbar.getContext(), R.style.SpinnerProgressDialog);
-        addContentView(new ProgressBar(toolbar.getContext()), new LayoutParams(
+    public SpinnerProgressDialog(Context context, List<View> tools, OnTouchListener listener) {
+        super(context, R.style.SpinnerProgressDialog);
+        addContentView(new ProgressBar(context), new LayoutParams(
                 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+        setCancelable(false);
+
+        for (View view : tools) {
+            if (view.isEnabled()) {
+                enabledTools.add(view);
+            }
+        }
+        this.listener = listener;
+    }
+
+    @Override
+    public void show() {
+        super.show();
+        // Disable enabled tools when showing spinner progress dialog.
+        for (View view : enabledTools) {
+            view.setEnabled(false);
+        }
+    }
+
+    @Override
+    public void dismiss() {
+        super.dismiss();
+        // Enable tools that were disabled by this spinner progress dialog.
+        for (View view : enabledTools) {
+            view.setEnabled(true);
+        }
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        super.onTouchEvent(event);
-        // Pass touch events to tools for killing idle even when the progress dialog is shown.
-        return toolbar.dispatchTouchEvent(event);
+        return listener.onTouch(this, event);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/Toolbar.java b/src/com/android/gallery3d/photoeditor/Toolbar.java
index 45ec016..df68025 100644
--- a/src/com/android/gallery3d/photoeditor/Toolbar.java
+++ b/src/com/android/gallery3d/photoeditor/Toolbar.java
@@ -17,6 +17,7 @@
 package com.android.gallery3d.photoeditor;
 
 import android.content.Context;
+import android.content.DialogInterface;
 import android.os.Handler;
 import android.os.Message;
 import android.util.AttributeSet;
@@ -37,33 +38,75 @@
 public class Toolbar extends RelativeLayout {
 
     private final ToolbarIdleHandler idleHandler;
+    private final List<View> tools = new ArrayList<View>();
+    private SpinnerProgressDialog spinner;
 
     public Toolbar(Context context, AttributeSet attrs) {
         super(context, attrs);
 
-        idleHandler = new ToolbarIdleHandler(context);
-        setOnHierarchyChangeListener(idleHandler);
+        setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
+
+            @Override
+            public void onChildViewAdded(View parent, View child) {
+                // Photo-view isn't treated as a tool that responds to user events.
+                if (child.getId() != R.id.photo_view) {
+                    tools.add(child);
+                }
+            }
+
+            @Override
+            public void onChildViewRemoved(View parent, View child) {
+                tools.remove(child);
+            }
+        });
+
+        idleHandler = new ToolbarIdleHandler(context, tools);
         idleHandler.killIdle();
     }
 
+    public void showSpinner() {
+        // There should be only one progress spinner running at a time.
+        if (spinner == null) {
+            spinner = new SpinnerProgressDialog(getContext(), tools,
+                    new SpinnerProgressDialog.OnTouchListener() {
+
+                @Override
+                public boolean onTouch(DialogInterface dialog, MotionEvent event) {
+                    // Kill idle even when the progress dialog is shown.
+                    idleHandler.killIdle();
+                    return true;
+                }
+            });
+            spinner.show();
+        }
+    }
+
+    public void dismissSpinner() {
+        if (spinner != null) {
+            spinner.dismiss();
+            spinner = null;
+        }
+    }
+
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         idleHandler.killIdle();
         return super.dispatchTouchEvent(ev);
     }
 
-    private static class ToolbarIdleHandler implements OnHierarchyChangeListener {
+    private static class ToolbarIdleHandler {
 
         private static final int MAKE_IDLE = 1;
         private static final int TIMEOUT_IDLE = 8000;
 
-        private final List<View> childViews = new ArrayList<View>();
+        private final List<View> tools;
         private final Handler mainHandler;
         private final Animation fadeIn;
         private final Animation fadeOut;
         private boolean idle;
 
-        public ToolbarIdleHandler(Context context) {
+        public ToolbarIdleHandler(Context context, final List<View> tools) {
+            this.tools = tools;
             mainHandler = new Handler() {
 
                 @Override
@@ -72,7 +115,7 @@
                         case MAKE_IDLE:
                             if (!idle) {
                                 idle = true;
-                                for (View view : childViews) {
+                                for (View view : tools) {
                                     view.startAnimation(fadeOut);
                                 }
                             }
@@ -89,24 +132,11 @@
             mainHandler.removeMessages(MAKE_IDLE);
             if (idle) {
                 idle = false;
-                for (View view : childViews) {
+                for (View view : tools) {
                     view.startAnimation(fadeIn);
                 }
             }
             mainHandler.sendEmptyMessageDelayed(MAKE_IDLE, TIMEOUT_IDLE);
         }
-
-        @Override
-        public void onChildViewAdded(View parent, View child) {
-            // All child views, except photo-view, will fade out on inactivity timeout.
-            if (child.getId() != R.id.photo_view) {
-                childViews.add(child);
-            }
-        }
-
-        @Override
-        public void onChildViewRemoved(View parent, View child) {
-            childViews.remove(child);
-        }
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/YesCancelDialogBuilder.java b/src/com/android/gallery3d/photoeditor/YesCancelDialogBuilder.java
new file mode 100644
index 0000000..4df76e6
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/YesCancelDialogBuilder.java
@@ -0,0 +1,31 @@
+package com.android.gallery3d.photoeditor;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+import com.android.gallery3d.R;
+
+/**
+ * Alert dialog builder that builds a simple Yes/Cancel dialog.
+ */
+public class YesCancelDialogBuilder extends AlertDialog.Builder {
+
+    public YesCancelDialogBuilder(Context context, final Runnable yes, int messageId) {
+        super(context);
+        setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                yes.run();
+            }
+        })
+        .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                // no-op
+            }
+        }).setMessage(messageId);
+    }
+}
diff --git a/src/com/android/gallery3d/photoeditor/YesNoCancelDialogBuilder.java b/src/com/android/gallery3d/photoeditor/YesNoCancelDialogBuilder.java
deleted file mode 100644
index fcc99c0..0000000
--- a/src/com/android/gallery3d/photoeditor/YesNoCancelDialogBuilder.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.android.gallery3d.photoeditor;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-
-import com.android.gallery3d.R;
-
-/**
- * Alert dialog builder that builds a simple Yes/No/Cancel dialog.
- */
-public class YesNoCancelDialogBuilder extends AlertDialog.Builder {
-
-    public YesNoCancelDialogBuilder(Context context, final Runnable yes, final Runnable no,
-            int messageId) {
-        super(context);
-        setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
-
-            @Override
-            public void onClick(DialogInterface dialog, int which) {
-                yes.run();
-            }
-        })
-        .setNeutralButton(R.string.no, new DialogInterface.OnClickListener() {
-
-            @Override
-            public void onClick(DialogInterface dialog, int which) {
-                no.run();
-            }
-        })
-        .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
-
-            @Override
-            public void onClick(DialogInterface dialog, int which) {
-                // no-op
-            }
-        }).setMessage(messageId);
-    }
-}
diff --git a/src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java b/src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java
index 27a0bce..aaf0e5b 100644
--- a/src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java
+++ b/src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java
@@ -28,7 +28,7 @@
 import com.android.gallery3d.R;
 
 /**
- * Seek-bar base that implements a draggable thumb that fits seek-bar height.
+ * Seek-bar base that implements a draggable thumb that fits seek-bar's track height.
  */
 abstract class AbstractSeekBar extends SeekBar {
 
@@ -38,22 +38,22 @@
 
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-
-        // Scale the thumb to fit seek-bar height.
+        // Scale the thumb to fit seek-bar's track height.
         Resources res = getResources();
         Drawable thumb = res.getDrawable(R.drawable.photoeditor_seekbar_thumb);
+        int height = h - getPaddingTop() - getPaddingBottom();
+        int scaledWidth = thumb.getIntrinsicWidth() * height / thumb.getIntrinsicHeight();
 
-        // Set the left/right padding to half width of the thumb drawn.
-        int scaledWidth = thumb.getIntrinsicWidth() * h / thumb.getIntrinsicHeight();
-        int padding = (scaledWidth + 1) / 2;
-        setPadding(padding, 0, padding, 0);
-
-        Bitmap bitmap = Bitmap.createBitmap(scaledWidth, h, Bitmap.Config.ARGB_8888);
+        Bitmap bitmap = Bitmap.createBitmap(scaledWidth, height, Bitmap.Config.ARGB_8888);
         Canvas canvas = new Canvas(bitmap);
         thumb.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
         thumb.draw(canvas);
 
+        // The thumb should not extend out of the track per UX design.
         setThumb(new BitmapDrawable(res, bitmap));
+        setThumbOffset(0);
+
+        // The thumb position is updated here after the thumb is changed.
+        super.onSizeChanged(w, h, oldw, oldh);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java b/src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java
index 26b5f51..a419840 100644
--- a/src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java
@@ -33,14 +33,10 @@
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         AutoFixFilter filter = new AutoFixFilter();
         filter.setScale(DEFAULT_SCALE);
-        notifyFilterChanged(filter, true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+        notifyChanged(filter);
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java b/src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java
index 5f9809b..41d1f24 100644
--- a/src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java
+++ b/src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java
@@ -42,7 +42,7 @@
     }
 
     private final int[] colors;
-    private Bitmap background;
+    private Bitmap progressDrawable;
 
     public ColorSeekBar(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -61,46 +61,47 @@
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
 
-        if (background != null) {
-            background.recycle();
+        if (progressDrawable != null) {
+            progressDrawable.recycle();
         }
-        background = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(background);
+        int width = w - getPaddingLeft() - getPaddingRight();
+        int height = h - getPaddingTop() - getPaddingBottom();
+        progressDrawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(progressDrawable);
 
         Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
         paint.setStyle(Paint.Style.FILL);
 
-        // Draw two half circles in the first and last colors at seek-bar left/right ends.
-        int radius = getThumbOffset();
+        // Draw two half circles in the first and last colors at the left/right ends.
+        int radius = height / 2;
         float left = radius;
-        float right = w - radius;
-        float cy = h / 2;
+        float right = width - radius;
 
         canvas.save();
-        canvas.clipRect(left, 0, right, h, Op.DIFFERENCE);
+        canvas.clipRect(left, 0, right, height, Op.DIFFERENCE);
         paint.setColor(colors[0]);
-        canvas.drawCircle(left, cy, radius, paint);
+        canvas.drawCircle(left, radius, radius, paint);
         paint.setColor(colors[colors.length - 1]);
-        canvas.drawCircle(right, cy, radius, paint);
+        canvas.drawCircle(right, radius, radius, paint);
         canvas.restore();
 
         // Draw color strips that make the thumb stop at every strip's center during seeking.
         float strip = (right - left) / (colors.length - 1);
         right = left + strip / 2;
         paint.setColor(colors[0]);
-        canvas.drawRect(left, 0, right, h, paint);
+        canvas.drawRect(left, 0, right, height, paint);
         left = right;
         for (int i = 1; i < colors.length - 1; i++) {
             right = left + strip;
             paint.setColor(colors[i]);
-            canvas.drawRect(left, 0, right, h, paint);
+            canvas.drawRect(left, 0, right, height, paint);
             left = right;
         }
         right = left + strip / 2;
         paint.setColor(colors[colors.length - 1]);
-        canvas.drawRect(left, 0, right, h, paint);
+        canvas.drawRect(left, 0, right, height, paint);
 
-        setBackgroundDrawable(new BitmapDrawable(getResources(), background));
+        setProgressDrawable(new BitmapDrawable(getResources(), progressDrawable));
     }
 
     public void setOnColorChangeListener(final OnColorChangeListener listener) {
diff --git a/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java b/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
index 24978fa..76bc446 100644
--- a/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
@@ -28,32 +28,25 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public ColorTemperatureAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final ColorTemperatureFilter filter = new ColorTemperatureFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.COLOR);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.COLOR);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
     }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/CropAction.java b/src/com/android/gallery3d/photoeditor/actions/CropAction.java
index 60a0179..e06c4e9 100644
--- a/src/com/android/gallery3d/photoeditor/actions/CropAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/CropAction.java
@@ -29,25 +29,24 @@
 
     private static final float DEFAULT_CROP = 0.2f;
 
-    private CropFilter filter;
-    private CropView cropView;
-
     public CropAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
-        filter = new CropFilter();
+    public void prepare() {
+        // Cropped results wouldn't be previewed for changed crop bounds.
+        final CropFilter filter = new CropFilter();
+        disableFilterOutput();
 
-        cropView = factory.createCropView();
+        CropView cropView = toolKit.addCropView();
         cropView.setOnCropChangeListener(new CropView.OnCropChangeListener() {
 
             @Override
             public void onCropChanged(RectF cropBounds, boolean fromUser) {
                 if (fromUser) {
                     filter.setCropBounds(cropBounds);
-                    notifyFilterChanged(filter, false);
+                    notifyChanged(filter);
                 }
             }
         });
@@ -55,12 +54,6 @@
         RectF bounds = new RectF(DEFAULT_CROP, DEFAULT_CROP, 1 - DEFAULT_CROP, 1 - DEFAULT_CROP);
         cropView.setCropBounds(bounds);
         filter.setCropBounds(bounds);
-        notifyFilterChanged(filter, false);
-    }
-
-    @Override
-    public void doEnd() {
-        cropView.setOnCropChangeListener(null);
-        notifyFilterChanged(filter, true);
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java b/src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java
index 8be60d3..b61f6fa 100644
--- a/src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java
@@ -31,12 +31,8 @@
     }
 
     @Override
-    public void doBegin() {
-        notifyFilterChanged(new CrossProcessFilter(), true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+    public void prepare() {
+        notifyChanged(new CrossProcessFilter());
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java b/src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java
index 0ec4a02..b7f9a90 100644
--- a/src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java
@@ -31,12 +31,8 @@
     }
 
     @Override
-    public void doBegin() {
-        notifyFilterChanged(new DocumentaryFilter(), true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+    public void prepare() {
+        notifyChanged(new DocumentaryFilter());
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/Doodle.java b/src/com/android/gallery3d/photoeditor/actions/Doodle.java
index ea23e23..bd08983 100644
--- a/src/com/android/gallery3d/photoeditor/actions/Doodle.java
+++ b/src/com/android/gallery3d/photoeditor/actions/Doodle.java
@@ -55,20 +55,23 @@
     }
 
     /**
-     * Adds control points whose coordinates range from 0 to 1 to construct the doodle path.
-     *
-     * @return true if the constructed path is in (0, 0, 1, 1) bounds; otherwise, false.
+     * Checks if the constructed doodle path is in (0, 0, 1, 1) bounds.
      */
-    public boolean addControlPoint(PointF point) {
-        PointF last = points.lastElement();
-        normalizedPath.quadTo(last.x, last.y, (last.x + point.x) / 2, (last.y + point.y) / 2);
-        points.add(point);
-
+    public boolean inBounds() {
         RectF r = new RectF();
         normalizedPath.computeBounds(r, false);
         return r.intersects(0, 0, 1, 1);
     }
 
+    /**
+     * Adds control points whose coordinates range from 0 to 1 to construct the doodle path.
+     */
+    public void addControlPoint(PointF point) {
+        PointF last = points.lastElement();
+        normalizedPath.quadTo(last.x, last.y, (last.x + point.x) / 2, (last.y + point.y) / 2);
+        points.add(point);
+    }
+
     public int getColor() {
         return color;
     }
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java b/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
index 4ad2cfb..f148287 100644
--- a/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
@@ -28,19 +28,37 @@
 
     private static final int DEFAULT_COLOR_INDEX = 4;
 
-    private DoodleFilter filter;
-    private ColorSeekBar colorPicker;
-    private DoodleView doodleView;
-
     public DoodleAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
-        filter = new DoodleFilter();
+    public void prepare() {
+        // Directly draw on doodle-view because running the doodle filter isn't fast enough.
+        final DoodleFilter filter = new DoodleFilter();
+        disableFilterOutput();
 
-        colorPicker = factory.createColorPicker();
+        final DoodleView doodleView = toolKit.addDoodleView();
+        doodleView.setOnDoodleChangeListener(new DoodleView.OnDoodleChangeListener() {
+
+            @Override
+            public void onDoodleChanged(Doodle doodle) {
+                // Check if the user draws within photo bounds and makes visible changes on photo.
+                if (doodle.inBounds()) {
+                    notifyChanged(filter);
+                }
+            }
+
+            @Override
+            public void onDoodleFinished(Doodle doodle) {
+                if (doodle.inBounds()) {
+                    filter.addDoodle(doodle);
+                    notifyChanged(filter);
+                }
+            }
+        });
+
+        ColorSeekBar colorPicker = toolKit.addColorPicker();
         colorPicker.setOnColorChangeListener(new ColorSeekBar.OnColorChangeListener() {
 
             @Override
@@ -51,30 +69,6 @@
             }
         });
         colorPicker.setColorIndex(DEFAULT_COLOR_INDEX);
-
-        doodleView = factory.createDoodleView();
-        doodleView.setOnDoodleChangeListener(new DoodleView.OnDoodleChangeListener() {
-
-            @Override
-            public void onDoodleInPhotoBounds() {
-                // Notify the user has drawn within photo bounds and made visible changes on photo.
-                filter.setDoodledInPhotoBounds();
-                notifyFilterChanged(filter, false);
-            }
-
-            @Override
-            public void onDoodleFinished(Doodle doodle) {
-                filter.addDoodle(doodle);
-                notifyFilterChanged(filter, false);
-            }
-        });
         doodleView.setColor(colorPicker.getColor());
     }
-
-    @Override
-    public void doEnd() {
-        colorPicker.setOnColorChangeListener(null);
-        doodleView.setOnDoodleChangeListener(null);
-        notifyFilterChanged(filter, true);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodleView.java b/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
index b596861..d396049 100644
--- a/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
+++ b/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
@@ -37,7 +37,7 @@
      */
     public interface OnDoodleChangeListener {
 
-        void onDoodleInPhotoBounds();
+        void onDoodleChanged(Doodle doodle);
 
         void onDoodleFinished(Doodle doodle);
     }
@@ -111,9 +111,10 @@
     }
 
     private void addLastPointIntoDoodle() {
-        if ((doodle != null) && doodle.addControlPoint(new PointF(lastPoint.x, lastPoint.y))) {
+        if (doodle != null) {
+            doodle.addControlPoint(new PointF(lastPoint.x, lastPoint.y));
             if (listener != null) {
-                listener.onDoodleInPhotoBounds();
+                listener.onDoodleChanged(doodle);
             }
             invalidate();
         }
diff --git a/src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java b/src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java
index b8da71e..3ba8aa5 100644
--- a/src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java
@@ -34,15 +34,11 @@
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         // TODO: Add several sets of duo-tone colors to select from.
         DuotoneFilter filter = new DuotoneFilter();
         filter.setDuotone(DEFAULT_FIRST_COLOR, DEFAULT_SECOND_COLOR);
-        notifyFilterChanged(filter, true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+        notifyChanged(filter);
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/EffectAction.java b/src/com/android/gallery3d/photoeditor/actions/EffectAction.java
index 6c6a893..92bcee4 100644
--- a/src/com/android/gallery3d/photoeditor/actions/EffectAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/EffectAction.java
@@ -37,46 +37,32 @@
     /**
      * Listener of effect action.
      */
-    public interface Listener {
+    public interface ActionListener {
 
-        void onClick();
-
-        void onDone();
+        /**
+         * Invoked when the action is okayed (effect is applied and completed).
+         */
+        void onOk();
     }
 
-    protected EffectToolFactory factory;
-
-    private Listener listener;
+    protected EffectToolKit toolKit;
     private Toast tooltip;
     private FilterStack filterStack;
     private boolean pushedFilter;
+    private boolean disableFilterOutput;
     private FilterChangedCallback lastFilterChangedCallback;
+    private ActionListener listener;
 
     public EffectAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
-    public void setListener(Listener l) {
-        listener = l;
-        findViewById(R.id.effect_button).setOnClickListener(
-                (listener == null) ? null : new View.OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                listener.onClick();
-            }
-        });
-    }
-
-    public CharSequence name() {
-        return ((TextView) findViewById(R.id.effect_label)).getText();
-    }
-
-    public void begin(FilterStack filterStack, EffectToolFactory factory) {
+    public void begin(View root, FilterStack filterStack, ActionListener listener) {
         // This view is already detached from UI view hierarchy by reaching here; findViewById()
         // could only access its own child views from here.
+        toolKit = new EffectToolKit(root, ((TextView) findViewById(R.id.effect_label)).getText());
         this.filterStack = filterStack;
-        this.factory = factory;
+        this.listener = listener;
 
         // Shows the tooltip if it's available.
         if (getTag() != null) {
@@ -84,14 +70,30 @@
             tooltip.setGravity(Gravity.CENTER, 0, 0);
             tooltip.show();
         }
-        doBegin();
+        prepare();
     }
 
     /**
+     * Subclasses should create a specific filter and bind the filter to necessary UI controls here
+     * when the action is about to begin.
+     */
+    protected abstract void prepare();
+
+    /**
      * Ends the effect and then executes the runnable after the effect is finished.
      */
     public void end(final Runnable runnableOnODone) {
-        doEnd();
+        // Cancel the tooltip if it's still showing.
+        if ((tooltip != null) && (tooltip.getView().getParent() != null)) {
+            tooltip.cancel();
+            tooltip = null;
+        }
+        // End tool editing by canceling unfinished touch events.
+        toolKit.cancel();
+        // Output the pushed filter if it wasn't outputted.
+        if (pushedFilter && disableFilterOutput) {
+            outputFilter();
+        }
 
         // Wait till last output callback is done before finishing.
         if ((lastFilterChangedCallback == null) || lastFilterChangedCallback.done) {
@@ -108,45 +110,38 @@
     }
 
     private void finish(Runnable runnableOnDone) {
-        // Close the tooltip if it's still showing.
-        if ((tooltip != null) && (tooltip.getView().getParent() != null)) {
-            tooltip.cancel();
-            tooltip = null;
-        }
+        toolKit.close();
         pushedFilter = false;
+        disableFilterOutput = false;
         lastFilterChangedCallback = null;
 
         runnableOnDone.run();
     }
 
-    protected void notifyDone() {
-        if (listener != null) {
-            listener.onDone();
-        }
+    protected void disableFilterOutput() {
+        // Filter output won't be outputted until this effect has done editing its filter.
+        disableFilterOutput = true;
     }
 
-    protected void notifyFilterChanged(Filter filter, boolean output) {
-        if (!pushedFilter && filter.isValid()) {
+    protected void outputFilter() {
+        // Notify the stack to execute the changed top filter and output the results.
+        lastFilterChangedCallback = new FilterChangedCallback();
+        filterStack.topFilterChanged(lastFilterChangedCallback);
+    }
+
+    protected void notifyChanged(Filter filter) {
+        if (!pushedFilter) {
             filterStack.pushFilter(filter);
             pushedFilter = true;
         }
-        if (pushedFilter && output) {
-            // Notify the stack to execute the changed top filter and output the results.
-            lastFilterChangedCallback = new FilterChangedCallback();
-            filterStack.topFilterChanged(lastFilterChangedCallback);
+        if (pushedFilter && !disableFilterOutput) {
+            outputFilter();
         }
     }
 
-    /**
-     * Subclasses should creates a specific filter and binds the filter to necessary UI controls
-     * here when the action is about to begin.
-     */
-    protected abstract void doBegin();
-
-    /**
-     * Subclasses could do specific ending operations here when the action is about to end.
-     */
-    protected abstract void doEnd();
+    protected void notifyOk() {
+        listener.onOk();
+    }
 
     /**
      * Done callback for executing top filter changes.
diff --git a/src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java b/src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java
deleted file mode 100644
index 3641828..0000000
--- a/src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.photoeditor.actions;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.gallery3d.R;
-import com.android.gallery3d.photoeditor.PhotoView;
-
-/**
- * Factory to create tools that will be used by effect actions.
- */
-public class EffectToolFactory {
-
-    public enum ScalePickerType {
-        LIGHT, SHADOW, COLOR, GENERIC
-    }
-
-    private final ViewGroup effectToolPanel;
-    private final LayoutInflater inflater;
-
-    public EffectToolFactory(ViewGroup effectToolPanel, LayoutInflater inflater) {
-        this.effectToolPanel = effectToolPanel;
-        this.inflater = inflater;
-    }
-
-    private View createFullscreenTool(int toolId) {
-        // Create full screen effect tool on top of photo-view and place it within the same
-        // view group that contains photo-view.
-        View photoView = effectToolPanel.getRootView().findViewById(R.id.photo_view);
-        ViewGroup parent = (ViewGroup) photoView.getParent();
-        FullscreenToolView view = (FullscreenToolView) inflater.inflate(toolId, parent, false);
-        view.setPhotoBounds(((PhotoView) photoView).getPhotoBounds());
-        parent.addView(view, parent.indexOfChild(photoView) + 1);
-        return view;
-    }
-
-    private View createPanelTool(int toolId) {
-        View view = inflater.inflate(toolId, effectToolPanel, false);
-        effectToolPanel.addView(view, 0);
-        return view;
-    }
-
-    private int getScalePickerBackground(ScalePickerType type) {
-        switch (type) {
-            case LIGHT:
-                return R.drawable.photoeditor_scale_seekbar_light;
-
-            case SHADOW:
-                return R.drawable.photoeditor_scale_seekbar_shadow;
-
-            case COLOR:
-                return R.drawable.photoeditor_scale_seekbar_color;
-        }
-        return R.drawable.photoeditor_scale_seekbar_generic;
-    }
-
-    public ScaleSeekBar createScalePicker(ScalePickerType type) {
-        ScaleSeekBar scalePicker = (ScaleSeekBar) createPanelTool(
-                R.layout.photoeditor_scale_seekbar);
-        scalePicker.setBackgroundResource(getScalePickerBackground(type));
-        return scalePicker;
-    }
-
-    public ColorSeekBar createColorPicker() {
-        return (ColorSeekBar) createPanelTool(R.layout.photoeditor_color_seekbar);
-    }
-
-    public DoodleView createDoodleView() {
-        return (DoodleView) createFullscreenTool(R.layout.photoeditor_doodle_view);
-    }
-
-    public TouchView createTouchView() {
-        return (TouchView) createFullscreenTool(R.layout.photoeditor_touch_view);
-    }
-
-    public FlipView createFlipView() {
-        return (FlipView) createFullscreenTool(R.layout.photoeditor_flip_view);
-    }
-
-    public RotateView createRotateView() {
-        return (RotateView) createFullscreenTool(R.layout.photoeditor_rotate_view);
-    }
-
-    public CropView createCropView() {
-        return (CropView) createFullscreenTool(R.layout.photoeditor_crop_view);
-    }
-}
diff --git a/src/com/android/gallery3d/photoeditor/actions/EffectToolKit.java b/src/com/android/gallery3d/photoeditor/actions/EffectToolKit.java
new file mode 100644
index 0000000..285e06b
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/EffectToolKit.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.photoeditor.PhotoView;
+
+/**
+ * Tool kit used by effect actions to retrieve tools, including managing tool creation/removal.
+ */
+public class EffectToolKit {
+
+    public enum ScaleType {
+        LIGHT, SHADOW, COLOR, GENERIC
+    }
+
+    private final LayoutInflater inflater;
+    private final PhotoView photoView;
+    private final ViewGroup toolPanel;
+    private final ViewGroup toolFullscreen;
+
+    public EffectToolKit(View root, CharSequence label) {
+        inflater = (LayoutInflater) root.getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        // Create effect tool panel as the first child of effects-bar.
+        ViewGroup effectsBar = (ViewGroup) root.findViewById(R.id.effects_bar);
+        toolPanel = (ViewGroup) inflater.inflate(
+                R.layout.photoeditor_effect_tool_panel, effectsBar, false);
+        ((TextView) toolPanel.findViewById(R.id.effect_label)).setText(label);
+        effectsBar.addView(toolPanel, 0);
+
+        // Create effect tool full-screen on top of photo-view and place it within the same
+        // view group that contains photo-view.
+        photoView = (PhotoView) root.findViewById(R.id.photo_view);
+        ViewGroup parent = (ViewGroup) photoView.getParent();
+        toolFullscreen = (ViewGroup) inflater.inflate(
+                R.layout.photoeditor_effect_tool_fullscreen, parent, false);
+        parent.addView(toolFullscreen, parent.indexOfChild(photoView) + 1);
+    }
+
+    public PhotoView getPhotoView() {
+        return photoView;
+    }
+
+    /**
+     * Cancel pending touch events and stop dispatching further touch events to tools.
+     */
+    public void cancel() {
+        long now = SystemClock.uptimeMillis();
+        MotionEvent cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+        toolFullscreen.dispatchTouchEvent(cancelEvent);
+        toolPanel.dispatchTouchEvent(cancelEvent);
+        cancelEvent.recycle();
+        View.OnTouchListener listener = new View.OnTouchListener() {
+
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                // Consume all further touch events and don't dispatch them.
+                return true;
+            }
+        };
+        toolFullscreen.setOnTouchListener(listener);
+        toolPanel.setOnTouchListener(listener);
+    }
+
+    /**
+     * Close to remove all created tools.
+     */
+    public void close() {
+        ((ViewGroup) toolFullscreen.getParent()).removeView(toolFullscreen);
+        ((ViewGroup) toolPanel.getParent()).removeView(toolPanel);
+    }
+
+    private View addFullscreenTool(int toolId) {
+        FullscreenToolView tool = (FullscreenToolView) inflater.inflate(
+                toolId, toolFullscreen, false);
+        tool.setPhotoBounds(getPhotoView().getPhotoBounds());
+        toolFullscreen.addView(tool);
+        return tool;
+    }
+
+    private View addPanelTool(int toolId) {
+        // Add the tool right above the effect-label in the panel.
+        View tool = inflater.inflate(toolId, toolPanel, false);
+        toolPanel.addView(tool, toolPanel.indexOfChild(toolPanel.findViewById(R.id.effect_label)));
+        return tool;
+    }
+
+    private Drawable getScalePickerProgressDrawable(Resources res, ScaleType type) {
+        switch (type) {
+            case LIGHT:
+                return res.getDrawable(R.drawable.photoeditor_scale_seekbar_light);
+
+            case SHADOW:
+                return res.getDrawable(R.drawable.photoeditor_scale_seekbar_shadow);
+
+            case COLOR:
+                return res.getDrawable(R.drawable.photoeditor_scale_seekbar_color);
+        }
+        return res.getDrawable(R.drawable.photoeditor_scale_seekbar_generic);
+    }
+
+    public ScaleSeekBar addScalePicker(ScaleType type) {
+        ScaleSeekBar scalePicker = (ScaleSeekBar) addPanelTool(
+                R.layout.photoeditor_scale_seekbar);
+        scalePicker.setProgressDrawable(getScalePickerProgressDrawable(
+                toolPanel.getResources(), type));
+        return scalePicker;
+    }
+
+    public ColorSeekBar addColorPicker() {
+        return (ColorSeekBar) addPanelTool(R.layout.photoeditor_color_seekbar);
+    }
+
+    public DoodleView addDoodleView() {
+        return (DoodleView) addFullscreenTool(R.layout.photoeditor_doodle_view);
+    }
+
+    public TouchView addTouchView() {
+        return (TouchView) addFullscreenTool(R.layout.photoeditor_touch_view);
+    }
+
+    public FlipView addFlipView() {
+        return (FlipView) addFullscreenTool(R.layout.photoeditor_flip_view);
+    }
+
+    public RotateView addRotateView() {
+        return (RotateView) addFullscreenTool(R.layout.photoeditor_rotate_view);
+    }
+
+    public CropView addCropView() {
+        return (CropView) addFullscreenTool(R.layout.photoeditor_crop_view);
+    }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/FaceTanAction.java b/src/com/android/gallery3d/photoeditor/actions/FaceTanAction.java
index a82f330..6b8f1d1 100644
--- a/src/com/android/gallery3d/photoeditor/actions/FaceTanAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/FaceTanAction.java
@@ -28,35 +28,28 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public FaceTanAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final FaceTanFilter filter = new FaceTanFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.GENERIC);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
 
         filter.setScale(DEFAULT_SCALE);
-        notifyFilterChanged(filter, true);
-    }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/FaceliftAction.java b/src/com/android/gallery3d/photoeditor/actions/FaceliftAction.java
index 90d4e0c..4c1a918 100644
--- a/src/com/android/gallery3d/photoeditor/actions/FaceliftAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/FaceliftAction.java
@@ -28,35 +28,28 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public FaceliftAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final FaceliftFilter filter = new FaceliftFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.GENERIC);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
 
         filter.setScale(DEFAULT_SCALE);
-        notifyFilterChanged(filter, true);
-    }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java b/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
index 73cf3d8..323bfd6 100644
--- a/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
@@ -28,32 +28,25 @@
 
     private static final float DEFAULT_SCALE = 0f;
 
-    private ScaleSeekBar scalePicker;
-
     public FillLightAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final FillLightFilter filter = new FillLightFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.LIGHT);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.LIGHT);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
     }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java b/src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java
index 348f004..df7ed78 100644
--- a/src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java
@@ -28,35 +28,28 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public FisheyeAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final FisheyeFilter filter = new FisheyeFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.GENERIC);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
 
         filter.setScale(DEFAULT_SCALE);
-        notifyFilterChanged(filter, true);
-    }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/FlipAction.java b/src/com/android/gallery3d/photoeditor/actions/FlipAction.java
index da238ba..71e5a90 100644
--- a/src/com/android/gallery3d/photoeditor/actions/FlipAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/FlipAction.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.util.AttributeSet;
 
-import com.android.gallery3d.R;
 import com.android.gallery3d.photoeditor.PhotoView;
 import com.android.gallery3d.photoeditor.filters.FlipFilter;
 
@@ -31,35 +30,32 @@
     private static final float DEFAULT_ANGLE = 0.0f;
     private static final float DEFAULT_FLIP_SPAN = 180.0f;
 
-    private FlipFilter filter;
-    private float horizontalFlipDegrees;
-    private float verticalFlipDegrees;
-    private Runnable queuedFlipChange;
-    private FlipView flipView;
-
     public FlipAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
-        filter = new FlipFilter();
+    public void prepare() {
+        // Disable outputting flipped results and directly flip photo-view for animations.
+        final FlipFilter filter = new FlipFilter();
+        disableFilterOutput();
 
-        flipView = factory.createFlipView();
+        final FlipView flipView = toolKit.addFlipView();
         flipView.setOnFlipChangeListener(new FlipView.OnFlipChangeListener() {
 
-            // Directly transform photo-view because running the flip filter isn't fast enough.
-            PhotoView photoView = (PhotoView) flipView.getRootView().findViewById(
-                    R.id.photo_view);
+            float horizontalDegrees;
+            float verticalDegrees;
+            Runnable queuedTransform;
+            PhotoView photoView = toolKit.getPhotoView();
 
             @Override
             public void onAngleChanged(float horizontalDegrees, float verticalDegrees,
                     boolean fromUser) {
                 if (fromUser) {
-                    horizontalFlipDegrees = horizontalDegrees;
-                    verticalFlipDegrees = verticalDegrees;
-                    updateFlipFilter(false);
+                    this.horizontalDegrees = horizontalDegrees;
+                    this.verticalDegrees = verticalDegrees;
                     transformPhotoView(horizontalDegrees, verticalDegrees);
+                    notifyChanged(filter);
                 }
             }
 
@@ -70,59 +66,42 @@
 
             @Override
             public void onStopTrackingTouch() {
-                roundFlipDegrees();
-                updateFlipFilter(false);
-                transformPhotoView(horizontalFlipDegrees, verticalFlipDegrees);
-                flipView.setFlippedAngles(horizontalFlipDegrees, verticalFlipDegrees);
+                // Round flip degrees to multiples of 180 degrees.
+                horizontalDegrees = roundTo180(horizontalDegrees);
+                verticalDegrees = roundTo180(verticalDegrees);
+                transformPhotoView(horizontalDegrees, verticalDegrees);
+                flipView.setFlippedAngles(horizontalDegrees, verticalDegrees);
+
+                // Flip the filter according to the flipped directions of flip-view.
+                filter.setFlip(((int) horizontalDegrees / 180) % 2 != 0,
+                        ((int) verticalDegrees / 180) % 2 != 0);
+                notifyChanged(filter);
+            }
+
+            private float roundTo180(float degrees) {
+                if (degrees % 180 != 0) {
+                    degrees = Math.round(degrees / 180) * 180;
+                }
+                return degrees;
             }
 
             private void transformPhotoView(final float horizontalDegrees,
                     final float verticalDegrees) {
                 // Remove the outdated flip change before queuing a new one.
-                if (queuedFlipChange != null) {
-                    photoView.remove(queuedFlipChange);
+                if (queuedTransform != null) {
+                    photoView.remove(queuedTransform);
                 }
-                queuedFlipChange = new Runnable() {
+                queuedTransform = new Runnable() {
 
                     @Override
                     public void run() {
                         photoView.flipPhoto(horizontalDegrees, verticalDegrees);
                     }
                 };
-                photoView.queue(queuedFlipChange);
+                photoView.queue(queuedTransform);
             }
         });
         flipView.setFlippedAngles(DEFAULT_ANGLE, DEFAULT_ANGLE);
         flipView.setFlipSpan(DEFAULT_FLIP_SPAN);
-        horizontalFlipDegrees = 0;
-        verticalFlipDegrees = 0;
-        queuedFlipChange = null;
-    }
-
-    @Override
-    public void doEnd() {
-        flipView.setOnFlipChangeListener(null);
-        // Round the current flip degrees in case flip tracking has not stopped yet.
-        roundFlipDegrees();
-        updateFlipFilter(true);
-    }
-
-    /**
-     * Rounds flip degrees to multiples of 180 degrees.
-     */
-    private void roundFlipDegrees() {
-        if (horizontalFlipDegrees % 180 != 0) {
-            horizontalFlipDegrees = Math.round(horizontalFlipDegrees / 180) * 180;
-        }
-        if (verticalFlipDegrees % 180 != 0) {
-            verticalFlipDegrees = Math.round(verticalFlipDegrees / 180) * 180;
-        }
-    }
-
-    private void updateFlipFilter(boolean outputFilter) {
-        // Flip the filter if the flipped degrees are at the opposite directions.
-        filter.setFlip(((int) horizontalFlipDegrees / 180) % 2 != 0,
-                ((int) verticalFlipDegrees / 180) % 2 != 0);
-        notifyFilterChanged(filter, outputFilter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/GrainAction.java b/src/com/android/gallery3d/photoeditor/actions/GrainAction.java
index 258eb8a..5e99129 100644
--- a/src/com/android/gallery3d/photoeditor/actions/GrainAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/GrainAction.java
@@ -28,35 +28,28 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public GrainAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final GrainFilter filter = new GrainFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.GENERIC);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
 
         filter.setScale(DEFAULT_SCALE);
-        notifyFilterChanged(filter, true);
-    }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java b/src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java
index ac89cd1..f534ca8 100644
--- a/src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java
@@ -31,12 +31,8 @@
     }
 
     @Override
-    public void doBegin() {
-        notifyFilterChanged(new GrayscaleFilter(), true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+    public void prepare() {
+        notifyChanged(new GrayscaleFilter());
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java b/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
index a3d62d2..18d7add 100644
--- a/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
@@ -28,32 +28,25 @@
 
     private static final float DEFAULT_SCALE = 0f;
 
-    private ScaleSeekBar scalePicker;
-
     public HighlightAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final HighlightFilter filter = new HighlightFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.LIGHT);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.LIGHT);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
     }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/LomoishAction.java b/src/com/android/gallery3d/photoeditor/actions/LomoishAction.java
index 44ffc52..17af429 100644
--- a/src/com/android/gallery3d/photoeditor/actions/LomoishAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/LomoishAction.java
@@ -31,12 +31,8 @@
     }
 
     @Override
-    public void doBegin() {
-        notifyFilterChanged(new LomoishFilter(), true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+    public void prepare() {
+        notifyChanged(new LomoishFilter());
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/NegativeAction.java b/src/com/android/gallery3d/photoeditor/actions/NegativeAction.java
index 5276421..8893154 100644
--- a/src/com/android/gallery3d/photoeditor/actions/NegativeAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/NegativeAction.java
@@ -31,12 +31,8 @@
     }
 
     @Override
-    public void doBegin() {
-        notifyFilterChanged(new NegativeFilter(), true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+    public void prepare() {
+        notifyChanged(new NegativeFilter());
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java b/src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java
index 760539d..9a9f874 100644
--- a/src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java
@@ -31,12 +31,8 @@
     }
 
     @Override
-    public void doBegin() {
-        notifyFilterChanged(new PosterizeFilter(), true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+    public void prepare() {
+        notifyChanged(new PosterizeFilter());
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java b/src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java
index a472ad9..b957715 100644
--- a/src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.graphics.PointF;
+import android.graphics.RectF;
 import android.util.AttributeSet;
 
 import com.android.gallery3d.photoeditor.filters.RedEyeFilter;
@@ -27,29 +28,27 @@
  */
 public class RedEyeAction extends EffectAction {
 
-    private TouchView touchView;
-
     public RedEyeAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final RedEyeFilter filter = new RedEyeFilter();
 
-        touchView = factory.createTouchView();
+        TouchView touchView = toolKit.addTouchView();
         touchView.setSingleTapListener(new TouchView.SingleTapListener() {
 
+            final RectF bounds = new RectF(0, 0, 1, 1);
+
             @Override
             public void onSingleTap(PointF point) {
-                filter.addRedEyePosition(point);
-                notifyFilterChanged(filter, true);
+                // Check if the user taps within photo bounds to remove red eye on photo.
+                if (bounds.contains(point.x, point.y)) {
+                    filter.addRedEyePosition(point);
+                    notifyChanged(filter);
+                }
             }
         });
     }
-
-    @Override
-    public void doEnd() {
-        touchView.setSingleTapListener(null);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/RotateAction.java b/src/com/android/gallery3d/photoeditor/actions/RotateAction.java
index 36a09d9..98d6555 100644
--- a/src/com/android/gallery3d/photoeditor/actions/RotateAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/RotateAction.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.util.AttributeSet;
 
-import com.android.gallery3d.R;
 import com.android.gallery3d.photoeditor.PhotoView;
 import com.android.gallery3d.photoeditor.filters.RotateFilter;
 
@@ -31,32 +30,29 @@
     private static final float DEFAULT_ANGLE = 0.0f;
     private static final float DEFAULT_ROTATE_SPAN = 360.0f;
 
-    private RotateFilter filter;
-    private float rotateDegrees;
-    private Runnable queuedRotationChange;
-    private RotateView rotateView;
-
     public RotateAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
-        filter = new RotateFilter();
+    public void prepare() {
+        // Disable outputting rotated results and directly rotate photo-view for animations.
+        final RotateFilter filter = new RotateFilter();
+        disableFilterOutput();
 
-        rotateView = factory.createRotateView();
+        final RotateView rotateView = toolKit.addRotateView();
         rotateView.setOnRotateChangeListener(new RotateView.OnRotateChangeListener() {
 
-            // Directly transform photo-view because running the rotate filter isn't fast enough.
-            PhotoView photoView = (PhotoView) rotateView.getRootView().findViewById(
-                    R.id.photo_view);
+            float rotateDegrees;
+            Runnable queuedTransform;
+            PhotoView photoView = toolKit.getPhotoView();
 
             @Override
-            public void onAngleChanged(float degrees, boolean fromUser){
+            public void onAngleChanged(float degrees, boolean fromUser) {
                 if (fromUser) {
                     rotateDegrees = degrees;
-                    updateRotateFilter(false);
-                    transformPhotoView(degrees);
+                    transformPhotoView(rotateDegrees);
+                    notifyChanged(filter);
                 }
             }
 
@@ -67,52 +63,32 @@
 
             @Override
             public void onStopTrackingTouch() {
-                roundRotateDegrees();
-                updateRotateFilter(false);
+                // Round rotate degrees to multiples of 90 degrees.
+                if (rotateDegrees % 90 != 0) {
+                    rotateDegrees = Math.round(rotateDegrees / 90) * 90;
+                }
                 transformPhotoView(rotateDegrees);
                 rotateView.setRotatedAngle(rotateDegrees);
+                filter.setAngle(rotateDegrees);
+                notifyChanged(filter);
             }
 
             private void transformPhotoView(final float degrees) {
                 // Remove the outdated rotation change before queuing a new one.
-                if (queuedRotationChange != null) {
-                    photoView.remove(queuedRotationChange);
+                if (queuedTransform != null) {
+                    photoView.remove(queuedTransform);
                 }
-                queuedRotationChange = new Runnable() {
+                queuedTransform = new Runnable() {
 
                     @Override
                     public void run() {
                         photoView.rotatePhoto(degrees);
                     }
                 };
-                photoView.queue(queuedRotationChange);
+                photoView.queue(queuedTransform);
             }
         });
         rotateView.setRotatedAngle(DEFAULT_ANGLE);
         rotateView.setRotateSpan(DEFAULT_ROTATE_SPAN);
-        rotateDegrees = 0;
-        queuedRotationChange = null;
-    }
-
-    @Override
-    public void doEnd() {
-        rotateView.setOnRotateChangeListener(null);
-        // Round the current rotation degrees in case rotation tracking has not stopped yet.
-        roundRotateDegrees();
-        updateRotateFilter(true);
-    }
-
-    /**
-     * Rounds rotate degrees to multiples of 90 degrees.
-     */
-    private void roundRotateDegrees() {
-        if (rotateDegrees % 90 != 0) {
-            rotateDegrees = Math.round(rotateDegrees / 90) * 90;
-        }
-    }
-
-    private void updateRotateFilter(boolean outputFilter) {
-        filter.setAngle(rotateDegrees);
-        notifyFilterChanged(filter, outputFilter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java b/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
index 2f67e0a..6afd7ba 100644
--- a/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
@@ -28,32 +28,25 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public SaturationAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final SaturationFilter filter = new SaturationFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.COLOR);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.COLOR);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
     }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/SepiaAction.java b/src/com/android/gallery3d/photoeditor/actions/SepiaAction.java
index c431115..5f551c8 100644
--- a/src/com/android/gallery3d/photoeditor/actions/SepiaAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/SepiaAction.java
@@ -31,12 +31,8 @@
     }
 
     @Override
-    public void doBegin() {
-        notifyFilterChanged(new SepiaFilter(), true);
-        notifyDone();
-    }
-
-    @Override
-    public void doEnd() {
+    public void prepare() {
+        notifyChanged(new SepiaFilter());
+        notifyOk();
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java b/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
index 15ba850..cfd0538 100644
--- a/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
@@ -28,32 +28,25 @@
 
     private static final float DEFAULT_SCALE = 0f;
 
-    private ScaleSeekBar scalePicker;
-
     public ShadowAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final ShadowFilter filter = new ShadowFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.SHADOW);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.SHADOW);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
     }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java b/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
index c6b240b..7c00b21 100644
--- a/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
@@ -28,35 +28,28 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public SharpenAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final SharpenFilter filter = new SharpenFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.GENERIC);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
 
         filter.setScale(DEFAULT_SCALE);
-        notifyFilterChanged(filter, true);
-    }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/StraightenAction.java b/src/com/android/gallery3d/photoeditor/actions/StraightenAction.java
index 42b384d..55eb8fd 100644
--- a/src/com/android/gallery3d/photoeditor/actions/StraightenAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/StraightenAction.java
@@ -29,24 +29,22 @@
     private static final float DEFAULT_ANGLE = 0.0f;
     private static final float DEFAULT_ROTATE_SPAN = StraightenFilter.MAX_DEGREES * 2;
 
-    private RotateView rotateView;
-
     public StraightenAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final StraightenFilter filter = new StraightenFilter();
 
-        rotateView = factory.createRotateView();
+        RotateView rotateView = toolKit.addRotateView();
         rotateView.setOnRotateChangeListener(new RotateView.OnRotateChangeListener() {
 
             @Override
             public void onAngleChanged(float degrees, boolean fromUser){
                 if (fromUser) {
                     filter.setAngle(degrees);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
 
@@ -64,9 +62,4 @@
         rotateView.setRotatedAngle(DEFAULT_ANGLE);
         rotateView.setRotateSpan(DEFAULT_ROTATE_SPAN);
     }
-
-    @Override
-    public void doEnd() {
-        rotateView.setOnRotateChangeListener(null);
-    }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/TintAction.java b/src/com/android/gallery3d/photoeditor/actions/TintAction.java
index defd2a3..417c8f5 100644
--- a/src/com/android/gallery3d/photoeditor/actions/TintAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/TintAction.java
@@ -28,35 +28,28 @@
 
     private static final int DEFAULT_COLOR_INDEX = 13;
 
-    private ColorSeekBar colorPicker;
-
     public TintAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final TintFilter filter = new TintFilter();
 
-        colorPicker = factory.createColorPicker();
+        ColorSeekBar colorPicker = toolKit.addColorPicker();
         colorPicker.setOnColorChangeListener(new ColorSeekBar.OnColorChangeListener() {
 
             @Override
             public void onColorChanged(int color, boolean fromUser) {
                 if (fromUser) {
                     filter.setTint(color);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
-        // Tint photo with the default color.
         colorPicker.setColorIndex(DEFAULT_COLOR_INDEX);
-        filter.setTint(colorPicker.getColor());
-        notifyFilterChanged(filter, true);
-    }
 
-    @Override
-    public void doEnd() {
-        colorPicker.setOnColorChangeListener(null);
+        filter.setTint(colorPicker.getColor());
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/actions/TouchView.java b/src/com/android/gallery3d/photoeditor/actions/TouchView.java
index 0548cc4..d5f311c 100644
--- a/src/com/android/gallery3d/photoeditor/actions/TouchView.java
+++ b/src/com/android/gallery3d/photoeditor/actions/TouchView.java
@@ -28,20 +28,6 @@
 class TouchView extends FullscreenToolView {
 
     /**
-     * Listener of swipes.
-     */
-    public interface SwipeListener {
-
-        void onSwipeLeft();
-
-        void onSwipeRight();
-
-        void onSwipeUp();
-
-        void onSwipeDown();
-    }
-
-    /**
      * Listener of single tap on a point (relative to photo coordinates).
      */
     public interface SingleTapListener {
@@ -50,21 +36,18 @@
     }
 
     private final GestureDetector gestureDetector;
-
-    private SwipeListener swipeListener;
     private SingleTapListener singleTapListener;
 
     public TouchView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
-        final int swipeThreshold = (int) (500 * getResources().getDisplayMetrics().density);
         gestureDetector = new GestureDetector(
                 context, new GestureDetector.SimpleOnGestureListener() {
 
             @Override
             public boolean onDown(MotionEvent e) {
-                // GestureDetector onTouchEvent returns true for fling events only when their
-                // preceding down events are consumed.
+                // GestureDetector onTouchEvent returns true only for events whose preceding
+                // down-events have been consumed.
                 return true;
             }
 
@@ -77,37 +60,10 @@
                 }
                 return true;
             }
-
-            @Override
-            public boolean onFling(
-                    MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) {
-                if (swipeListener != null) {
-                    float absX = Math.abs(velocityX);
-                    float absY = Math.abs(velocityY);
-                    float deltaX = me2.getX() - me1.getX();
-                    float deltaY = me2.getY() - me1.getY();
-                    int travelX = getWidth() / 4;
-                    int travelY = getHeight() / 4;
-                    if (velocityX > swipeThreshold && absY < absX && deltaX > travelX) {
-                        swipeListener.onSwipeRight();
-                    } else if (velocityX < -swipeThreshold && absY < absX && deltaX < -travelX) {
-                        swipeListener.onSwipeLeft();
-                    } else if (velocityY < -swipeThreshold && absX < absY && deltaY < -travelY) {
-                        swipeListener.onSwipeUp();
-                    } else if (velocityY > swipeThreshold && absX < absY / 2 && deltaY > travelY) {
-                        swipeListener.onSwipeDown();
-                    }
-                }
-                return true;
-            }
         });
         gestureDetector.setIsLongpressEnabled(false);
     }
 
-    public void setSwipeListener(SwipeListener listener) {
-        swipeListener = listener;
-    }
-
     public void setSingleTapListener(SingleTapListener listener) {
         singleTapListener = listener;
     }
diff --git a/src/com/android/gallery3d/photoeditor/actions/VignetteAction.java b/src/com/android/gallery3d/photoeditor/actions/VignetteAction.java
index f59c636..9f6bcc7 100644
--- a/src/com/android/gallery3d/photoeditor/actions/VignetteAction.java
+++ b/src/com/android/gallery3d/photoeditor/actions/VignetteAction.java
@@ -28,35 +28,28 @@
 
     private static final float DEFAULT_SCALE = 0.5f;
 
-    private ScaleSeekBar scalePicker;
-
     public VignetteAction(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
-    public void doBegin() {
+    public void prepare() {
         final VignetteFilter filter = new VignetteFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+        ScaleSeekBar scalePicker = toolKit.addScalePicker(EffectToolKit.ScaleType.GENERIC);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
             public void onProgressChanged(float progress, boolean fromUser) {
                 if (fromUser) {
                     filter.setScale(progress);
-                    notifyFilterChanged(filter, true);
+                    notifyChanged(filter);
                 }
             }
         });
         scalePicker.setProgress(DEFAULT_SCALE);
 
         filter.setScale(DEFAULT_SCALE);
-        notifyFilterChanged(filter, true);
-    }
-
-    @Override
-    public void doEnd() {
-        scalePicker.setOnScaleChangeListener(null);
+        notifyChanged(filter);
     }
 }
diff --git a/src/com/android/gallery3d/photoeditor/filters/AbstractScaleFilter.java b/src/com/android/gallery3d/photoeditor/filters/AbstractScaleFilter.java
index 727a98c..6376d33 100644
--- a/src/com/android/gallery3d/photoeditor/filters/AbstractScaleFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/AbstractScaleFilter.java
@@ -30,7 +30,6 @@
      */
     public void setScale(float scale) {
         this.scale = scale;
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/filters/CropFilter.java b/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
index ccca813..00a6c42 100644
--- a/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
@@ -37,7 +37,6 @@
      */
     public void setCropBounds(RectF bounds) {
         this.bounds = bounds;
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java b/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
index e82a667..bc233da 100644
--- a/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
@@ -27,10 +27,6 @@
 
     public static final Creator<CrossProcessFilter> CREATOR = creatorOf(CrossProcessFilter.class);
 
-    public CrossProcessFilter() {
-        validate();
-    }
-
     @Override
     public void process(Photo src, Photo dst) {
         getEffect(EffectFactory.EFFECT_CROSSPROCESS).apply(
diff --git a/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java b/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
index d6f347b..d2e4c7c 100644
--- a/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
@@ -27,10 +27,6 @@
 
     public static final Creator<DocumentaryFilter> CREATOR = creatorOf(DocumentaryFilter.class);
 
-    public DocumentaryFilter() {
-        validate();
-    }
-
     @Override
     public void process(Photo src, Photo dst) {
         getEffect(EffectFactory.EFFECT_DOCUMENTARY).apply(
diff --git a/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java b/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
index 277e06d..61920d3 100644
--- a/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
@@ -40,14 +40,6 @@
 
     private final Vector<Doodle> doodles = new Vector<Doodle>();
 
-    /**
-     * Signals once at least a doodle drawn within photo bounds; this filter is regarded as invalid
-     * (no-op on the photo) until not all its doodling is out of bounds.
-     */
-    public void setDoodledInPhotoBounds() {
-        validate();
-    }
-
     public void addDoodle(Doodle doodle) {
         doodles.add(doodle);
     }
diff --git a/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java b/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
index b94f95e..b2c5525 100644
--- a/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
@@ -35,7 +35,6 @@
     public void setDuotone(int firstColor, int secondColor) {
         this.firstColor = firstColor;
         this.secondColor = secondColor;
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/filters/Filter.java b/src/com/android/gallery3d/photoeditor/filters/Filter.java
index baa3747..5d1ac22 100644
--- a/src/com/android/gallery3d/photoeditor/filters/Filter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/Filter.java
@@ -26,8 +26,7 @@
 import java.util.HashMap;
 
 /**
- * Image filter for photo editing; most of its methods must be called from a single GL thread except
- * validate()/isValid() that are called from UI thread.
+ * Image filter for photo editing; all of its methods must be called from a single GL thread.
  */
 public abstract class Filter implements Parcelable {
 
@@ -37,8 +36,6 @@
     private static final HashMap<Filter, Effect> effects = new HashMap<Filter, Effect>();
     private static EffectContext context;
 
-    private boolean isValid;
-
     /**
      * Filter context should be released before the current GL context is lost.
      */
@@ -74,18 +71,6 @@
         return effect;
     }
 
-    protected void validate() {
-        isValid = true;
-    }
-
-    /**
-     * Some filters, e.g. lighting filters, are initially invalid until set up with parameters while
-     * others, e.g. Sepia or Posterize filters, are initially valid without parameters.
-     */
-    public boolean isValid() {
-        return isValid;
-    }
-
     /**
      * Processes the source bitmap and matrix and output the destination bitmap and matrix.
      *
diff --git a/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java b/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
index 816aad8..9c325c1 100644
--- a/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
@@ -34,7 +34,6 @@
     public void setFlip(boolean flipHorizontal, boolean flipVertical) {
         flips[0] = flipHorizontal;
         flips[1] = flipVertical;
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java b/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
index 38dfb52..b0e94ef 100644
--- a/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
@@ -27,10 +27,6 @@
 
     public static final Creator<GrayscaleFilter> CREATOR = creatorOf(GrayscaleFilter.class);
 
-    public GrayscaleFilter() {
-        validate();
-    }
-
     @Override
     public void process(Photo src, Photo dst) {
         getEffect(EffectFactory.EFFECT_GRAYSCALE).apply(
diff --git a/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java b/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
index f8c5173..16a1d61 100644
--- a/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
@@ -27,10 +27,6 @@
 
     public static final Creator<LomoishFilter> CREATOR = creatorOf(LomoishFilter.class);
 
-    public LomoishFilter() {
-        validate();
-    }
-
     @Override
     public void process(Photo src, Photo dst) {
         getEffect(EffectFactory.EFFECT_LOMOISH).apply(
diff --git a/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java b/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
index 88bbd58..db702d7 100644
--- a/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
@@ -27,10 +27,6 @@
 
     public static final Creator<NegativeFilter> CREATOR = creatorOf(NegativeFilter.class);
 
-    public NegativeFilter() {
-        validate();
-    }
-
     @Override
     public void process(Photo src, Photo dst) {
         getEffect(EffectFactory.EFFECT_NEGATIVE).apply(
diff --git a/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java b/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
index 186baa9..23e78bf 100644
--- a/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
@@ -27,10 +27,6 @@
 
     public static final Creator<PosterizeFilter> CREATOR = creatorOf(PosterizeFilter.class);
 
-    public PosterizeFilter() {
-        validate();
-    }
-
     @Override
     public void process(Photo src, Photo dst) {
         getEffect(EffectFactory.EFFECT_POSTERIZE).apply(
diff --git a/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java b/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
index 257d322..32e8f7c 100644
--- a/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
@@ -39,7 +39,6 @@
      */
     public void addRedEyePosition(PointF point) {
         redeyes.add(point);
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java b/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
index d377f96..d820bda 100644
--- a/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
@@ -31,9 +31,11 @@
 
     private float degrees;
 
+    /**
+     * Sets rotation angle which must be multiples of 90 degrees.
+     */
     public void setAngle(float degrees) {
         this.degrees = degrees;
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java b/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
index 6c1a70e..d95c0d8 100644
--- a/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
@@ -27,10 +27,6 @@
 
     public static final Creator<SepiaFilter> CREATOR = creatorOf(SepiaFilter.class);
 
-    public SepiaFilter() {
-        validate();
-    }
-
     @Override
     public void process(Photo src, Photo dst) {
         getEffect(EffectFactory.EFFECT_SEPIA).apply(
diff --git a/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java b/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
index 90738f0..bf4ace5 100644
--- a/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
@@ -28,13 +28,12 @@
 public class StraightenFilter extends Filter {
 
     public static final Creator<StraightenFilter> CREATOR = creatorOf(StraightenFilter.class);
-    public static final float MAX_DEGREES = 30.0f;
+    public static final float MAX_DEGREES = 45.0f;
 
     private float degrees;
 
     public void setAngle(float degrees) {
         this.degrees = degrees;
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/photoeditor/filters/TintFilter.java b/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
index af3d777..7a7463e 100644
--- a/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
+++ b/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
@@ -33,7 +33,6 @@
 
     public void setTint(int color) {
         this.color = color;
-        validate();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/provider/GalleryProvider.java b/src/com/android/gallery3d/provider/GalleryProvider.java
index 4068d46..79ec66b 100644
--- a/src/com/android/gallery3d/provider/GalleryProvider.java
+++ b/src/com/android/gallery3d/provider/GalleryProvider.java
@@ -16,17 +16,6 @@
 
 package com.android.gallery3d.provider;
 
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.DownloadCache;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.MediaObject;
-import com.android.gallery3d.data.MtpImage;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.picasasource.PicasaSource;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
@@ -39,6 +28,16 @@
 import android.provider.MediaStore.Images.ImageColumns;
 import android.util.Log;
 
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MtpImage;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -48,7 +47,15 @@
 
     public static final String AUTHORITY = "com.android.gallery3d.provider";
     public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+
+    public static interface PicasaColumns {
+        public static final String USER_ACCOUNT = "user_account";
+        public static final String PICASA_ID = "picasa_id";
+    }
+
     private static final String[] SUPPORTED_PICASA_COLUMNS = {
+            PicasaColumns.USER_ACCOUNT,
+            PicasaColumns.PICASA_ID,
             ImageColumns.DISPLAY_NAME,
             ImageColumns.SIZE,
             ImageColumns.MIME_TYPE,
@@ -58,7 +65,6 @@
             ImageColumns.ORIENTATION};
 
     private DataManager mDataManager;
-    private DownloadCache mDownloadCache;
     private static Uri sBaseUri;
 
     public static String getAuthority(Context context) {
@@ -104,14 +110,6 @@
         return true;
     }
 
-    private DownloadCache getDownloadCache() {
-        if (mDownloadCache == null) {
-            GalleryApp app = (GalleryApp) getContext().getApplicationContext();
-            mDownloadCache = app.getDownloadCache();
-        }
-        return mDownloadCache;
-    }
-
     // TODO: consider concurrent access
     @Override
     public Cursor query(Uri uri, String[] projection,
@@ -170,7 +168,11 @@
 
         for (int i = 0, n = projection.length; i < n; ++i) {
             String column = projection[i];
-            if (ImageColumns.DISPLAY_NAME.equals(column)) {
+            if (PicasaColumns.USER_ACCOUNT.equals(column)) {
+                columnValues[i] = PicasaSource.getUserAccount(getContext(), image);
+            } else if (PicasaColumns.PICASA_ID.equals(column)) {
+                columnValues[i] = PicasaSource.getPicasaId(image);
+            } else if (ImageColumns.DISPLAY_NAME.equals(column)) {
                 columnValues[i] = PicasaSource.getImageTitle(image);
             } else if (ImageColumns.SIZE.equals(column)){
                 columnValues[i] = PicasaSource.getImageSize(image);
diff --git a/src/com/android/gallery3d/ui/AbstractDisplayItem.java b/src/com/android/gallery3d/ui/AbstractDisplayItem.java
deleted file mode 100644
index 28acc3b..0000000
--- a/src/com/android/gallery3d/ui/AbstractDisplayItem.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.data.MediaItem;
-
-import android.graphics.Bitmap;
-
-public abstract class AbstractDisplayItem extends DisplayItem {
-
-    private static final String TAG = "AbstractDisplayItem";
-
-    private static final int STATE_INVALID = 0x01;
-    private static final int STATE_VALID = 0x02;
-    private static final int STATE_UPDATING = 0x04;
-    private static final int STATE_CANCELING = 0x08;
-    private static final int STATE_ERROR = 0x10;
-
-    private int mState = STATE_INVALID;
-    private boolean mImageRequested = false;
-    private boolean mRecycling = false;
-    private Bitmap mBitmap;
-
-    protected final MediaItem mMediaItem;
-
-    public AbstractDisplayItem(MediaItem item) {
-        mMediaItem = item;
-        if (item == null) mState = STATE_ERROR;
-    }
-
-    protected void updateImage(Bitmap bitmap, boolean isCancelled) {
-        if (mRecycling) {
-            return;
-        }
-
-        if (isCancelled && bitmap == null) {
-            mState = STATE_INVALID;
-            if (mImageRequested) {
-                // request image again.
-                requestImage();
-            }
-            return;
-        }
-
-        mBitmap = bitmap;
-        mState = bitmap == null ? STATE_ERROR : STATE_VALID ;
-        onBitmapAvailable(mBitmap);
-    }
-
-    @Override
-    public int getRotation() {
-        if (mMediaItem != null) return mMediaItem.getRotation();
-        return 0;
-    }
-
-    @Override
-    public long getIdentity() {
-        return mMediaItem != null
-                ? System.identityHashCode(mMediaItem.getPath())
-                : System.identityHashCode(this);
-    }
-
-    public void requestImage() {
-        mImageRequested = true;
-        if (mState == STATE_INVALID) {
-            mState = STATE_UPDATING;
-            startLoadBitmap();
-        }
-    }
-
-    public void cancelImageRequest() {
-        mImageRequested = false;
-        if (mState == STATE_UPDATING) {
-            mState = STATE_CANCELING;
-            cancelLoadBitmap();
-        }
-    }
-
-    private boolean inState(int states) {
-        return (mState & states) != 0;
-    }
-
-    public void recycle() {
-        if (!inState(STATE_UPDATING | STATE_CANCELING)) {
-            if (mBitmap != null) mBitmap = null;
-        } else {
-            mRecycling = true;
-            cancelImageRequest();
-        }
-    }
-
-    public boolean isRequestInProgress() {
-        return mImageRequested && inState(STATE_UPDATING | STATE_CANCELING);
-    }
-
-    abstract protected void startLoadBitmap();
-    abstract protected void cancelLoadBitmap();
-    abstract protected void onBitmapAvailable(Bitmap bitmap);
-}
diff --git a/src/com/android/gallery3d/ui/AbstractSlotRenderer.java b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
new file mode 100644
index 0000000..98eae56
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+
+public abstract class AbstractSlotRenderer implements SlotView.SlotRenderer {
+
+    private final ResourceTexture mVideoOverlay;
+    private final ResourceTexture mVideoPlayIcon;
+    private final NinePatchTexture mPanoramaBorder;
+    private final NinePatchTexture mFramePressed;
+    private final NinePatchTexture mFrameSelected;
+    private FadeOutTexture mFramePressedUp;
+
+    protected AbstractSlotRenderer(Context context) {
+        mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb);
+        mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play);
+        mPanoramaBorder = new NinePatchTexture(context, R.drawable.ic_pan_thumb);
+        mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
+        mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+    }
+
+    protected void drawContent(GLCanvas canvas,
+            Texture content, int width, int height, int rotation) {
+        canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+        if (rotation != 0) {
+            canvas.translate(width / 2, height / 2);
+            canvas.rotate(rotation, 0, 0, 1);
+            canvas.translate(-width / 2, -height / 2);
+            if (((rotation % 90) & 1) != 0) {
+                int temp = height;
+                height = width;
+                width = height;
+            }
+        }
+
+        // Fit the content into the box
+        float scale = Math.min(
+                (float) width / content.getWidth(),
+                (float) height / content.getHeight());
+        canvas.scale(scale, scale, 1);
+        content.draw(canvas, 0, 0);
+
+        canvas.restore();
+    }
+
+    protected void drawVideoOverlay(GLCanvas canvas, int width, int height) {
+        // Scale the video overlay to the height of the thumbnail and put it
+        // on the left side.
+        ResourceTexture v = mVideoOverlay;
+        float scale = (float) height / v.getHeight();
+        int w = Math.round(scale * v.getWidth());
+        int h = Math.round(scale * v.getHeight());
+        v.draw(canvas, 0, 0, w, h);
+
+        int s = Math.min(width, height) / 6;
+        mVideoPlayIcon.draw(canvas, (width - s) / 2, (height - s) / 2, s, s);
+    }
+
+    protected void drawPanoramaBorder(GLCanvas canvas, int width, int height) {
+        float scale = (float) width / mPanoramaBorder.getWidth();
+        int w = Math.round(scale * mPanoramaBorder.getWidth());
+        int h = Math.round(scale * mPanoramaBorder.getHeight());
+        // draw at the top
+        mPanoramaBorder.draw(canvas, 0, 0, w, h);
+        // draw at the bottom
+        mPanoramaBorder.draw(canvas, 0, height - h, w, h);
+    }
+
+    protected boolean isPressedUpFrameFinished() {
+        if (mFramePressedUp != null) {
+            if (mFramePressedUp.isAnimating()) {
+                return false;
+            } else {
+                mFramePressedUp = null;
+            }
+        }
+        return true;
+    }
+
+    protected void drawPressedUpFrame(GLCanvas canvas, int width, int height) {
+        if (mFramePressedUp == null) {
+            mFramePressedUp = new FadeOutTexture(mFramePressed);
+        }
+        drawFrame(canvas, mFramePressed.getPaddings(), mFramePressedUp, 0, 0, width, height);
+    }
+
+    protected void drawPressedFrame(GLCanvas canvas, int width, int height) {
+        drawFrame(canvas, mFramePressed.getPaddings(), mFramePressed, 0, 0, width, height);
+    }
+
+    protected void drawSelectedFrame(GLCanvas canvas, int width, int height) {
+        drawFrame(canvas, mFrameSelected.getPaddings(), mFrameSelected, 0, 0, width, height);
+    }
+
+    protected static void drawFrame(GLCanvas canvas, Rect padding, Texture frame,
+            int x, int y, int width, int height) {
+        frame.draw(canvas, x - padding.left, y - padding.top, width + padding.left + padding.right,
+                 height + padding.top + padding.bottom);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
index b96932e..746d41d 100644
--- a/src/com/android/gallery3d/ui/ActionModeHandler.java
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.nfc.NfcAdapter;
 import android.os.Handler;
 import android.view.ActionMode;
 import android.view.LayoutInflater;
@@ -28,8 +29,8 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.Button;
-import android.widget.ShareActionProvider;
 import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.ShareActionProvider;
 import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
 
 import com.android.gallery3d.R;
@@ -61,6 +62,7 @@
     private final GalleryActivity mActivity;
     private final MenuExecutor mMenuExecutor;
     private final SelectionManager mSelectionManager;
+    private final NfcAdapter mNfcAdapter;
     private Menu mMenu;
     private DropDownMenu mSelectionMenu;
     private ActionModeListener mListener;
@@ -74,6 +76,7 @@
         mSelectionManager = Utils.checkNotNull(selectionManager);
         mMenuExecutor = new MenuExecutor(activity, selectionManager);
         mMainHandler = new Handler(activity.getMainLooper());
+        mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
     }
 
     public ActionMode startActionMode() {
@@ -88,6 +91,7 @@
                 R.menu.selection);
         updateSelectionMenu();
         customMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+            @Override
             public boolean onMenuItemClick(MenuItem item) {
                 return onActionItemClicked(actionMode, item);
             }
@@ -103,25 +107,41 @@
         mListener = listener;
     }
 
+    @Override
     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-        boolean result;
-        if (mListener != null) {
-            result = mListener.onActionItemClicked(item);
-            if (result) {
-                mSelectionManager.leaveSelectionMode();
-                return result;
+        GLRoot root = mActivity.getGLRoot();
+        root.lockRenderThread();
+        try {
+            boolean result;
+            // Give listener a chance to process this command before it's routed to
+            // ActionModeHandler, which handles command only based on the action id.
+            // Sometimes the listener may have more background information to handle
+            // an action command.
+            if (mListener != null) {
+                result = mListener.onActionItemClicked(item);
+                if (result) {
+                    mSelectionManager.leaveSelectionMode();
+                    return result;
+                }
             }
+            ProgressListener listener = null;
+            String confirmMsg = null;
+            int action = item.getItemId();
+            if (action == R.id.action_import) {
+                listener = new ImportCompleteListener(mActivity);
+            } else if (item.getItemId() == R.id.action_delete) {
+                confirmMsg = mActivity.getResources().getQuantityString(
+                        R.plurals.delete_selection, mSelectionManager.getSelectedCount());
+            }
+            mMenuExecutor.onMenuClicked(item, confirmMsg, listener);
+            if (action == R.id.action_select_all) {
+                updateSupportedOperation();
+                updateSelectionMenu();
+            }
+        } finally {
+            root.unlockRenderThread();
         }
-        ProgressListener listener = null;
-        if (item.getItemId() == R.id.action_import) {
-            listener = new ImportCompleteListener(mActivity);
-        }
-        result = mMenuExecutor.onMenuClicked(item, listener);
-        if (item.getItemId() == R.id.action_select_all) {
-            updateSupportedOperation();
-            updateSelectionMenu();
-        }
-        return result;
+        return true;
     }
 
     private void updateSelectionMenu() {
@@ -173,55 +193,53 @@
     // We cannot expand it because MenuExecuter executes it based on
     // the selection set instead of the expanded result.
     // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
-    private void updateMenuOptions(JobContext jc) {
-        ArrayList<Path> paths = mSelectionManager.getSelected(false);
-
+    private int computeMenuOptions(JobContext jc) {
+        ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false);
+        if (unexpandedPaths.isEmpty()) {
+            // This happens when starting selection mode from overflow menu
+            // (instead of long press a media object)
+            return 0;
+        }
         int operation = MediaObject.SUPPORT_ALL;
         DataManager manager = mActivity.getDataManager();
         int type = 0;
-        for (Path path : paths) {
-            if (jc.isCancelled()) return;
+        for (Path path : unexpandedPaths) {
+            if (jc.isCancelled()) return 0;
             int support = manager.getSupportedOperations(path);
             type |= manager.getMediaType(path);
             operation &= support;
         }
 
-        final String mimeType = MenuExecutor.getMimeType(type);
-        if (paths.size() == 0) {
-            operation = 0;
-        } else if (paths.size() == 1) {
-            if (!GalleryUtils.isEditorAvailable((Context) mActivity, mimeType)) {
-                operation &= ~MediaObject.SUPPORT_EDIT;
-            }
-        } else {
-            operation &= SUPPORT_MULTIPLE_MASK;
+        switch (unexpandedPaths.size()) {
+            case 1:
+                final String mimeType = MenuExecutor.getMimeType(type);
+                if (!GalleryUtils.isEditorAvailable((Context) mActivity, mimeType)) {
+                    operation &= ~MediaObject.SUPPORT_EDIT;
+                }
+                break;
+            default:
+                operation &= SUPPORT_MULTIPLE_MASK;
         }
 
-        final int supportedOperation = operation;
-
-        mMainHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mMenuTask = null;
-                MenuExecutor.updateMenuOperation(mMenu, supportedOperation);
-            }
-        });
+        return operation;
     }
 
     // Share intent needs to expand the selection set so we can get URI of
     // each media item
-    private void updateSharingIntent(JobContext jc) {
-        if (mShareActionProvider == null) return;
-        ArrayList<Path> paths = mSelectionManager.getSelected(true);
-        if (paths.size() == 0) return;
-
+    private Intent computeSharingIntent(JobContext jc) {
+        ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true);
+        if (expandedPaths.size() == 0) {
+            if (mNfcAdapter != null) {
+                mNfcAdapter.setBeamPushUris(null, (Activity)mActivity);
+            }
+            return null;
+        }
         final ArrayList<Uri> uris = new ArrayList<Uri>();
-
         DataManager manager = mActivity.getDataManager();
         int type = 0;
-
         final Intent intent = new Intent();
-        for (Path path : paths) {
+        for (Path path : expandedPaths) {
+            if (jc.isCancelled()) return null;
             int support = manager.getSupportedOperations(path);
             type |= manager.getMediaType(path);
 
@@ -241,15 +259,17 @@
                 intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
             }
             intent.setType(mimeType);
-
-            mMainHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    Log.v(TAG, "Sharing intent is ready: action = " + intent.getAction());
-                    mShareActionProvider.setShareIntent(intent);
-                }
-            });
+            if (mNfcAdapter != null) {
+                mNfcAdapter.setBeamPushUris(uris.toArray(new Uri[uris.size()]),
+                        (Activity)mActivity);
+            }
+        } else {
+            if (mNfcAdapter != null) {
+                mNfcAdapter.setBeamPushUris(null, (Activity)mActivity);
+            }
         }
+
+        return intent;
     }
 
     public void updateSupportedOperation(Path path, boolean selected) {
@@ -258,21 +278,40 @@
     }
 
     public void updateSupportedOperation() {
+        // Interrupt previous unfinished task, mMenuTask is only accessed in main thread
         if (mMenuTask != null) {
             mMenuTask.cancel();
         }
 
+        updateSelectionMenu();
+
         // Disable share action until share intent is in good shape
-        if (mShareActionProvider != null) {
-            Log.v(TAG, "Disable sharing until intent is ready");
-            mShareActionProvider.setShareIntent(null);
-        }
+        final MenuItem item = mShareActionProvider != null ?
+                mMenu.findItem(R.id.action_share) : null;
+        final boolean supportShare = item != null;
+        if (supportShare) item.setEnabled(false);
 
         // Generate sharing intent and update supported operations in the background
+        // The task can take a long time and be canceled in the mean time.
         mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
-            public Void run(JobContext jc) {
-                updateMenuOptions(jc);
-                updateSharingIntent(jc);
+            public Void run(final JobContext jc) {
+                // Pass1: Deal with unexpanded media object list for menu operation.
+                final int operation = computeMenuOptions(jc);
+
+                // Pass2: Deal with expanded media object list for sharing operation.
+                final Intent intent = supportShare ? computeSharingIntent(jc) : null;
+                mMainHandler.post(new Runnable() {
+                    public void run() {
+                        mMenuTask = null;
+                        if (!jc.isCancelled()) {
+                            MenuExecutor.updateMenuOperation(mMenu, operation);
+                            if (supportShare) {
+                                item.setEnabled(true);
+                                mShareActionProvider.setShareIntent(intent);
+                            }
+                        }
+                    }
+                });
                 return null;
             }
         });
diff --git a/src/com/android/gallery3d/ui/AdaptiveBackground.java b/src/com/android/gallery3d/ui/AdaptiveBackground.java
deleted file mode 100644
index 42cb2cc..0000000
--- a/src/com/android/gallery3d/ui/AdaptiveBackground.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.LightingColorFilter;
-import android.graphics.Paint;
-
-import com.android.gallery3d.anim.FloatAnimation;
-
-public class AdaptiveBackground extends GLView {
-
-    private static final int BACKGROUND_WIDTH = 128;
-    private static final int BACKGROUND_HEIGHT = 64;
-    private static final int FILTERED_COLOR = 0xffaaaaaa;
-    private static final int ANIMATION_DURATION = 500;
-
-    private BasicTexture mOldBackground;
-    private BasicTexture mBackground;
-
-    private final Paint mPaint;
-    private Bitmap mPendingBitmap;
-    private final FloatAnimation mAnimation =
-            new FloatAnimation(0, 1, ANIMATION_DURATION);
-
-    public AdaptiveBackground() {
-        Paint paint = new Paint();
-        paint.setFilterBitmap(true);
-        paint.setColorFilter(new LightingColorFilter(FILTERED_COLOR, 0));
-        mPaint = paint;
-    }
-
-    public Bitmap getAdaptiveBitmap(Bitmap bitmap) {
-        Bitmap target = Bitmap.createBitmap(
-                BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(target);
-        int width = bitmap.getWidth();
-        int height = bitmap.getHeight();
-        int left = 0;
-        int top = 0;
-        if (width * BACKGROUND_HEIGHT > height * BACKGROUND_WIDTH) {
-            float scale = (float) BACKGROUND_HEIGHT / height;
-            canvas.scale(scale, scale);
-            left = (BACKGROUND_WIDTH - (int) (width * scale + 0.5)) / 2;
-        } else {
-            float scale = (float) BACKGROUND_WIDTH / width;
-            canvas.scale(scale, scale);
-            top = (BACKGROUND_HEIGHT - (int) (height * scale + 0.5)) / 2;
-        }
-        canvas.drawBitmap(bitmap, left, top, mPaint);
-        BoxBlurFilter.apply(target,
-                BoxBlurFilter.MODE_REPEAT, BoxBlurFilter.MODE_CLAMP);
-        return target;
-    }
-
-    private void startTransition(Bitmap bitmap) {
-        BitmapTexture texture = new BitmapTexture(bitmap);
-        if (mBackground == null) {
-            mBackground = texture;
-        } else {
-            if (mOldBackground != null) mOldBackground.recycle();
-            mOldBackground = mBackground;
-            mBackground = texture;
-            mAnimation.start();
-        }
-        invalidate();
-    }
-
-    public void setImage(Bitmap bitmap) {
-        if (mAnimation.isActive()) {
-            mPendingBitmap = bitmap;
-        } else {
-            startTransition(bitmap);
-        }
-    }
-
-    public void setScrollPosition(int position) {
-        if (mScrollX == position) return;
-        mScrollX = position;
-        invalidate();
-    }
-
-    @Override
-    protected void render(GLCanvas canvas) {
-        if (mBackground == null) return;
-
-        int height = getHeight();
-        float scale = (float) height / BACKGROUND_HEIGHT;
-        int width = (int) (BACKGROUND_WIDTH * scale + 0.5f);
-        int scroll = mScrollX;
-        int start = (scroll / width) * width;
-
-        if (mOldBackground == null) {
-            for (int i = start, n = scroll + getWidth(); i < n; i += width) {
-                mBackground.draw(canvas, i - scroll, 0, width, height);
-            }
-        } else {
-            boolean moreAnimation =
-                    mAnimation.calculate(canvas.currentAnimationTimeMillis());
-            float ratio = mAnimation.get();
-            for (int i = start, n = scroll + getWidth(); i < n; i += width) {
-                canvas.drawMixed(mOldBackground,
-                        mBackground, ratio, i - scroll, 0, width, height);
-            }
-            if (moreAnimation) {
-                invalidate();
-            } else if (mPendingBitmap != null) {
-                startTransition(mPendingBitmap);
-                mPendingBitmap = null;
-            }
-        }
-    }
-}
diff --git a/src/com/android/gallery3d/ui/AlbumLabelMaker.java b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
new file mode 100644
index 0000000..93b37ce
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.BitmapPool;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class AlbumLabelMaker {
+    private static final int FONT_COLOR_TITLE = Color.WHITE;
+    private static final int FONT_COLOR_COUNT = 0x80FFFFFF;  // 50% white
+
+    // We keep a border around the album label to prevent aliasing
+    private static final int BORDER_SIZE = 1;
+    private static final int BACKGROUND_COLOR = 0x60000000; // 36% Dark
+
+    private final AlbumSetSlotRenderer.LabelSpec mSpec;
+    private final TextPaint mTitlePaint;
+    private final TextPaint mCountPaint;
+    private final Context mContext;
+
+    private int mLabelWidth;
+    private BitmapPool mBitmapPool;
+
+    private final LazyLoadedBitmap mLocalSetIcon;
+    private final LazyLoadedBitmap mPicasaIcon;
+    private final LazyLoadedBitmap mCameraIcon;
+    private final LazyLoadedBitmap mMtpIcon;
+
+    public AlbumLabelMaker(Context context, AlbumSetSlotRenderer.LabelSpec spec) {
+        mContext = context;
+        mSpec = spec;
+        mTitlePaint = getTextPaint(spec.titleFontSize, FONT_COLOR_TITLE, false);
+        mCountPaint = getTextPaint(spec.countFontSize, FONT_COLOR_COUNT, true);
+
+        mLocalSetIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_folder);
+        mPicasaIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_picasa);
+        mCameraIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_camera);
+        mMtpIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_ptp);
+    }
+
+    public static int getBorderSize() {
+        return BORDER_SIZE;
+    }
+
+    private Bitmap getOverlayAlbumIcon(int sourceType) {
+        switch (sourceType) {
+            case DataSourceType.TYPE_CAMERA:
+                return mCameraIcon.get();
+            case DataSourceType.TYPE_LOCAL:
+                return mLocalSetIcon.get();
+            case DataSourceType.TYPE_MTP:
+                return mMtpIcon.get();
+            case DataSourceType.TYPE_PICASA:
+                return mPicasaIcon.get();
+        }
+        return null;
+    }
+
+    private static TextPaint getTextPaint(int textSize, int color, boolean isBold) {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(textSize);
+        paint.setAntiAlias(true);
+        paint.setColor(color);
+        paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);
+        if (isBold) {
+            paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+        }
+        return paint;
+    }
+
+    private class LazyLoadedBitmap {
+        private Bitmap mBitmap;
+        private int mResId;
+
+        public LazyLoadedBitmap(int resId) {
+            mResId = resId;
+        }
+
+        public synchronized Bitmap get() {
+            if (mBitmap == null) {
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+                mBitmap = BitmapFactory.decodeResource(
+                        mContext.getResources(), mResId, options);
+            }
+            return mBitmap;
+        }
+    }
+
+    public synchronized void setLabelWidth(int width) {
+        if (mLabelWidth == width) return;
+        mLabelWidth = width;
+        int borders = 2 * BORDER_SIZE;
+        mBitmapPool = new BitmapPool(
+                width + borders, mSpec.labelBackgroundHeight + borders, 16);
+    }
+
+    public ThreadPool.Job<Bitmap> requestLabel(
+            String title, String count, int sourceType) {
+        return new AlbumLabelJob(title, count, sourceType);
+    }
+
+    static void drawText(Canvas canvas,
+            int x, int y, String text, int lengthLimit, TextPaint p) {
+        // The TextPaint cannot be used concurrently
+        synchronized (p) {
+            text = TextUtils.ellipsize(
+                    text, p, lengthLimit, TextUtils.TruncateAt.END).toString();
+            canvas.drawText(text, x, y - p.getFontMetricsInt().ascent, p);
+        }
+    }
+
+    private class AlbumLabelJob implements ThreadPool.Job<Bitmap> {
+        private final String mTitle;
+        private final String mCount;
+        private final int mSourceType;
+
+        public AlbumLabelJob(String title, String count, int sourceType) {
+            mTitle = title;
+            mCount = count;
+            mSourceType = sourceType;
+        }
+
+        @Override
+        public Bitmap run(JobContext jc) {
+            AlbumSetSlotRenderer.LabelSpec s = mSpec;
+
+            String title = mTitle;
+            String count = mCount;
+            Bitmap icon = getOverlayAlbumIcon(mSourceType);
+
+            Bitmap bitmap;
+            int labelWidth;
+
+            synchronized (this) {
+                labelWidth = mLabelWidth;
+                bitmap = mBitmapPool.getBitmap();
+            }
+
+            if (bitmap == null) {
+                int borders = 2 * BORDER_SIZE;
+                bitmap = Bitmap.createBitmap(labelWidth + borders,
+                        s.labelBackgroundHeight + borders, Config.ARGB_8888);
+            }
+
+            Canvas canvas = new Canvas(bitmap);
+            canvas.clipRect(BORDER_SIZE, BORDER_SIZE,
+                    bitmap.getWidth() - BORDER_SIZE,
+                    bitmap.getHeight() - BORDER_SIZE);
+            canvas.drawColor(BACKGROUND_COLOR, PorterDuff.Mode.SRC);
+
+            canvas.translate(BORDER_SIZE, BORDER_SIZE);
+
+            // draw title
+            if (jc.isCancelled()) return null;
+            int x = s.leftMargin;
+            int y = s.titleOffset;
+            drawText(canvas, x, y, title, labelWidth - s.leftMargin, mTitlePaint);
+
+            // draw the count
+            if (jc.isCancelled()) return null;
+            if (icon != null) x = s.iconSize;
+            y += s.titleFontSize + s.countOffset;
+            drawText(canvas, x, y, count,
+                    labelWidth - s.leftMargin - s.iconSize, mCountPaint);
+
+            // draw the icon
+            if (icon != null) {
+                if (jc.isCancelled()) return null;
+                float scale = (float) s.iconSize / icon.getWidth();
+                canvas.translate(0, bitmap.getHeight()
+                        - Math.round(scale * icon.getHeight()));
+                canvas.scale(scale, scale);
+                canvas.drawBitmap(icon, 0, 0, null);
+            }
+
+            return bitmap;
+        }
+    }
+
+    public void recycleLabel(Bitmap label) {
+        mBitmapPool.recycle(label);
+    }
+
+    public void clearRecycledLabels() {
+        if (mBitmapPool != null) mBitmapPool.clear();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
index 87ff557..6561a23 100644
--- a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -1,4 +1,4 @@
-/*
+/*T
  * Copyright (C) 2010 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,37 +17,33 @@
 package com.android.gallery3d.ui;
 
 import android.graphics.Bitmap;
-import android.graphics.Color;
 import android.os.Message;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.app.AlbumSetDataLoader;
 import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataSourceType;
 import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.MediaSetUtils;
 import com.android.gallery3d.util.ThreadPool;
 
-public class AlbumSetSlidingWindow implements AlbumSetView.ModelListener {
-    private static final String TAG = "GallerySlidingWindow";
-    private static final int MSG_LOAD_BITMAP_DONE = 0;
-    private static final int PLACEHOLDER_COLOR = 0xFF222222;
+public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener {
+    private static final String TAG = "AlbumSetSlidingWindow";
+    private static final int MSG_UPDATE_ALBUM_ENTRY = 1;
 
     public static interface Listener {
         public void onSizeChanged(int size);
-        public void onContentInvalidated();
-        public void onWindowContentChanged(
-                int slot, AlbumSetItem old, AlbumSetItem update);
+        public void onContentChanged();
     }
 
-    private final AlbumSetView.Model mSource;
+    private final AlbumSetDataLoader mSource;
     private int mSize;
-    private final AlbumSetView.LabelSpec mLabelSpec;
 
     private int mContentStart = 0;
     private int mContentEnd = 0;
@@ -57,61 +53,71 @@
 
     private Listener mListener;
 
-    private final MyAlbumSetItem mData[];
-    private SelectionDrawer mSelectionDrawer;
-    private final ColorTexture mWaitLoadingTexture;
-
+    private final AlbumSetEntry mData[];
     private final SynchronizedHandler mHandler;
     private final ThreadPool mThreadPool;
+    private final AlbumLabelMaker mLabelMaker;
+    private final String mLoadingText;
+    private final TextureUploader mTextureUploader;
 
     private int mActiveRequestCount = 0;
-    private final String mLoadingLabel;
     private boolean mIsActive = false;
+    private BitmapTexture mLoadingLabel;
 
-    private static class MyAlbumSetItem extends AlbumSetItem {
+    private int mSlotWidth;
+
+    public static class AlbumSetEntry {
+        public MediaSet album;
+        public MediaItem coverItem;
+        public Texture content;
+        public BitmapTexture labelTexture;
+        public BitmapTexture bitmapTexture;
         public Path setPath;
+        public String title;
+        public int totalCount;
         public int sourceType;
         public int cacheFlag;
         public int cacheStatus;
+        public int rotation;
+        public int mediaType;
+        public boolean isPanorama;
+        public boolean isWaitLoadingDisplayed;
+        public long setDataVersion;
+        public long coverDataVersion;
+        private BitmapLoader labelLoader;
+        private BitmapLoader coverLoader;
     }
 
     public AlbumSetSlidingWindow(GalleryActivity activity,
-            AlbumSetView.LabelSpec labelSpec, SelectionDrawer drawer,
-            AlbumSetView.Model source, int cacheSize) {
+            AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) {
         source.setModelListener(this);
-        mLabelSpec = labelSpec;
-        mLoadingLabel = activity.getAndroidContext().getString(R.string.loading);
         mSource = source;
-        mSelectionDrawer = drawer;
-        mData = new MyAlbumSetItem[cacheSize];
+        mData = new AlbumSetEntry[cacheSize];
         mSize = source.size();
+        mThreadPool = activity.getThreadPool();
 
-        mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR);
-        mWaitLoadingTexture.setSize(1, 1);
+        mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec);
+        mLoadingText = activity.getAndroidContext().getString(R.string.loading);
+        mTextureUploader = new TextureUploader(activity.getGLRoot());
 
         mHandler = new SynchronizedHandler(activity.getGLRoot()) {
             @Override
             public void handleMessage(Message message) {
-                Utils.assertTrue(message.what == MSG_LOAD_BITMAP_DONE);
-                ((GalleryDisplayItem) message.obj).onLoadBitmapDone();
+                Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY);
+                ((EntryUpdater) message.obj).updateEntry();
             }
         };
-
-        mThreadPool = activity.getThreadPool();
-    }
-
-    public void setSelectionDrawer(SelectionDrawer drawer) {
-        mSelectionDrawer = drawer;
     }
 
     public void setListener(Listener listener) {
         mListener = listener;
     }
 
-    public AlbumSetItem get(int slotIndex) {
-        Utils.assertTrue(isActiveSlot(slotIndex),
-                "invalid slot: %s outsides (%s, %s)",
-                slotIndex, mActiveStart, mActiveEnd);
+    public AlbumSetEntry get(int slotIndex) {
+        if (!isActiveSlot(slotIndex)) {
+            Utils.fail("invalid slot: %s outsides (%s, %s)",
+                    slotIndex, mActiveStart, mActiveEnd);
+        }
         return mData[slotIndex % mData.length];
     }
 
@@ -155,21 +161,23 @@
     }
 
     public void setActiveWindow(int start, int end) {
-        Utils.assertTrue(
-                start <= end && end - start <= mData.length && end <= mSize,
-                "start = %s, end = %s, length = %s, size = %s",
-                start, end, mData.length, mSize);
+        if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+            Utils.fail("start = %s, end = %s, length = %s, size = %s",
+                    start, end, mData.length, mSize);
+        }
 
-        AlbumSetItem data[] = mData;
-
+        AlbumSetEntry data[] = mData;
         mActiveStart = start;
         mActiveEnd = end;
-
         int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
                 0, Math.max(0, mSize - data.length));
         int contentEnd = Math.min(contentStart + data.length, mSize);
         setContentWindow(contentStart, contentEnd);
-        if (mIsActive) updateAllImageRequests();
+
+        if (mIsActive) {
+            updateTextureUploadQueue();
+            updateAllImageRequests();
+        }
     }
 
     // We would like to request non active slots in the following order:
@@ -197,129 +205,139 @@
 
     private void requestImagesInSlot(int slotIndex) {
         if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
-        AlbumSetItem items = mData[slotIndex % mData.length];
-        for (DisplayItem item : items.covers) {
-            ((GalleryDisplayItem) item).requestImage();
-        }
+        AlbumSetEntry entry = mData[slotIndex % mData.length];
+        if (entry.coverLoader != null) entry.coverLoader.startLoad();
+        if (entry.labelLoader != null) entry.labelLoader.startLoad();
     }
 
     private void cancelImagesInSlot(int slotIndex) {
         if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
-        AlbumSetItem items = mData[slotIndex % mData.length];
-        for (DisplayItem item : items.covers) {
-            ((GalleryDisplayItem) item).cancelImageRequest();
-        }
+        AlbumSetEntry entry = mData[slotIndex % mData.length];
+        if (entry.coverLoader != null) entry.coverLoader.cancelLoad();
+        if (entry.labelLoader != null) entry.labelLoader.cancelLoad();
+    }
+
+    private static long getDataVersion(MediaObject object) {
+        return object == null
+                ? MediaSet.INVALID_DATA_VERSION
+                : object.getDataVersion();
     }
 
     private void freeSlotContent(int slotIndex) {
-        AlbumSetItem data[] = mData;
-        int index = slotIndex % data.length;
-        AlbumSetItem original = data[index];
-        if (original != null) {
-            data[index] = null;
-            for (DisplayItem item : original.covers) {
-                ((GalleryDisplayItem) item).recycle();
-            }
-        }
+        AlbumSetEntry entry = mData[slotIndex % mData.length];
+        if (entry.coverLoader != null) entry.coverLoader.recycle();
+        if (entry.labelLoader != null) entry.labelLoader.recycle();
+        if (entry.labelTexture != null) entry.labelTexture.recycle();
+        if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+        mData[slotIndex % mData.length] = null;
     }
 
-    private long getMediaSetDataVersion(MediaSet set) {
-        return set == null
-                ? MediaSet.INVALID_DATA_VERSION
-                : set.getDataVersion();
+    private boolean isLabelChanged(
+            AlbumSetEntry entry, String title, int totalCount, int sourceType) {
+        return !Utils.equals(entry.title, title)
+                || entry.totalCount != totalCount
+                || entry.sourceType != sourceType;
+    }
+
+    private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) {
+        MediaSet album = mSource.getMediaSet(slotIndex);
+        MediaItem cover = mSource.getCoverItem(slotIndex);
+        int totalCount = mSource.getTotalCount(slotIndex);
+
+        entry.album = album;
+        entry.setDataVersion = getDataVersion(album);
+        entry.cacheFlag = identifyCacheFlag(album);
+        entry.cacheStatus = identifyCacheStatus(album);
+        entry.setPath = (album == null) ? null : album.getPath();
+
+        String title = (album == null) ? "" : Utils.ensureNotNull(album.getName());
+        int sourceType = DataSourceType.identifySourceType(album);
+        if (isLabelChanged(entry, title, totalCount, sourceType)) {
+            entry.title = title;
+            entry.totalCount = totalCount;
+            entry.sourceType = sourceType;
+            if (entry.labelLoader != null) {
+                entry.labelLoader.recycle();
+                entry.labelLoader = null;
+                entry.labelTexture = null;
+            }
+            if (album != null) {
+                entry.labelLoader = new AlbumLabelLoader(
+                        slotIndex, title, totalCount, sourceType);
+            }
+        }
+
+        entry.coverItem = cover;
+        if (getDataVersion(cover) != entry.coverDataVersion) {
+            entry.coverDataVersion = getDataVersion(cover);
+            entry.isPanorama = GalleryUtils.isPanorama(cover);
+            entry.rotation = (cover == null) ? 0 : cover.getRotation();
+            entry.mediaType = (cover == null) ? 0 : cover.getMediaType();
+            if (entry.coverLoader != null) {
+                entry.coverLoader.recycle();
+                entry.coverLoader = null;
+                entry.bitmapTexture = null;
+                entry.content = null;
+            }
+            if (cover != null) {
+                entry.coverLoader = new AlbumCoverLoader(slotIndex, cover);
+            }
+        }
     }
 
     private void prepareSlotContent(int slotIndex) {
-        MediaSet set = mSource.getMediaSet(slotIndex);
-
-        MyAlbumSetItem item = new MyAlbumSetItem();
-        MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
-        item.covers = new GalleryDisplayItem[coverItems.length];
-        item.sourceType = identifySourceType(set);
-        item.cacheFlag = identifyCacheFlag(set);
-        item.cacheStatus = identifyCacheStatus(set);
-        item.setPath = set == null ? null : set.getPath();
-
-        for (int i = 0; i < coverItems.length; ++i) {
-            item.covers[i] = new GalleryDisplayItem(slotIndex, i, coverItems[i]);
-        }
-        item.labelItem = new LabelDisplayItem(slotIndex);
-        item.setDataVersion = getMediaSetDataVersion(set);
-        mData[slotIndex % mData.length] = item;
+        AlbumSetEntry entry = new AlbumSetEntry();
+        updateAlbumSetEntry(entry, slotIndex);
+        mData[slotIndex % mData.length] = entry;
     }
 
-    private boolean isCoverItemsChanged(int slotIndex) {
-        AlbumSetItem original = mData[slotIndex % mData.length];
-        if (original == null) return true;
-        MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
-
-        if (original.covers.length != coverItems.length) return true;
-        for (int i = 0, n = coverItems.length; i < n; ++i) {
-            GalleryDisplayItem g = (GalleryDisplayItem) original.covers[i];
-            if (g.mDataVersion != coverItems[i].getDataVersion()) return true;
-        }
-        return false;
+    private static boolean startLoadBitmap(BitmapLoader loader) {
+        if (loader == null) return false;
+        loader.startLoad();
+        return loader.isRequestInProgress();
     }
 
-    private void updateSlotContent(final int slotIndex) {
-
-        MyAlbumSetItem data[] = mData;
-        int pos = slotIndex % data.length;
-        MyAlbumSetItem original = data[pos];
-
-        if (!isCoverItemsChanged(slotIndex)) {
-            MediaSet set = mSource.getMediaSet(slotIndex);
-            original.sourceType = identifySourceType(set);
-            original.cacheFlag = identifyCacheFlag(set);
-            original.cacheStatus = identifyCacheStatus(set);
-            original.setPath = set == null ? null : set.getPath();
-            ((LabelDisplayItem) original.labelItem).updateContent();
-            if (mListener != null) mListener.onContentInvalidated();
-            return;
+    private void uploadBackgroundTextureInSlot(int index) {
+        if (index < mContentStart || index >= mContentEnd) return;
+        AlbumSetEntry entry = mData[index % mData.length];
+        if (entry.bitmapTexture != null) {
+            mTextureUploader.addBgTexture(entry.bitmapTexture);
         }
-
-        prepareSlotContent(slotIndex);
-        AlbumSetItem update = data[pos];
-
-        if (mListener != null && isActiveSlot(slotIndex)) {
-            mListener.onWindowContentChanged(slotIndex, original, update);
+        if (entry.labelTexture != null) {
+            mTextureUploader.addBgTexture(entry.labelTexture);
         }
-        if (original != null) {
-            for (DisplayItem item : original.covers) {
-                ((GalleryDisplayItem) item).recycle();
+    }
+
+    private void updateTextureUploadQueue() {
+        if (!mIsActive) return;
+        mTextureUploader.clear();
+
+        // Upload foreground texture
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            AlbumSetEntry entry = mData[i % mData.length];
+            if (entry.bitmapTexture != null) {
+                mTextureUploader.addFgTexture(entry.bitmapTexture);
+            }
+            if (entry.labelTexture != null) {
+                mTextureUploader.addFgTexture(entry.labelTexture);
             }
         }
-    }
 
-    private void notifySlotChanged(int slotIndex) {
-        // If the updated content is not cached, ignore it
-        if (slotIndex < mContentStart || slotIndex >= mContentEnd) {
-            Log.w(TAG, String.format(
-                    "invalid update: %s is outside (%s, %s)",
-                    slotIndex, mContentStart, mContentEnd) );
-            return;
-        }
-        updateSlotContent(slotIndex);
-        boolean isActiveSlot = isActiveSlot(slotIndex);
-        if (mActiveRequestCount == 0 || isActiveSlot) {
-            for (DisplayItem item : mData[slotIndex % mData.length].covers) {
-                GalleryDisplayItem galleryItem = (GalleryDisplayItem) item;
-                galleryItem.requestImage();
-                if (isActiveSlot && galleryItem.isRequestInProgress()) {
-                    ++mActiveRequestCount;
-                }
-            }
+        // add background textures
+        int range = Math.max(
+                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+        for (int i = 0; i < range; ++i) {
+            uploadBackgroundTextureInSlot(mActiveEnd + i);
+            uploadBackgroundTextureInSlot(mActiveStart - i - 1);
         }
     }
 
     private void updateAllImageRequests() {
         mActiveRequestCount = 0;
         for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
-            for (DisplayItem item : mData[i % mData.length].covers) {
-                GalleryDisplayItem coverItem = (GalleryDisplayItem) item;
-                coverItem.requestImage();
-                if (coverItem.isRequestInProgress()) ++mActiveRequestCount;
-            }
+            AlbumSetEntry entry = mData[i % mData.length];
+            if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount;
+            if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount;
         }
         if (mActiveRequestCount == 0) {
             requestNonactiveImages();
@@ -328,143 +346,116 @@
         }
     }
 
-    private class GalleryDisplayItem extends AbstractDisplayItem
-            implements FutureListener<Bitmap> {
-        private Future<Bitmap> mFuture;
-        private final int mSlotIndex;
-        private final int mCoverIndex;
-        private final int mMediaType;
-        private Texture mContent;
-        private final long mDataVersion;
-        private final boolean mIsPanorama;
-        private boolean mWaitLoadingDisplayed;
-
-        public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) {
-            super(item);
-            mSlotIndex = slotIndex;
-            mCoverIndex = coverIndex;
-            mMediaType = item.getMediaType();
-            mDataVersion = item.getDataVersion();
-            mIsPanorama = GalleryUtils.isPanorama(item);
-            updateContent(mWaitLoadingTexture);
-        }
-
-        @Override
-        protected void onBitmapAvailable(Bitmap bitmap) {
-            if (isActiveSlot(mSlotIndex)) {
-                --mActiveRequestCount;
-                if (mActiveRequestCount == 0) requestNonactiveImages();
-            }
-            if (bitmap != null) {
-                BitmapTexture texture = new BitmapTexture(bitmap, true);
-                texture.setThrottled(true);
-                if (mWaitLoadingDisplayed) {
-                    updateContent(new FadeInTexture(PLACEHOLDER_COLOR, texture));
-                } else {
-                    updateContent(texture);
-                }
-                if (mListener != null) mListener.onContentInvalidated();
-            }
-        }
-
-        private void updateContent(Texture content) {
-            mContent = content;
-        }
-
-        @Override
-        public int render(GLCanvas canvas, int pass) {
-            // Fit the content into the box
-            int width = mContent.getWidth();
-            int height = mContent.getHeight();
-
-            float scalex = mBoxWidth / (float) width;
-            float scaley = mBoxHeight / (float) height;
-            float scale = Math.min(scalex, scaley);
-
-            width = (int) Math.floor(width * scale);
-            height = (int) Math.floor(height * scale);
-
-            // Now draw it
-            int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
-            int cacheFlag = MediaSet.CACHE_FLAG_NO;
-            int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED;
-            MyAlbumSetItem set = mData[mSlotIndex % mData.length];
-            Path path = set.setPath;
-            if (mCoverIndex == 0) {
-                sourceType = set.sourceType;
-                cacheFlag = set.cacheFlag;
-                cacheStatus = set.cacheStatus;
-            }
-
-            mSelectionDrawer.draw(canvas, mContent, width, height,
-                    getRotation(), path, sourceType, mMediaType,
-                    mIsPanorama, mLabelSpec.labelBackgroundHeight,
-                    cacheFlag == MediaSet.CACHE_FLAG_FULL,
-                    (cacheFlag == MediaSet.CACHE_FLAG_FULL)
-                    && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL));
-
-            if (mContent == mWaitLoadingTexture) {
-                mWaitLoadingDisplayed = true;
-            }
-
-            if ((mContent instanceof FadeInTexture) &&
-                    ((FadeInTexture) mContent).isAnimating()) {
-                return RENDER_MORE_FRAME;
-            } else {
-                return 0;
-            }
-        }
-
-        @Override
-        public void startLoadBitmap() {
-            mFuture = mThreadPool.submit(mMediaItem.requestImage(
-                    MediaItem.TYPE_MICROTHUMBNAIL), this);
-        }
-
-        @Override
-        public void cancelLoadBitmap() {
-            mFuture.cancel();
-        }
-
-        @Override
-        public void onFutureDone(Future<Bitmap> future) {
-            mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
-        }
-
-        private void onLoadBitmapDone() {
-            Future<Bitmap> future = mFuture;
-            mFuture = null;
-            updateImage(future.get(), future.isCancelled());
-        }
-
-        @Override
-        public String toString() {
-            return String.format("GalleryDisplayItem(%s, %s)", mSlotIndex, mCoverIndex);
+    @Override
+    public void onSizeChanged(int size) {
+        if (mIsActive && mSize != size) {
+            mSize = size;
+            if (mListener != null) mListener.onSizeChanged(mSize);
+            if (mContentEnd > mSize) mContentEnd = mSize;
+            if (mActiveEnd > mSize) mActiveEnd = mSize;
         }
     }
 
-    private static int identifySourceType(MediaSet set) {
-        if (set == null) {
-            return SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
+    @Override
+    public void onContentChanged(int index) {
+        if (!mIsActive) {
+            // paused, ignore slot changed event
+            return;
         }
 
-        Path path = set.getPath();
-        if (MediaSetUtils.isCameraSource(path)) {
-            return SelectionDrawer.DATASOURCE_TYPE_CAMERA;
+        // If the updated content is not cached, ignore it
+        if (index < mContentStart || index >= mContentEnd) {
+            Log.w(TAG, String.format(
+                    "invalid update: %s is outside (%s, %s)",
+                    index, mContentStart, mContentEnd) );
+            return;
         }
 
-        int type = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
-        String prefix = path.getPrefix();
+        AlbumSetEntry entry = mData[index % mData.length];
+        updateAlbumSetEntry(entry, index);
+        updateAllImageRequests();
+        updateTextureUploadQueue();
+        if (mListener != null && isActiveSlot(index)) {
+            mListener.onContentChanged();
+        }
+    }
 
-        if (prefix.equals("picasa")) {
-            type = SelectionDrawer.DATASOURCE_TYPE_PICASA;
-        } else if (prefix.equals("local") || prefix.equals("merge")) {
-            type = SelectionDrawer.DATASOURCE_TYPE_LOCAL;
-        } else if (prefix.equals("mtp")) {
-            type = SelectionDrawer.DATASOURCE_TYPE_MTP;
+    public BitmapTexture getLoadingTexture() {
+        if (mLoadingLabel == null) {
+            Bitmap bitmap = mLabelMaker.requestLabel(
+                    mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED)
+                    .run(ThreadPool.JOB_CONTEXT_STUB);
+            mLoadingLabel = new BitmapTexture(bitmap);
+            mLoadingLabel.setOpaque(false);
+        }
+        return mLoadingLabel;
+    }
+
+    public void pause() {
+        mIsActive = false;
+        mTextureUploader.clear();
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            freeSlotContent(i);
+        }
+        mLabelMaker.clearRecycledLabels();
+    }
+
+    public void resume() {
+        mIsActive = true;
+        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+            prepareSlotContent(i);
+        }
+        updateAllImageRequests();
+    }
+
+    private static interface EntryUpdater {
+        public void updateEntry();
+    }
+
+    private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater {
+        private MediaItem mMediaItem;
+        private final int mSlotIndex;
+
+        public AlbumCoverLoader(int slotIndex, MediaItem item) {
+            mSlotIndex = slotIndex;
+            mMediaItem = item;
         }
 
-        return type;
+        @Override
+        protected void recycleBitmap(Bitmap bitmap) {
+            MediaItem.getMicroThumbPool().recycle(bitmap);
+        }
+
+        @Override
+        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+            return mThreadPool.submit(mMediaItem.requestImage(
+                    MediaItem.TYPE_MICROTHUMBNAIL), l);
+        }
+
+        @Override
+        protected void onLoadComplete(Bitmap bitmap) {
+            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+        }
+
+        @Override
+        public void updateEntry() {
+            Bitmap bitmap = getBitmap();
+            if (bitmap == null) return; // error or recycled
+
+            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+            BitmapTexture texture = new BitmapTexture(bitmap);
+            entry.bitmapTexture = texture;
+            entry.content = texture;
+
+            if (isActiveSlot(mSlotIndex)) {
+                mTextureUploader.addFgTexture(texture);
+                --mActiveRequestCount;
+                if (mActiveRequestCount == 0) requestNonactiveImages();
+                if (mListener != null) mListener.onContentChanged();
+            } else {
+                mTextureUploader.addBgTexture(texture);
+            }
+        }
     }
 
     private static int identifyCacheFlag(MediaSet set) {
@@ -485,102 +476,79 @@
         return set.getCacheStatus();
     }
 
-    private class LabelDisplayItem extends DisplayItem {
-        private static final int FONT_COLOR_TITLE = Color.WHITE;
-        private static final int FONT_COLOR_COUNT = 0x80FFFFFF;  // 50% white
-
-        private StringTexture mTextureTitle;
-        private StringTexture mTextureCount;
-        private String mTitle;
-        private String mCount;
-        private int mLastWidth;
+    private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater {
         private final int mSlotIndex;
-        private boolean mHasIcon;
+        private final String mTitle;
+        private final int mTotalCount;
+        private final int mSourceType;
 
-        public LabelDisplayItem(int slotIndex) {
+        public AlbumLabelLoader(
+                int slotIndex, String title, int totalCount, int sourceType) {
             mSlotIndex = slotIndex;
-        }
-
-        public boolean updateContent() {
-            String title = mLoadingLabel;
-            String count = "";
-            MediaSet set = mSource.getMediaSet(mSlotIndex);
-            if (set != null) {
-                title = Utils.ensureNotNull(set.getName());
-                count = "" + set.getTotalMediaItemCount();
-            }
-            if (Utils.equals(title, mTitle)
-                    && Utils.equals(count, mCount)
-                    && Utils.equals(mBoxWidth, mLastWidth)) {
-                    return false;
-            }
             mTitle = title;
-            mCount = count;
-            mLastWidth = mBoxWidth;
-            mHasIcon = (identifySourceType(set) !=
-                    SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED);
-
-            AlbumSetView.LabelSpec s = mLabelSpec;
-            mTextureTitle = StringTexture.newInstance(
-                    title, s.titleFontSize, FONT_COLOR_TITLE,
-                    mBoxWidth - s.leftMargin, false);
-            mTextureCount = StringTexture.newInstance(
-                    count, s.countFontSize, FONT_COLOR_COUNT,
-                    mBoxWidth - s.leftMargin, true);
-
-            return true;
+            mTotalCount = totalCount;
+            mSourceType = sourceType;
         }
 
         @Override
-        public int render(GLCanvas canvas, int pass) {
-            if (mBoxWidth != mLastWidth) {
-                updateContent();
+        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+            return mThreadPool.submit(mLabelMaker.requestLabel(
+                    mTitle, String.valueOf(mTotalCount), mSourceType), l);
+        }
+
+        @Override
+        protected void recycleBitmap(Bitmap bitmap) {
+            mLabelMaker.recycleLabel(bitmap);
+        }
+
+        @Override
+        protected void onLoadComplete(Bitmap bitmap) {
+            mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+        }
+
+        @Override
+        public void updateEntry() {
+            Bitmap bitmap = getBitmap();
+            if (bitmap == null) return; // Error or recycled
+
+            AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+            BitmapTexture texture = new BitmapTexture(bitmap);
+            texture.setOpaque(false);
+            entry.labelTexture = texture;
+
+            if (isActiveSlot(mSlotIndex)) {
+                mTextureUploader.addFgTexture(texture);
+                --mActiveRequestCount;
+                if (mActiveRequestCount == 0) requestNonactiveImages();
+                if (mListener != null) mListener.onContentChanged();
+            } else {
+                mTextureUploader.addBgTexture(texture);
             }
-
-            AlbumSetView.LabelSpec s = mLabelSpec;
-            int x = -mBoxWidth / 2;
-            int y = (mBoxHeight + 1) / 2 - s.labelBackgroundHeight;
-            y += s.titleOffset;
-            mTextureTitle.draw(canvas, x + s.leftMargin, y);
-            y += s.titleFontSize + s.countOffset;
-            x += mHasIcon ? s.iconSize : s.leftMargin;
-            mTextureCount.draw(canvas, x, y);
-            return 0;
-        }
-
-        @Override
-        public long getIdentity() {
-            return System.identityHashCode(this);
         }
     }
 
-    public void onSizeChanged(int size) {
-        if (mIsActive && mSize != size) {
-            mSize = size;
-            if (mListener != null) mListener.onSizeChanged(mSize);
-        }
-    }
+    public void onSlotSizeChanged(int width, int height) {
+        if (mSlotWidth == width) return;
 
-    public void onWindowContentChanged(int index) {
-        if (!mIsActive) {
-            // paused, ignore slot changed event
-            return;
-        }
-        notifySlotChanged(index);
-    }
+        mSlotWidth = width;
+        mLoadingLabel = null;
+        mLabelMaker.setLabelWidth(mSlotWidth);
 
-    public void pause() {
-        mIsActive = false;
+        if (!mIsActive) return;
+
         for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
-            freeSlotContent(i);
-        }
-    }
-
-    public void resume() {
-        mIsActive = true;
-        for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
-            prepareSlotContent(i);
+            AlbumSetEntry entry = mData[i % mData.length];
+            if (entry.labelLoader != null) {
+                entry.labelLoader.recycle();
+                entry.labelLoader = null;
+                entry.labelTexture = null;
+            }
+            if (entry.album != null) {
+                entry.labelLoader = new AlbumLabelLoader(i,
+                        entry.title, entry.totalCount, entry.sourceType);
+            }
         }
         updateAllImageRequests();
+        updateTextureUploadQueue();
     }
 }
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
new file mode 100644
index 0000000..cca8d40
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+
+import com.android.gallery3d.app.AlbumSetDataLoader;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class AlbumSetSlotRenderer extends AbstractSlotRenderer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumSetView";
+    private static final int CACHE_SIZE = 96;
+    private static final int PLACEHOLDER_COLOR = 0xFF222222;
+
+    private final ColorTexture mWaitLoadingTexture;
+    private final GalleryActivity mActivity;
+    private final SelectionManager mSelectionManager;
+    protected final LabelSpec mLabelSpec;
+
+    protected AlbumSetSlidingWindow mDataWindow;
+    private SlotView mSlotView;
+
+    private int mPressedIndex = -1;
+    private boolean mAnimatePressedUp;
+    private Path mHighlightItemPath = null;
+    private boolean mInSelectionMode;
+
+    public static class LabelSpec {
+        public int labelBackgroundHeight;
+        public int titleOffset;
+        public int countOffset;
+        public int titleFontSize;
+        public int countFontSize;
+        public int leftMargin;
+        public int iconSize;
+    }
+
+    public AlbumSetSlotRenderer(GalleryActivity activity, SelectionManager selectionManager,
+            SlotView slotView, LabelSpec labelSpec) {
+        super ((Context) activity);
+        mActivity = activity;
+        mSelectionManager = selectionManager;
+        mSlotView = slotView;
+        mLabelSpec = labelSpec;
+
+        mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR);
+        mWaitLoadingTexture.setSize(1, 1);
+
+        Context context = activity.getAndroidContext();
+    }
+
+    public void setPressedIndex(int index) {
+        if (mPressedIndex == index) return;
+        mPressedIndex = index;
+        mSlotView.invalidate();
+    }
+
+    public void setPressedUp() {
+        if (mPressedIndex == -1) return;
+        mAnimatePressedUp = true;
+        mSlotView.invalidate();
+    }
+
+    public void setHighlightItemPath(Path path) {
+        if (mHighlightItemPath == path) return;
+        mHighlightItemPath = path;
+        mSlotView.invalidate();
+    }
+
+    public void setModel(AlbumSetDataLoader model) {
+        if (mDataWindow != null) {
+            mDataWindow.setListener(null);
+            mDataWindow = null;
+            mSlotView.setSlotCount(0);
+        }
+        if (model != null) {
+            mDataWindow = new AlbumSetSlidingWindow(
+                    mActivity, model, mLabelSpec, CACHE_SIZE);
+            mDataWindow.setListener(new MyCacheListener());
+            mSlotView.setSlotCount(mDataWindow.size());
+        }
+    }
+
+    private static Texture checkTexture(Texture texture) {
+        return ((texture instanceof UploadedTexture)
+                && ((UploadedTexture) texture).isUploading())
+                ? null
+                : texture;
+    }
+
+    @Override
+    public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        AlbumSetEntry entry = mDataWindow.get(index);
+        int renderRequestFlags = 0;
+        renderRequestFlags |= renderContent(canvas, entry, width, height);
+        renderRequestFlags |= renderLabel(canvas, entry, width, height);
+        renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+        return renderRequestFlags;
+    }
+
+    protected int renderOverlay(
+            GLCanvas canvas, int index, AlbumSetEntry entry, int width, int height) {
+        int renderRequestFlags = 0;
+        if (mPressedIndex == index) {
+            if (mAnimatePressedUp) {
+                drawPressedUpFrame(canvas, width, height);
+                renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+                if (isPressedUpFrameFinished()) {
+                    mAnimatePressedUp = false;
+                    mPressedIndex = -1;
+                }
+            } else {
+                drawPressedFrame(canvas, width, height);
+            }
+        } else if ((mHighlightItemPath != null) && (mHighlightItemPath == entry.setPath)) {
+            drawSelectedFrame(canvas, width, height);
+        } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.setPath)) {
+            drawSelectedFrame(canvas, width, height);
+        }
+        return renderRequestFlags;
+    }
+
+    protected int renderContent(
+            GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+        int renderRequestFlags = 0;
+
+        Texture content = checkTexture(entry.content);
+        if (content == null) {
+            content = mWaitLoadingTexture;
+            entry.isWaitLoadingDisplayed = true;
+        } else if (entry.isWaitLoadingDisplayed) {
+            entry.isWaitLoadingDisplayed = false;
+            content = new FadeInTexture(PLACEHOLDER_COLOR, entry.bitmapTexture);
+            entry.content = content;
+        }
+        drawContent(canvas, content, width, height, entry.rotation);
+        if ((content instanceof FadeInTexture) &&
+                ((FadeInTexture) content).isAnimating()) {
+            renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+        }
+
+        if (entry.mediaType == MediaObject.MEDIA_TYPE_VIDEO) {
+            drawVideoOverlay(canvas, width, height);
+        }
+
+        if (entry.isPanorama) {
+            drawPanoramaBorder(canvas, width, height);
+        }
+
+        return renderRequestFlags;
+    }
+
+    protected int renderLabel(
+            GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+        Texture content = checkTexture(entry.labelTexture);
+        if (content != null) {
+            int b = AlbumLabelMaker.getBorderSize();
+            int h = content.getHeight();
+            content.draw(canvas, -b, height - h + b, width + b + b, h);
+        }
+        return 0;
+    }
+
+    @Override
+    public void prepareDrawing() {
+        mInSelectionMode = mSelectionManager.inSelectionMode();
+    }
+
+    private class MyCacheListener implements AlbumSetSlidingWindow.Listener {
+
+        @Override
+        public void onSizeChanged(int size) {
+            mSlotView.setSlotCount(size);
+        }
+
+        @Override
+        public void onContentChanged() {
+            mSlotView.invalidate();
+        }
+    }
+
+    public void pause() {
+        mDataWindow.pause();
+    }
+
+    public void resume() {
+        mDataWindow.resume();
+    }
+
+    @Override
+    public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+        if (mDataWindow != null) {
+            mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+        }
+    }
+
+    @Override
+    public void onSlotSizeChanged(int width, int height) {
+        if (mDataWindow != null) {
+            mDataWindow.onSlotSizeChanged(width, height);
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetView.java b/src/com/android/gallery3d/ui/AlbumSetView.java
deleted file mode 100644
index 86398ec..0000000
--- a/src/com/android/gallery3d/ui/AlbumSetView.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.graphics.Rect;
-
-import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.ui.PositionRepository.Position;
-
-import java.util.Random;
-
-public class AlbumSetView extends SlotView {
-    @SuppressWarnings("unused")
-    private static final String TAG = "AlbumSetView";
-    private static final int CACHE_SIZE = 32;
-    private static final float PHOTO_DISTANCE = 35f;
-
-    private int mVisibleStart;
-    private int mVisibleEnd;
-
-    private final Random mRandom = new Random();
-    private final long mSeed = mRandom.nextLong();
-
-    private AlbumSetSlidingWindow mDataWindow;
-    private final GalleryActivity mActivity;
-    private final LabelSpec mLabelSpec;
-
-    private SelectionDrawer mSelectionDrawer;
-
-    public static interface Model {
-        public MediaItem[] getCoverItems(int index);
-        public MediaSet getMediaSet(int index);
-        public int size();
-        public void setActiveWindow(int start, int end);
-        public void setModelListener(ModelListener listener);
-    }
-
-    public static interface ModelListener {
-        public void onWindowContentChanged(int index);
-        public void onSizeChanged(int size);
-    }
-
-    public static class AlbumSetItem {
-        public DisplayItem[] covers;
-        public DisplayItem labelItem;
-        public long setDataVersion;
-    }
-
-    public static class LabelSpec {
-        public int labelBackgroundHeight;
-        public int titleOffset;
-        public int countOffset;
-        public int titleFontSize;
-        public int countFontSize;
-        public int leftMargin;
-        public int iconSize;
-    }
-
-    public AlbumSetView(GalleryActivity activity, SelectionDrawer drawer,
-            SlotView.Spec slotViewSpec, LabelSpec labelSpec) {
-        super(activity.getAndroidContext());
-        mActivity = activity;
-        setSelectionDrawer(drawer);
-        setSlotSpec(slotViewSpec);
-        mLabelSpec = labelSpec;
-    }
-
-    public void setSelectionDrawer(SelectionDrawer drawer) {
-        mSelectionDrawer = drawer;
-        if (mDataWindow != null) {
-            mDataWindow.setSelectionDrawer(drawer);
-        }
-    }
-
-    public void setModel(AlbumSetView.Model model) {
-        if (mDataWindow != null) {
-            mDataWindow.setListener(null);
-            setSlotCount(0);
-            mDataWindow = null;
-        }
-        if (model != null) {
-            mDataWindow = new AlbumSetSlidingWindow(mActivity, mLabelSpec,
-                    mSelectionDrawer, model, CACHE_SIZE);
-            mDataWindow.setListener(new MyCacheListener());
-            setSlotCount(mDataWindow.size());
-            updateVisibleRange(getVisibleStart(), getVisibleEnd());
-        }
-    }
-
-    private void putSlotContent(int slotIndex, AlbumSetItem entry) {
-        // Get displayItems from mItemsetMap or create them from MediaSet.
-        Utils.assertTrue(entry != null);
-        Rect rect = getSlotRect(slotIndex);
-
-        DisplayItem[] items = entry.covers;
-        mRandom.setSeed(slotIndex ^ mSeed);
-
-        int x = (rect.left + rect.right) / 2;
-        int y = (rect.top + rect.bottom) / 2;
-
-        Position basePosition = new Position(x, y, 0);
-
-        // Put the cover items in reverse order, so that the first item is on
-        // top of the rest.
-        Position position = new Position(x, y, 0f);
-        putDisplayItem(position, position, entry.labelItem);
-
-        for (int i = 0, n = items.length; i < n; ++i) {
-            DisplayItem item = items[i];
-            float dx = 0;
-            float dy = 0;
-            float dz = 0f;
-            float theta = 0;
-            if (i != 0) {
-                dz = i * PHOTO_DISTANCE;
-            }
-            position = new Position(x + dx, y + dy, dz);
-            position.theta = theta;
-            putDisplayItem(position, basePosition, item);
-        }
-
-    }
-
-    private void freeSlotContent(int index, AlbumSetItem entry) {
-        if (entry == null) return;
-        for (DisplayItem item : entry.covers) {
-            removeDisplayItem(item);
-        }
-        removeDisplayItem(entry.labelItem);
-    }
-
-    public int size() {
-        return mDataWindow.size();
-    }
-
-    @Override
-    public void onLayoutChanged(int width, int height) {
-        updateVisibleRange(0, 0);
-        updateVisibleRange(getVisibleStart(), getVisibleEnd());
-    }
-
-    @Override
-    public void onScrollPositionChanged(int position) {
-        super.onScrollPositionChanged(position);
-        updateVisibleRange(getVisibleStart(), getVisibleEnd());
-    }
-
-    private void updateVisibleRange(int start, int end) {
-        if (start == mVisibleStart && end == mVisibleEnd) {
-            // we need to set the mDataWindow active range in any case.
-            mDataWindow.setActiveWindow(start, end);
-            return;
-        }
-        if (start >= mVisibleEnd || mVisibleStart >= end) {
-            for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-                freeSlotContent(i, mDataWindow.get(i));
-            }
-            mDataWindow.setActiveWindow(start, end);
-            for (int i = start; i < end; ++i) {
-                putSlotContent(i, mDataWindow.get(i));
-            }
-        } else {
-            for (int i = mVisibleStart; i < start; ++i) {
-                freeSlotContent(i, mDataWindow.get(i));
-            }
-            for (int i = end, n = mVisibleEnd; i < n; ++i) {
-                freeSlotContent(i, mDataWindow.get(i));
-            }
-            mDataWindow.setActiveWindow(start, end);
-            for (int i = start, n = mVisibleStart; i < n; ++i) {
-                putSlotContent(i, mDataWindow.get(i));
-            }
-            for (int i = mVisibleEnd; i < end; ++i) {
-                putSlotContent(i, mDataWindow.get(i));
-            }
-        }
-        mVisibleStart = start;
-        mVisibleEnd = end;
-
-        invalidate();
-    }
-
-    @Override
-    protected void render(GLCanvas canvas) {
-        mSelectionDrawer.prepareDrawing();
-        super.render(canvas);
-    }
-
-    private class MyCacheListener implements AlbumSetSlidingWindow.Listener {
-
-        public void onSizeChanged(int size) {
-            if (setSlotCount(size)) {
-                // If the layout parameters are changed, we need reput all items.
-                // We keep the visible range at the same center but with size 0.
-                // So that we can:
-                //     1.) flush all visible items
-                //     2.) keep the cached data
-                int center = (getVisibleStart() + getVisibleEnd()) / 2;
-                updateVisibleRange(center, center);
-            }
-            updateVisibleRange(getVisibleStart(), getVisibleEnd());
-            invalidate();
-        }
-
-        public void onWindowContentChanged(int slot, AlbumSetItem old, AlbumSetItem update) {
-            freeSlotContent(slot, old);
-            putSlotContent(slot, update);
-            invalidate();
-        }
-
-        public void onContentInvalidated() {
-            invalidate();
-        }
-    }
-
-    public void pause() {
-        for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-            freeSlotContent(i, mDataWindow.get(i));
-        }
-        mDataWindow.pause();
-    }
-
-    public void resume() {
-        mDataWindow.resume();
-        for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-            putSlotContent(i, mDataWindow.get(i));
-        }
-    }
-}
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
index b40d72c..deec171 100644
--- a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -19,9 +19,8 @@
 import android.graphics.Bitmap;
 import android.os.Message;
 
+import com.android.gallery3d.app.AlbumDataLoader;
 import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.common.LruCache;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.Path;
@@ -29,26 +28,37 @@
 import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.JobLimiter;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
 
-public class AlbumSlidingWindow implements AlbumView.ModelListener {
+public class AlbumSlidingWindow implements AlbumDataLoader.DataListener {
     @SuppressWarnings("unused")
     private static final String TAG = "AlbumSlidingWindow";
 
-    private static final int MSG_LOAD_BITMAP_DONE = 0;
-    private static final int MSG_UPDATE_SLOT = 1;
+    private static final int MSG_UPDATE_ENTRY = 0;
     private static final int JOB_LIMIT = 2;
-    private static final int PLACEHOLDER_COLOR = 0xFF222222;
 
     public static interface Listener {
         public void onSizeChanged(int size);
-        public void onContentInvalidated();
-        public void onWindowContentChanged(
-                int slot, DisplayItem old, DisplayItem update);
+        public void onContentChanged();
     }
 
-    private final AlbumView.Model mSource;
+    public static class AlbumEntry {
+        public MediaItem item;
+        public Path path;
+        public boolean isPanorama;
+        public int rotation;
+        public int mediaType;
+        public boolean isWaitDisplayed;
+        public BitmapTexture bitmapTexture;
+        public Texture content;
+        private BitmapLoader contentLoader;
+    }
+
+    private final AlbumDataLoader mSource;
+    private final AlbumEntry mData[];
+    private final SynchronizedHandler mHandler;
+    private final JobLimiter mThreadPool;
+    private final TextureUploader mTextureUploader;
+
     private int mSize;
 
     private int mContentStart = 0;
@@ -58,74 +68,41 @@
     private int mActiveEnd = 0;
 
     private Listener mListener;
-    private int mFocusIndex = -1;
-
-    private final AlbumDisplayItem mData[];
-    private final ColorTexture mWaitLoadingTexture;
-    private SelectionDrawer mSelectionDrawer;
-
-    private SynchronizedHandler mHandler;
-    private JobLimiter mThreadPool;
 
     private int mActiveRequestCount = 0;
     private boolean mIsActive = false;
 
-    private int mCacheThumbSize;  // 0: Don't cache the thumbnails
-    private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000);
-
     public AlbumSlidingWindow(GalleryActivity activity,
-            AlbumView.Model source, int cacheSize,
-            int cacheThumbSize) {
-        source.setModelListener(this);
+            AlbumDataLoader source, int cacheSize) {
+        source.setDataListener(this);
         mSource = source;
-        mData = new AlbumDisplayItem[cacheSize];
+        mData = new AlbumEntry[cacheSize];
         mSize = source.size();
 
-        mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR);
-        mWaitLoadingTexture.setSize(1, 1);
-
         mHandler = new SynchronizedHandler(activity.getGLRoot()) {
             @Override
             public void handleMessage(Message message) {
-                switch (message.what) {
-                    case MSG_LOAD_BITMAP_DONE: {
-                        ((AlbumDisplayItem) message.obj).onLoadBitmapDone();
-                        break;
-                    }
-                    case MSG_UPDATE_SLOT: {
-                        updateSlotContent(message.arg1);
-                        break;
-                    }
-                }
+                Utils.assertTrue(message.what == MSG_UPDATE_ENTRY);
+                ((ThumbnailLoader) message.obj).updateEntry();
             }
         };
 
         mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT);
-    }
-
-    public void setSelectionDrawer(SelectionDrawer drawer) {
-        mSelectionDrawer = drawer;
+        mTextureUploader = new TextureUploader(activity.getGLRoot());
     }
 
     public void setListener(Listener listener) {
         mListener = listener;
     }
 
-    public void setFocusIndex(int slotIndex) {
-        mFocusIndex = slotIndex;
-    }
-
-    public DisplayItem get(int slotIndex) {
-        Utils.assertTrue(isActiveSlot(slotIndex),
-                "invalid slot: %s outsides (%s, %s)",
-                slotIndex, mActiveStart, mActiveEnd);
+    public AlbumEntry get(int slotIndex) {
+        if (!isActiveSlot(slotIndex)) {
+            Utils.fail("invalid slot: %s outsides (%s, %s)",
+                    slotIndex, mActiveStart, mActiveEnd);
+        }
         return mData[slotIndex % mData.length];
     }
 
-    public int size() {
-        return mSize;
-    }
-
     public boolean isActiveSlot(int slotIndex) {
         return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
     }
@@ -169,10 +146,10 @@
     }
 
     public void setActiveWindow(int start, int end) {
-        Utils.assertTrue(start <= end
-                && end - start <= mData.length && end <= mSize,
-                "%s, %s, %s, %s", start, end, mData.length, mSize);
-        DisplayItem data[] = mData;
+        if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+            Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize);
+        }
+        AlbumEntry data[] = mData;
 
         mActiveStart = start;
         mActiveEnd = end;
@@ -181,9 +158,40 @@
                 0, Math.max(0, mSize - data.length));
         int contentEnd = Math.min(contentStart + data.length, mSize);
         setContentWindow(contentStart, contentEnd);
+        updateTextureUploadQueue();
         if (mIsActive) updateAllImageRequests();
     }
 
+    private void uploadBgTextureInSlot(int index) {
+        if (index < mContentEnd && index >= mContentStart) {
+            AlbumEntry entry = mData[index % mData.length];
+            if (entry.bitmapTexture != null) {
+                mTextureUploader.addBgTexture(entry.bitmapTexture);
+            }
+        }
+    }
+
+    private void updateTextureUploadQueue() {
+        if (!mIsActive) return;
+        mTextureUploader.clear();
+
+        // add foreground textures
+        for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+            AlbumEntry entry = mData[i % mData.length];
+            if (entry.bitmapTexture != null) {
+                mTextureUploader.addFgTexture(entry.bitmapTexture);
+            }
+        }
+
+        // add background textures
+        int range = Math.max(
+                (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+        for (int i = 0; i < range; ++i) {
+            uploadBgTextureInSlot(mActiveEnd + i);
+            uploadBgTextureInSlot(mActiveStart - i - 1);
+        }
+    }
+
     // We would like to request non active slots in the following order:
     // Order:    8 6 4 2                   1 3 5 7
     //         |---------|---------------|---------|
@@ -193,80 +201,63 @@
         int range = Math.max(
                 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
         for (int i = 0 ;i < range; ++i) {
-            requestSlotImage(mActiveEnd + i, false);
-            requestSlotImage(mActiveStart - 1 - i, false);
+            requestSlotImage(mActiveEnd + i);
+            requestSlotImage(mActiveStart - 1 - i);
         }
     }
 
-    private void requestSlotImage(int slotIndex, boolean isActive) {
-        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
-        AlbumDisplayItem item = mData[slotIndex % mData.length];
-        item.requestImage();
+    // return whether the request is in progress or not
+    private boolean requestSlotImage(int slotIndex) {
+        if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false;
+        AlbumEntry entry = mData[slotIndex % mData.length];
+        if (entry.content != null || entry.item == null) return false;
+
+        entry.contentLoader.startLoad();
+        return entry.contentLoader.isRequestInProgress();
     }
 
     private void cancelNonactiveImages() {
         int range = Math.max(
                 (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
         for (int i = 0 ;i < range; ++i) {
-            cancelSlotImage(mActiveEnd + i, false);
-            cancelSlotImage(mActiveStart - 1 - i, false);
+            cancelSlotImage(mActiveEnd + i);
+            cancelSlotImage(mActiveStart - 1 - i);
         }
     }
 
-    private void cancelSlotImage(int slotIndex, boolean isActive) {
+    private void cancelSlotImage(int slotIndex) {
         if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
-        AlbumDisplayItem item = mData[slotIndex % mData.length];
-        item.cancelImageRequest();
+        AlbumEntry item = mData[slotIndex % mData.length];
+        if (item.contentLoader != null) item.contentLoader.cancelLoad();
     }
 
     private void freeSlotContent(int slotIndex) {
-        AlbumDisplayItem data[] = mData;
+        AlbumEntry data[] = mData;
         int index = slotIndex % data.length;
-        AlbumDisplayItem original = data[index];
-        if (original != null) {
-            original.recycle();
-            data[index] = null;
-        }
+        AlbumEntry entry = data[index];
+        if (entry.contentLoader != null) entry.contentLoader.recycle();
+        if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+        data[index] = null;
     }
 
-    private void prepareSlotContent(final int slotIndex) {
-        mData[slotIndex % mData.length] = new AlbumDisplayItem(
-                slotIndex, mSource.get(slotIndex));
-    }
-
-    private void updateSlotContent(final int slotIndex) {
-        MediaItem item = mSource.get(slotIndex);
-        AlbumDisplayItem data[] = mData;
-        int index = slotIndex % data.length;
-        AlbumDisplayItem original = data[index];
-        AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item);
-        data[index] = update;
-        boolean isActive = isActiveSlot(slotIndex);
-        if (mListener != null && isActive) {
-            mListener.onWindowContentChanged(slotIndex, original, update);
-        }
-        if (original != null) {
-            if (isActive && original.isRequestInProgress()) {
-                --mActiveRequestCount;
-            }
-            original.recycle();
-        }
-        if (isActive) {
-            if (mActiveRequestCount == 0) cancelNonactiveImages();
-            ++mActiveRequestCount;
-            update.requestImage();
-        } else {
-            if (mActiveRequestCount == 0) update.requestImage();
-        }
+    private void prepareSlotContent(int slotIndex) {
+        AlbumEntry entry = new AlbumEntry();
+        MediaItem item = mSource.get(slotIndex); // item could be null;
+        entry.item = item;
+        entry.isPanorama = GalleryUtils.isPanorama(entry.item);
+        entry.mediaType = (item == null)
+                ? MediaItem.MEDIA_TYPE_UNKNOWN
+                : entry.item.getMediaType();
+        entry.path = (item == null) ? null : item.getPath();
+        entry.rotation = (item == null) ? 0 : item.getRotation();
+        entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item);
+        mData[slotIndex % mData.length] = entry;
     }
 
     private void updateAllImageRequests() {
         mActiveRequestCount = 0;
-        AlbumDisplayItem data[] = mData;
         for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
-            AlbumDisplayItem item = data[i % data.length];
-            item.requestImage();
-            if (item.isRequestInProgress()) ++mActiveRequestCount;
+            if (requestSlotImage(i)) ++mActiveRequestCount;
         }
         if (mActiveRequestCount == 0) {
             requestNonactiveImages();
@@ -275,155 +266,69 @@
         }
     }
 
-    private class AlbumDisplayItem extends AbstractDisplayItem
-            implements FutureListener<Bitmap>, Job<Bitmap> {
-        private Future<Bitmap> mFuture;
+    private class ThumbnailLoader extends BitmapLoader  {
         private final int mSlotIndex;
-        private final int mMediaType;
-        private Texture mContent;
-        private boolean mIsPanorama;
-        private boolean mWaitLoadingDisplayed;
+        private final MediaItem mItem;
 
-        public AlbumDisplayItem(int slotIndex, MediaItem item) {
-            super(item);
-            mMediaType = (item == null)
-                    ? MediaItem.MEDIA_TYPE_UNKNOWN
-                    : item.getMediaType();
+        public ThumbnailLoader(int slotIndex, MediaItem item) {
             mSlotIndex = slotIndex;
-            mIsPanorama = GalleryUtils.isPanorama(item);
-            updateContent(mWaitLoadingTexture);
+            mItem = item;
         }
 
         @Override
-        protected void onBitmapAvailable(Bitmap bitmap) {
-            boolean isActiveSlot = isActiveSlot(mSlotIndex);
-            if (isActiveSlot) {
+        protected void recycleBitmap(Bitmap bitmap) {
+            MediaItem.getMicroThumbPool().recycle(bitmap);
+        }
+
+        @Override
+        protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+            return mThreadPool.submit(
+                    mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this);
+        }
+
+        @Override
+        protected void onLoadComplete(Bitmap bitmap) {
+            mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget();
+        }
+
+        public void updateEntry() {
+            Bitmap bitmap = getBitmap();
+            if (bitmap == null) return; // error or recycled
+
+            AlbumEntry entry = mData[mSlotIndex % mData.length];
+            entry.bitmapTexture = new BitmapTexture(bitmap);
+            entry.content = entry.bitmapTexture;
+
+            if (isActiveSlot(mSlotIndex)) {
+                mTextureUploader.addFgTexture(entry.bitmapTexture);
                 --mActiveRequestCount;
                 if (mActiveRequestCount == 0) requestNonactiveImages();
-            }
-            if (bitmap != null) {
-                BitmapTexture texture = new BitmapTexture(bitmap, true);
-                texture.setThrottled(true);
-                if (mWaitLoadingDisplayed) {
-                    updateContent(new FadeInTexture(PLACEHOLDER_COLOR, texture));
-                } else {
-                    updateContent(texture);
-                }
-                if (mListener != null && isActiveSlot) {
-                    mListener.onContentInvalidated();
-                }
-            }
-        }
-
-        private void updateContent(Texture content) {
-            mContent = content;
-        }
-
-        @Override
-        public int render(GLCanvas canvas, int pass) {
-            // Fit the content into the box
-            int width = mContent.getWidth();
-            int height = mContent.getHeight();
-
-            float scalex = mBoxWidth / (float) width;
-            float scaley = mBoxHeight / (float) height;
-            float scale = Math.min(scalex, scaley);
-
-            width = (int) Math.floor(width * scale);
-            height = (int) Math.floor(height * scale);
-
-            // Now draw it
-            if (pass == 0) {
-                Path path = null;
-                if (mMediaItem != null) path = mMediaItem.getPath();
-                mSelectionDrawer.draw(canvas, mContent, width, height,
-                        getRotation(), path, mMediaType, mIsPanorama);
-                if (mContent == mWaitLoadingTexture) {
-                       mWaitLoadingDisplayed = true;
-                }
-                int result = 0;
-                if (mFocusIndex == mSlotIndex) {
-                    result |= RENDER_MORE_PASS;
-                }
-                if ((mContent instanceof FadeInTexture) &&
-                        ((FadeInTexture) mContent).isAnimating()) {
-                    result |= RENDER_MORE_FRAME;
-                }
-                return result;
-            } else if (pass == 1) {
-                mSelectionDrawer.drawFocus(canvas, width, height);
-            }
-            return 0;
-        }
-
-        @Override
-        public void startLoadBitmap() {
-            if (mCacheThumbSize > 0) {
-                Path path = mMediaItem.getPath();
-                if (mImageCache.containsKey(path)) {
-                    Bitmap bitmap = mImageCache.get(path);
-                    updateImage(bitmap, false);
-                    return;
-                }
-                mFuture = mThreadPool.submit(this, this);
+                if (mListener != null) mListener.onContentChanged();
             } else {
-                mFuture = mThreadPool.submit(mMediaItem.requestImage(
-                        MediaItem.TYPE_MICROTHUMBNAIL), this);
+                mTextureUploader.addBgTexture(entry.bitmapTexture);
             }
         }
-
-        // This gets the bitmap and scale it down.
-        public Bitmap run(JobContext jc) {
-            Job<Bitmap> job = mMediaItem.requestImage(
-                    MediaItem.TYPE_MICROTHUMBNAIL);
-            Bitmap bitmap = job.run(jc);
-            if (bitmap != null) {
-                bitmap = BitmapUtils.resizeDownBySideLength(
-                        bitmap, mCacheThumbSize, true);
-            }
-            return bitmap;
-        }
-
-        @Override
-        public void cancelLoadBitmap() {
-            if (mFuture != null) {
-                mFuture.cancel();
-            }
-        }
-
-        @Override
-        public void onFutureDone(Future<Bitmap> bitmap) {
-            mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
-        }
-
-        private void onLoadBitmapDone() {
-            Future<Bitmap> future = mFuture;
-            mFuture = null;
-            Bitmap bitmap = future.get();
-            boolean isCancelled = future.isCancelled();
-            if (mCacheThumbSize > 0 && (bitmap != null || !isCancelled)) {
-                Path path = mMediaItem.getPath();
-                mImageCache.put(path, bitmap);
-            }
-            updateImage(bitmap, isCancelled);
-        }
-
-        @Override
-        public String toString() {
-            return String.format("AlbumDisplayItem[%s]", mSlotIndex);
-        }
     }
 
+    @Override
     public void onSizeChanged(int size) {
         if (mSize != size) {
             mSize = size;
             if (mListener != null) mListener.onSizeChanged(mSize);
+            if (mContentEnd > mSize) mContentEnd = mSize;
+            if (mActiveEnd > mSize) mActiveEnd = mSize;
         }
     }
 
-    public void onWindowContentChanged(int index) {
+    @Override
+    public void onContentChanged(int index) {
         if (index >= mContentStart && index < mContentEnd && mIsActive) {
-            updateSlotContent(index);
+            freeSlotContent(index);
+            prepareSlotContent(index);
+            updateAllImageRequests();
+            if (mListener != null && isActiveSlot(index)) {
+                mListener.onContentChanged();
+            }
         }
     }
 
@@ -437,9 +342,9 @@
 
     public void pause() {
         mIsActive = false;
+        mTextureUploader.clear();
         for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
             freeSlotContent(i);
         }
-        mImageCache.clear();
     }
 }
diff --git a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
new file mode 100644
index 0000000..ab99766
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+
+import com.android.gallery3d.app.AlbumDataLoader;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+
+public class AlbumSlotRenderer extends AbstractSlotRenderer {
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumView";
+
+    public interface SlotFilter {
+        public boolean acceptSlot(int index);
+    }
+
+    private static final int PLACEHOLDER_COLOR = 0xFF222222;
+    private static final int CACHE_SIZE = 96;
+
+    private AlbumSlidingWindow mDataWindow;
+    private final GalleryActivity mActivity;
+    private final ColorTexture mWaitLoadingTexture;
+    private final SlotView mSlotView;
+    private final SelectionManager mSelectionManager;
+
+    private int mPressedIndex = -1;
+    private boolean mAnimatePressedUp;
+    private Path mHighlightItemPath = null;
+    private boolean mInSelectionMode;
+
+    private SlotFilter mSlotFilter;
+
+    public AlbumSlotRenderer(GalleryActivity activity, SlotView slotView,
+            SelectionManager selectionManager) {
+        super((Context) activity);
+        mActivity = activity;
+        mSlotView = slotView;
+        mSelectionManager = selectionManager;
+
+        mWaitLoadingTexture = new ColorTexture(PLACEHOLDER_COLOR);
+        mWaitLoadingTexture.setSize(1, 1);
+    }
+
+    public void setPressedIndex(int index) {
+        if (mPressedIndex == index) return;
+        mPressedIndex = index;
+        mSlotView.invalidate();
+    }
+
+    public void setPressedUp() {
+        if (mPressedIndex == -1) return;
+        mAnimatePressedUp = true;
+        mSlotView.invalidate();
+    }
+
+    public void setHighlightItemPath(Path path) {
+        if (mHighlightItemPath == path) return;
+        mHighlightItemPath = path;
+        mSlotView.invalidate();
+    }
+
+    public void setModel(AlbumDataLoader model) {
+        if (mDataWindow != null) {
+            mDataWindow.setListener(null);
+            mSlotView.setSlotCount(0);
+            mDataWindow = null;
+        }
+        if (model != null) {
+            mDataWindow = new AlbumSlidingWindow(mActivity, model, CACHE_SIZE);
+            mDataWindow.setListener(new MyDataModelListener());
+            mSlotView.setSlotCount(model.size());
+        }
+    }
+
+    private static Texture checkTexture(Texture texture) {
+        return (texture instanceof UploadedTexture)
+                && ((UploadedTexture) texture).isUploading()
+                ? null
+                : texture;
+    }
+
+    @Override
+    public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        if (mSlotFilter != null && !mSlotFilter.acceptSlot(index)) return 0;
+
+        AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index);
+
+        int renderRequestFlags = 0;
+
+        Texture content = checkTexture(entry.content);
+        if (content == null) {
+            content = mWaitLoadingTexture;
+            entry.isWaitDisplayed = true;
+        } else if (entry.isWaitDisplayed) {
+            entry.isWaitDisplayed = false;
+            content = new FadeInTexture(PLACEHOLDER_COLOR, entry.bitmapTexture);
+            entry.content = content;
+        }
+        drawContent(canvas, content, width, height, entry.rotation);
+        if ((content instanceof FadeInTexture) &&
+                ((FadeInTexture) content).isAnimating()) {
+            renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+        }
+
+        if (entry.mediaType == MediaObject.MEDIA_TYPE_VIDEO) {
+            drawVideoOverlay(canvas, width, height);
+        }
+
+        if (entry.isPanorama) {
+            drawPanoramaBorder(canvas, width, height);
+        }
+
+        renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+
+        return renderRequestFlags;
+    }
+
+    private int renderOverlay(GLCanvas canvas, int index,
+            AlbumSlidingWindow.AlbumEntry entry, int width, int height) {
+        int renderRequestFlags = 0;
+        if (mPressedIndex == index) {
+            if (mAnimatePressedUp) {
+                drawPressedUpFrame(canvas, width, height);
+                renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+                if (isPressedUpFrameFinished()) {
+                    mAnimatePressedUp = false;
+                    mPressedIndex = -1;
+                }
+            } else {
+                drawPressedFrame(canvas, width, height);
+            }
+        } else if ((entry.path != null) && (mHighlightItemPath == entry.path)) {
+            drawSelectedFrame(canvas, width, height);
+        } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.path)) {
+            drawSelectedFrame(canvas, width, height);
+        }
+        return renderRequestFlags;
+    }
+
+    private class MyDataModelListener implements AlbumSlidingWindow.Listener {
+        @Override
+        public void onContentChanged() {
+            mSlotView.invalidate();
+        }
+
+        @Override
+        public void onSizeChanged(int size) {
+            mSlotView.setSlotCount(size);
+        }
+    }
+
+    public void resume() {
+        mDataWindow.resume();
+    }
+
+    public void pause() {
+        mDataWindow.pause();
+    }
+
+    @Override
+    public void prepareDrawing() {
+        mInSelectionMode = mSelectionManager.inSelectionMode();
+    }
+
+    @Override
+    public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+        if (mDataWindow != null) {
+            mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+        }
+    }
+
+    @Override
+    public void onSlotSizeChanged(int width, int height) {
+        // Do nothing
+    }
+
+    public void setSlotFilter(SlotFilter slotFilter) {
+        mSlotFilter = slotFilter;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumView.java b/src/com/android/gallery3d/ui/AlbumView.java
deleted file mode 100644
index 6344851..0000000
--- a/src/com/android/gallery3d/ui/AlbumView.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.graphics.Rect;
-
-import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.ui.PositionRepository.Position;
-
-public class AlbumView extends SlotView {
-    @SuppressWarnings("unused")
-    private static final String TAG = "AlbumView";
-    private static final int CACHE_SIZE = 64;
-
-    private int mVisibleStart = 0;
-    private int mVisibleEnd = 0;
-
-    private AlbumSlidingWindow mDataWindow;
-    private final GalleryActivity mActivity;
-    private SelectionDrawer mSelectionDrawer;
-    private int mCacheThumbSize;
-
-    private boolean mIsActive = false;
-
-    public static interface Model {
-        public int size();
-        public MediaItem get(int index);
-        public void setActiveWindow(int start, int end);
-        public void setModelListener(ModelListener listener);
-    }
-
-    public static interface ModelListener {
-        public void onWindowContentChanged(int index);
-        public void onSizeChanged(int size);
-    }
-
-    public AlbumView(GalleryActivity activity, SlotView.Spec spec,
-            int cacheThumbSize) {
-        super(activity.getAndroidContext());
-        mCacheThumbSize = cacheThumbSize;
-        setSlotSpec(spec);
-        mActivity = activity;
-    }
-
-    public void setSelectionDrawer(SelectionDrawer drawer) {
-        mSelectionDrawer = drawer;
-        if (mDataWindow != null) mDataWindow.setSelectionDrawer(drawer);
-    }
-
-    public void setModel(Model model) {
-        if (mDataWindow != null) {
-            mDataWindow.setListener(null);
-            setSlotCount(0);
-            mDataWindow = null;
-        }
-        if (model != null) {
-            mDataWindow = new AlbumSlidingWindow(
-                    mActivity, model, CACHE_SIZE,
-                    mCacheThumbSize);
-            mDataWindow.setSelectionDrawer(mSelectionDrawer);
-            mDataWindow.setListener(new MyDataModelListener());
-            setSlotCount(model.size());
-            updateVisibleRange(getVisibleStart(), getVisibleEnd());
-        }
-    }
-
-    public void setFocusIndex(int slotIndex) {
-        if (mDataWindow != null) {
-            mDataWindow.setFocusIndex(slotIndex);
-        }
-    }
-
-    private void putSlotContent(int slotIndex, DisplayItem item) {
-        Rect rect = getSlotRect(slotIndex);
-        Position position = new Position(
-                (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, 0);
-        putDisplayItem(position, position, item);
-    }
-
-    private void updateVisibleRange(int start, int end) {
-        if (start == mVisibleStart && end == mVisibleEnd) {
-            // we need to set the mDataWindow active range in any case.
-            mDataWindow.setActiveWindow(start, end);
-            return;
-        }
-
-        if (!mIsActive) {
-            mVisibleStart = start;
-            mVisibleEnd = end;
-            mDataWindow.setActiveWindow(start, end);
-            return;
-        }
-
-        if (start >= mVisibleEnd || mVisibleStart >= end) {
-            for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-                DisplayItem item = mDataWindow.get(i);
-                if (item != null) removeDisplayItem(item);
-            }
-            mDataWindow.setActiveWindow(start, end);
-            for (int i = start; i < end; ++i) {
-                putSlotContent(i, mDataWindow.get(i));
-            }
-        } else {
-            for (int i = mVisibleStart; i < start; ++i) {
-                DisplayItem item = mDataWindow.get(i);
-                if (item != null) removeDisplayItem(item);
-            }
-            for (int i = end, n = mVisibleEnd; i < n; ++i) {
-                DisplayItem item = mDataWindow.get(i);
-                if (item != null) removeDisplayItem(item);
-            }
-            mDataWindow.setActiveWindow(start, end);
-            for (int i = start, n = mVisibleStart; i < n; ++i) {
-                putSlotContent(i, mDataWindow.get(i));
-            }
-            for (int i = mVisibleEnd; i < end; ++i) {
-                putSlotContent(i, mDataWindow.get(i));
-            }
-        }
-
-        mVisibleStart = start;
-        mVisibleEnd = end;
-    }
-
-    @Override
-    protected void onLayoutChanged(int width, int height) {
-        // Reput all the items
-        updateVisibleRange(0, 0);
-        updateVisibleRange(getVisibleStart(), getVisibleEnd());
-    }
-
-    @Override
-    protected void onScrollPositionChanged(int position) {
-        super.onScrollPositionChanged(position);
-        updateVisibleRange(getVisibleStart(), getVisibleEnd());
-    }
-
-    @Override
-    protected void render(GLCanvas canvas) {
-        mSelectionDrawer.prepareDrawing();
-        super.render(canvas);
-    }
-
-    private class MyDataModelListener implements AlbumSlidingWindow.Listener {
-
-        public void onContentInvalidated() {
-            invalidate();
-        }
-
-        public void onSizeChanged(int size) {
-            if (setSlotCount(size)) {
-                // If the layout parameters are changed, we need reput all items.
-                // We keep the visible range at the same center but with size 0.
-                // So that we can:
-                //     1.) flush all visible items
-                //     2.) keep the cached data
-                int center = (getVisibleStart() + getVisibleEnd()) / 2;
-                updateVisibleRange(center, center);
-            }
-            updateVisibleRange(getVisibleStart(), getVisibleEnd());
-            invalidate();
-        }
-
-        public void onWindowContentChanged(
-                int slotIndex, DisplayItem old, DisplayItem update) {
-            removeDisplayItem(old);
-            putSlotContent(slotIndex, update);
-        }
-    }
-
-    public void resume() {
-        mIsActive = true;
-        mDataWindow.resume();
-        for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-            putSlotContent(i, mDataWindow.get(i));
-        }
-    }
-
-    public void pause() {
-        mIsActive = false;
-        for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
-            removeDisplayItem(mDataWindow.get(i));
-        }
-        mDataWindow.pause();
-    }
-}
diff --git a/src/com/android/gallery3d/ui/AnimationTime.java b/src/com/android/gallery3d/ui/AnimationTime.java
new file mode 100644
index 0000000..0636774
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AnimationTime.java
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.os.SystemClock;
+
+//
+// The animation time should ideally be the vsync time the frame will be
+// displayed, but that is an unknown time in the future. So we use the system
+// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called)
+// as a approximation.
+//
+public class AnimationTime {
+    private static volatile long sTime;
+
+    // Sets current time as the animation time.
+    public static void update() {
+        sTime = SystemClock.uptimeMillis();
+    }
+
+    // Returns the animation time.
+    public static long get() {
+        return sTime;
+    }
+
+    public static long startTime() {
+        sTime = SystemClock.uptimeMillis();
+        return sTime;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java
index 8946036..7855927 100644
--- a/src/com/android/gallery3d/ui/BasicTexture.java
+++ b/src/com/android/gallery3d/ui/BasicTexture.java
@@ -18,7 +18,6 @@
 
 import com.android.gallery3d.common.Utils;
 
-import java.lang.ref.WeakReference;
 import java.util.WeakHashMap;
 
 // BasicTexture is a Texture corresponds to a real GL texture.
@@ -34,6 +33,8 @@
     protected static final int STATE_LOADED = 1;
     protected static final int STATE_ERROR = -1;
 
+    private static final int MAX_TEXTURE_SIZE = 2048;
+
     protected int mId;
     protected int mState;
 
@@ -45,7 +46,7 @@
 
     private boolean mHasBorder;
 
-    protected WeakReference<GLCanvas> mCanvasRef = null;
+    protected GLCanvas mCanvasRef = null;
     private static WeakHashMap<BasicTexture, Object> sAllTextures
             = new WeakHashMap<BasicTexture, Object>();
     private static ThreadLocal sInFinalizer = new ThreadLocal();
@@ -64,9 +65,7 @@
     }
 
     protected void setAssociatedCanvas(GLCanvas canvas) {
-        mCanvasRef = canvas == null
-                ? null
-                : new WeakReference<GLCanvas>(canvas);
+        mCanvasRef = canvas;
     }
 
     /**
@@ -78,6 +77,10 @@
         mHeight = height;
         mTextureWidth = Utils.nextPowerOf2(width);
         mTextureHeight = Utils.nextPowerOf2(height);
+        if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {
+            Log.w(TAG, String.format("texture is too large: %d x %d",
+                    mTextureWidth, mTextureHeight), new Exception());
+        }
     }
 
     public int getId() {
@@ -133,8 +136,11 @@
     // It should make sure the data is uploaded to GL memory.
     abstract protected boolean onBind(GLCanvas canvas);
 
-    public boolean isLoaded(GLCanvas canvas) {
-        return mState == STATE_LOADED && mCanvasRef.get() == canvas;
+    // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
+    abstract protected int getTarget();
+
+    public boolean isLoaded() {
+        return mState == STATE_LOADED;
     }
 
     // recycle() is called when the texture will never be used again,
@@ -153,11 +159,11 @@
     }
 
     private void freeResource() {
-        GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get();
-        if (canvas != null && isLoaded(canvas)) {
+        GLCanvas canvas = mCanvasRef;
+        if (canvas != null && isLoaded()) {
             canvas.unloadTexture(this);
         }
-        mState = BasicTexture.STATE_UNLOADED;
+        mState = STATE_UNLOADED;
         setAssociatedCanvas(null);
     }
 
@@ -182,4 +188,13 @@
             }
         }
     }
+
+    public static void invalidateAllTextures() {
+        synchronized (sAllTextures) {
+            for (BasicTexture t : sAllTextures.keySet()) {
+                t.mState = STATE_UNLOADED;
+                t.setAssociatedCanvas(null);
+            }
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/ui/BitmapLoader.java b/src/com/android/gallery3d/ui/BitmapLoader.java
new file mode 100644
index 0000000..4f07cc0
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapLoader.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+// We use this class to
+//     1.) load bitmaps in background.
+//     2.) as a place holder for the loaded bitmap
+public abstract class BitmapLoader implements FutureListener<Bitmap> {
+    @SuppressWarnings("unused")
+    private static final String TAG = "BitmapLoader";
+
+    /* Transition Map:
+     *   INIT -> REQUESTED, RECYCLED
+     *   REQUESTED -> INIT (cancel), LOADED, ERROR, RECYCLED
+     *   LOADED, ERROR -> RECYCLED
+     */
+    private static final int STATE_INIT = 0;
+    private static final int STATE_REQUESTED = 1;
+    private static final int STATE_LOADED = 2;
+    private static final int STATE_ERROR = 3;
+    private static final int STATE_RECYCLED = 4;
+
+    private int mState = STATE_INIT;
+    // mTask is not null only when a task is on the way
+    private Future<Bitmap> mTask;
+    private Bitmap mBitmap;
+
+    @Override
+    public void onFutureDone(Future<Bitmap> future) {
+        synchronized (this) {
+            mTask = null;
+            mBitmap = future.get();
+            if (mState == STATE_RECYCLED) {
+                if (mBitmap != null) {
+                    recycleBitmap(mBitmap);
+                    mBitmap = null;
+                }
+                return; // don't call callback
+            }
+            if (future.isCancelled() && mBitmap == null) {
+                if (mState == STATE_REQUESTED) mTask = submitBitmapTask(this);
+                return; // don't call callback
+            } else {
+                mState = mBitmap == null ? STATE_ERROR : STATE_LOADED;
+            }
+        }
+        onLoadComplete(mBitmap);
+    }
+
+    public synchronized void startLoad() {
+        if (mState == STATE_INIT) {
+            mState = STATE_REQUESTED;
+            if (mTask == null) mTask = submitBitmapTask(this);
+        }
+    }
+
+    public synchronized void cancelLoad() {
+        if (mState == STATE_REQUESTED) {
+            mState = STATE_INIT;
+            if (mTask != null) mTask.cancel();
+        }
+    }
+
+    // Recycle the loader and the bitmap
+    public synchronized void recycle() {
+        mState = STATE_RECYCLED;
+        if (mBitmap != null) {
+            recycleBitmap(mBitmap);
+            mBitmap = null;
+        }
+        if (mTask != null) mTask.cancel();
+    }
+
+    public synchronized boolean isRequestInProgress() {
+        return mState == STATE_REQUESTED;
+    }
+
+    public synchronized boolean isRecycled() {
+        return mState == STATE_RECYCLED;
+    }
+
+    public synchronized Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    abstract protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l);
+    abstract protected void recycleBitmap(Bitmap bitmap);
+    abstract protected void onLoadComplete(Bitmap bitmap);
+}
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
new file mode 100644
index 0000000..5c91757
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.RectF;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+
+// This is a ScreenNail wraps a Bitmap. There are some extra functions:
+//
+// - If we need to draw before the bitmap is available, we draw a rectange of
+// placeholder color (gray).
+//
+// - When the the bitmap is available, and we have drawn the placeholder color
+// before, we will do a fade-in animation.
+public class BitmapScreenNail implements ScreenNail {
+    private static final String TAG = "BitmapScreenNail";
+    private static final int PLACEHOLDER_COLOR = 0xFF222222;
+    // The duration of the fading animation in milliseconds
+    private static final int DURATION = 180;
+
+    private static final int MAX_SIDE = 640;
+
+    // These are special values for mAnimationStartTime
+    private static final long ANIMATION_NOT_NEEDED = -1;
+    private static final long ANIMATION_NEEDED = -2;
+    private static final long ANIMATION_DONE = -3;
+
+    private int mWidth;
+    private int mHeight;
+    private Bitmap mBitmap;
+    private BitmapTexture mTexture;
+    private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
+
+    public BitmapScreenNail(Bitmap bitmap) {
+        mWidth = bitmap.getWidth();
+        mHeight = bitmap.getHeight();
+        mBitmap = bitmap;
+        // We create mTexture lazily, so we don't incur the cost if we don't
+        // actually need it.
+    }
+
+    public BitmapScreenNail(int width, int height) {
+        setSize(width, height);
+    }
+
+    private void setSize(int width, int height) {
+        if (width == 0 || height == 0) {
+            width = 640;
+            height = 480;
+        }
+        float scale = Math.min(1, (float) MAX_SIDE / Math.max(width, height));
+        mWidth = Math.round(scale * width);
+        mHeight = Math.round(scale * height);
+    }
+
+    // Combines the two ScreenNails.
+    // Returns the used one and recycle the unused one.
+    public ScreenNail combine(ScreenNail other) {
+        if (other == null) {
+            return this;
+        }
+
+        if (!(other instanceof BitmapScreenNail)) {
+            recycle();
+            return other;
+        }
+
+        // Now both are BitmapScreenNail. Move over the information about width,
+        // height, and Bitmap, then recycle the other.
+        BitmapScreenNail newer = (BitmapScreenNail) other;
+        mWidth = newer.mWidth;
+        mHeight = newer.mHeight;
+        if (newer.mBitmap != null) {
+            if (mBitmap != null) {
+                MediaItem.getThumbPool().recycle(mBitmap);
+            }
+            mBitmap = newer.mBitmap;
+            newer.mBitmap = null;
+
+            if (mTexture != null) {
+                mTexture.recycle();
+                mTexture = null;
+            }
+        }
+
+        newer.recycle();
+        return this;
+    }
+
+    public void updatePlaceholderSize(int width, int height) {
+        if (mBitmap != null) return;
+        if (width == 0 || height == 0) return;
+        setSize(width, height);
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public void noDraw() {
+    }
+
+    @Override
+    public void recycle() {
+        if (mTexture != null) {
+            mTexture.recycle();
+            mTexture = null;
+        }
+        if (mBitmap != null) {
+            MediaItem.getThumbPool().recycle(mBitmap);
+            mBitmap = null;
+        }
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        if (mBitmap == null) {
+            if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
+                mAnimationStartTime = ANIMATION_NEEDED;
+            }
+            canvas.fillRect(x, y, width, height, PLACEHOLDER_COLOR);
+            return;
+        }
+
+        if (mTexture == null) {
+            mTexture = new BitmapTexture(mBitmap);
+        }
+
+        if (mAnimationStartTime == ANIMATION_NEEDED) {
+            mAnimationStartTime = now();
+        }
+
+        if (isAnimating()) {
+            canvas.drawMixed(mTexture, PLACEHOLDER_COLOR, getRatio(), x, y,
+                    width, height);
+        } else {
+            mTexture.draw(canvas, x, y, width, height);
+        }
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        if (mBitmap == null) {
+            canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
+                    PLACEHOLDER_COLOR);
+            return;
+        }
+
+        if (mTexture == null) {
+            mTexture = new BitmapTexture(mBitmap);
+        }
+
+        canvas.drawTexture(mTexture, source, dest);
+    }
+
+    public boolean isAnimating() {
+        if (mAnimationStartTime < 0) return false;
+        if (now() - mAnimationStartTime >= DURATION) {
+            mAnimationStartTime = ANIMATION_DONE;
+            return false;
+        }
+        return true;
+    }
+
+    private static long now() {
+        return AnimationTime.get();
+    }
+
+    private float getRatio() {
+        float r = (float)(now() - mAnimationStartTime) / DURATION;
+        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+    }
+
+    public boolean isShowingPlaceholder() {
+        return (mBitmap == null) || isAnimating();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTexture.java b/src/com/android/gallery3d/ui/BitmapTexture.java
index f5cb2bd..6075449 100644
--- a/src/com/android/gallery3d/ui/BitmapTexture.java
+++ b/src/com/android/gallery3d/ui/BitmapTexture.java
@@ -16,10 +16,10 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-
 import android.graphics.Bitmap;
 
+import com.android.gallery3d.common.Utils;
+
 // BitmapTexture is a texture whose content is specified by a fixed Bitmap.
 //
 // The texture does not own the Bitmap. The user should make sure the Bitmap
diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
index 3d4d4dc..d4c9b1d 100644
--- a/src/com/android/gallery3d/ui/BitmapTileProvider.java
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -16,16 +16,17 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.BitmapUtils;
-
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.BitmapPool;
 
 import java.util.ArrayList;
 
 public class BitmapTileProvider implements TileImageView.Model {
-    private final Bitmap mBackup;
+    private final ScreenNail mScreenNail;
     private final Bitmap[] mMipmaps;
     private final Config mConfig;
     private final int mImageWidth;
@@ -44,49 +45,51 @@
             list.add(bitmap);
         }
 
-        mBackup = list.remove(list.size() - 1);
+        mScreenNail = new BitmapScreenNail(list.remove(list.size() - 1));
         mMipmaps = list.toArray(new Bitmap[list.size()]);
         mConfig = Config.ARGB_8888;
     }
 
-    public Bitmap getBackupImage() {
-        return mBackup;
+    @Override
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
     }
 
+    @Override
     public int getImageHeight() {
         return mImageHeight;
     }
 
+    @Override
     public int getImageWidth() {
         return mImageWidth;
     }
 
+    @Override
     public int getLevelCount() {
         return mMipmaps.length;
     }
 
+    @Override
     public Bitmap getTile(int level, int x, int y, int tileSize,
-            int borderSize) {
+            int borderSize, BitmapPool pool) {
         x >>= level;
         y >>= level;
         int size = tileSize + 2 * borderSize;
-        Bitmap result = Bitmap.createBitmap(size, size, mConfig);
+
+        Bitmap result = pool == null ? null : pool.getBitmap();
+        if (result == null) {
+            result = Bitmap.createBitmap(size, size, mConfig);
+        } else {
+            result.eraseColor(0);
+        }
+
         Bitmap mipmap = mMipmaps[level];
         Canvas canvas = new Canvas(result);
         int offsetX = -x + borderSize;
         int offsetY = -y + borderSize;
         canvas.drawBitmap(mipmap, offsetX, offsetY, null);
-
-        // If the valid region (covered by mipmap or border) is smaller than the
-        // result bitmap, subset it.
-        int endX = offsetX + mipmap.getWidth() + borderSize;
-        int endY = offsetY + mipmap.getHeight() + borderSize;
-        if (endX < size || endY < size) {
-            return Bitmap.createBitmap(result, 0, 0, Math.min(size, endX),
-                    Math.min(size, endY));
-        } else {
-            return result;
-        }
+        return result;
     }
 
     public void recycle() {
@@ -95,14 +98,8 @@
         for (Bitmap bitmap : mMipmaps) {
             BitmapUtils.recycleSilently(bitmap);
         }
-        BitmapUtils.recycleSilently(mBackup);
-    }
-
-    public int getRotation() {
-        return 0;
-    }
-
-    public boolean isFailedToLoad() {
-        return false;
+        if (mScreenNail != null) {
+            mScreenNail.recycle();
+        }
     }
 }
diff --git a/src/com/android/gallery3d/ui/BoxBlurFilter.java b/src/com/android/gallery3d/ui/BoxBlurFilter.java
deleted file mode 100644
index 0497a61..0000000
--- a/src/com/android/gallery3d/ui/BoxBlurFilter.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.graphics.Bitmap;
-
-
-public class BoxBlurFilter {
-    private static final int RED_MASK = 0xff0000;
-    private static final int RED_MASK_SHIFT = 16;
-    private static final int GREEN_MASK = 0x00ff00;
-    private static final int GREEN_MASK_SHIFT = 8;
-    private static final int BLUE_MASK = 0x0000ff;
-    private static final int RADIUS = 4;
-    private static final int KERNEL_SIZE = RADIUS * 2 + 1;
-    private static final int NUM_COLORS = 256;
-    private static final int[] KERNEL_NORM = new int[KERNEL_SIZE * NUM_COLORS];
-
-    public static final int MODE_REPEAT = 1;
-    public static final int MODE_CLAMP = 2;
-
-    static {
-        int index = 0;
-        // Build a lookup table from summed to normalized kernel values.
-        // The formula: KERNAL_NORM[value] = value / KERNEL_SIZE
-        for (int i = 0; i < NUM_COLORS; ++i) {
-            for (int j = 0; j < KERNEL_SIZE; ++j) {
-                KERNEL_NORM[index++] = i;
-            }
-        }
-    }
-
-    private BoxBlurFilter() {
-    }
-
-    private static int sample(int x, int width, int mode) {
-        if (x >= 0 && x < width) return x;
-        return mode == MODE_REPEAT
-                ? x < 0 ? x + width : x - width
-                : x < 0 ? 0 : width - 1;
-    }
-
-    public static void apply(
-            Bitmap bitmap, int horizontalMode, int verticalMode) {
-
-        int width = bitmap.getWidth();
-        int height = bitmap.getHeight();
-        int data[] = new int[width * height];
-        bitmap.getPixels(data, 0, width, 0, 0, width, height);
-        int temp[] = new int[width * height];
-        applyOneDimension(data, temp, width, height, horizontalMode);
-        applyOneDimension(temp, data, height, width, verticalMode);
-        bitmap.setPixels(data, 0, width, 0, 0, width, height);
-    }
-
-    private static void applyOneDimension(
-            int[] in, int[] out, int width, int height, int mode) {
-        for (int y = 0, read = 0; y < height; ++y, read += width) {
-            // Evaluate the kernel for the first pixel in the row.
-            int red = 0;
-            int green = 0;
-            int blue = 0;
-            for (int i = -RADIUS; i <= RADIUS; ++i) {
-                int argb = in[read + sample(i, width, mode)];
-                red += (argb & RED_MASK) >> RED_MASK_SHIFT;
-                green += (argb & GREEN_MASK) >> GREEN_MASK_SHIFT;
-                blue += argb & BLUE_MASK;
-            }
-            for (int x = 0, write = y; x < width; ++x, write += height) {
-                // Output the current pixel.
-                out[write] = 0xFF000000
-                        | (KERNEL_NORM[red] << RED_MASK_SHIFT)
-                        | (KERNEL_NORM[green] << GREEN_MASK_SHIFT)
-                        | KERNEL_NORM[blue];
-
-                // Slide to the next pixel, adding the new rightmost pixel and
-                // subtracting the former leftmost.
-                int prev = in[read + sample(x - RADIUS, width, mode)];
-                int next = in[read + sample(x + RADIUS + 1, width, mode)];
-                red += ((next & RED_MASK) - (prev & RED_MASK)) >> RED_MASK_SHIFT;
-                green += ((next & GREEN_MASK) - (prev & GREEN_MASK)) >> GREEN_MASK_SHIFT;
-                blue += (next & BLUE_MASK) - (prev & BLUE_MASK);
-            }
-        }
-    }
-}
diff --git a/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
index 3c6e157..c65d77e 100644
--- a/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
+++ b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
@@ -16,12 +16,12 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.content.Context;
 import android.os.StatFs;
 
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 import java.io.File;
 
 public class CacheStorageUsageInfo {
diff --git a/src/com/android/gallery3d/ui/CanvasTexture.java b/src/com/android/gallery3d/ui/CanvasTexture.java
index 679a4bc..a2e9e48 100644
--- a/src/com/android/gallery3d/ui/CanvasTexture.java
+++ b/src/com/android/gallery3d/ui/CanvasTexture.java
@@ -17,8 +17,8 @@
 package com.android.gallery3d.ui;
 
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
 
 // CanvasTexture is a texture whose content is the drawing on a Canvas.
 // The subclasses should override onDraw() to draw on the bitmap.
diff --git a/src/com/android/gallery3d/ui/CaptureAnimation.java b/src/com/android/gallery3d/ui/CaptureAnimation.java
new file mode 100644
index 0000000..87c054a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CaptureAnimation.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+public class CaptureAnimation {
+    // The amount of change for zooming out.
+    private static final float ZOOM_DELTA = 0.2f;
+    // Pre-calculated value for convenience.
+    private static final float ZOOM_IN_BEGIN = 1f - ZOOM_DELTA;
+
+    private static final Interpolator sZoomOutInterpolator =
+            new DecelerateInterpolator();
+    private static final Interpolator sZoomInInterpolator =
+            new AccelerateInterpolator();
+    private static final Interpolator sSlideInterpolator =
+        new AccelerateDecelerateInterpolator();
+
+    // Calculate the slide factor based on the give time fraction.
+    public static float calculateSlide(float fraction) {
+        return sSlideInterpolator.getInterpolation(fraction);
+    }
+
+    // Calculate the scale factor based on the given time fraction.
+    public static float calculateScale(float fraction) {
+        float value;
+        if (fraction <= 0.5f) {
+            // Zoom in for the beginning.
+            value = 1f - ZOOM_DELTA *
+                    sZoomOutInterpolator.getInterpolation(fraction * 2);
+        } else {
+            // Zoom out for the ending.
+            value = ZOOM_IN_BEGIN + ZOOM_DELTA *
+                    sZoomInInterpolator.getInterpolation((fraction - 0.5f) * 2f);
+        }
+        return value;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/Config.java b/src/com/android/gallery3d/ui/Config.java
deleted file mode 100644
index 5c5b621..0000000
--- a/src/com/android/gallery3d/ui/Config.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-interface DetailsWindowConfig {
-    public static final int FONT_SIZE = 18;
-    public static final int PREFERRED_WIDTH = 400;
-    public static final int LEFT_RIGHT_EXTRA_PADDING = 9;
-    public static final int TOP_BOTTOM_EXTRA_PADDING = 9;
-    public static final int LINE_SPACING = 5;
-    public static final int FIRST_LINE_SPACING = 18;
-}
-
-interface TextButtonConfig {
-    public static final int HORIZONTAL_PADDINGS = 16;
-    public static final int VERTICAL_PADDINGS = 5;
-}
diff --git a/src/com/android/gallery3d/ui/CropView.java b/src/com/android/gallery3d/ui/CropView.java
index 71fdaab..78d5d37 100644
--- a/src/com/android/gallery3d/ui/CropView.java
+++ b/src/com/android/gallery3d/ui/CropView.java
@@ -16,11 +16,6 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.anim.Animation;
-import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.common.Utils;
-
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.Canvas;
@@ -31,11 +26,18 @@
 import android.media.FaceDetector;
 import android.os.Handler;
 import android.os.Message;
+import android.util.FloatMath;
 import android.view.MotionEvent;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+
 import java.util.ArrayList;
+
 import javax.microedition.khronos.opengles.GL11;
 
 /**
@@ -166,7 +168,7 @@
     @Override
     public void render(GLCanvas canvas) {
         AnimationController a = mAnimation;
-        if (a.calculate(canvas.currentAnimationTimeMillis())) invalidate();
+        if (a.calculate(AnimationTime.get())) invalidate();
         setImageViewPosition(a.getCenterX(), a.getCenterY(), a.getScale());
         super.render(canvas);
     }
@@ -756,8 +758,7 @@
         int rotation = mImageRotation;
         int width = bitmap.getWidth();
         int height = bitmap.getHeight();
-        float scale = (float) Math.sqrt(
-                (double) FACE_PIXEL_COUNT / (width * height));
+        float scale = FloatMath.sqrt((float) FACE_PIXEL_COUNT / (width * height));
 
         // faceBitmap is a correctly rotated bitmap, as viewed by a user.
         Bitmap faceBitmap;
diff --git a/src/com/android/gallery3d/ui/CustomMenu.java b/src/com/android/gallery3d/ui/CustomMenu.java
index de2367e..dd8e6ab 100644
--- a/src/com/android/gallery3d/ui/CustomMenu.java
+++ b/src/com/android/gallery3d/ui/CustomMenu.java
@@ -16,9 +16,6 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.R;
-
-import android.app.ActionBar;
 import android.content.Context;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -28,6 +25,8 @@
 import android.widget.PopupMenu;
 import android.widget.PopupMenu.OnMenuItemClickListener;
 
+import com.android.gallery3d.R;
+
 import java.util.ArrayList;
 
 public class CustomMenu implements OnMenuItemClickListener {
@@ -42,8 +41,6 @@
         public DropDownMenu(Context context, Button button, int menuId,
                 OnMenuItemClickListener listener) {
             mButton = button;
-            mButton.setBackgroundDrawable(context.getResources().getDrawable(
-                    R.drawable.dropdown_normal_holo_dark));
             mPopupMenu = new PopupMenu(context, mButton);
             mMenu = mPopupMenu.getMenu();
             mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
@@ -85,38 +82,6 @@
         mListener = listener;
     }
 
-    public MenuItem findMenuItem(int id) {
-        MenuItem item = null;
-        for (DropDownMenu menu : mMenus) {
-            item = menu.findItem(id);
-            if (item != null) return item;
-        }
-        return item;
-    }
-
-    public void setMenuItemAppliedEnabled(int id, boolean applied, boolean enabled,
-            boolean updateTitle) {
-        MenuItem item = null;
-        for (DropDownMenu menu : mMenus) {
-            item = menu.findItem(id);
-            if (item != null) {
-                item.setCheckable(true);
-                item.setChecked(applied);
-                item.setEnabled(enabled);
-                if (updateTitle) {
-                    menu.setTitle(item.getTitle());
-                }
-            }
-        }
-    }
-
-    public void setMenuItemVisibility(int id, boolean visibility) {
-        MenuItem item = findMenuItem(id);
-        if (item != null) {
-            item.setVisible(visibility);
-        }
-    }
-
     public boolean onMenuItemClick(MenuItem item) {
         if (mListener != null) {
             return mListener.onMenuItemClick(item);
diff --git a/src/com/android/gallery3d/ui/DetailsHelper.java b/src/com/android/gallery3d/ui/DetailsHelper.java
index 8f1cd0c..245145f 100644
--- a/src/com/android/gallery3d/ui/DetailsHelper.java
+++ b/src/com/android/gallery3d/ui/DetailsHelper.java
@@ -15,14 +15,14 @@
  */
 package com.android.gallery3d.ui;
 
+import android.content.Context;
+import android.view.View.MeasureSpec;
+
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.data.MediaDetails;
 import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener;
 
-import android.content.Context;
-import android.view.View.MeasureSpec;
-
 public class DetailsHelper {
     private static DetailsAddressResolver sAddressResolver;
     private DetailsViewContainer mContainer;
diff --git a/src/com/android/gallery3d/ui/DialogDetailsView.java b/src/com/android/gallery3d/ui/DialogDetailsView.java
index c037f2a..60ced03 100644
--- a/src/com/android/gallery3d/ui/DialogDetailsView.java
+++ b/src/com/android/gallery3d/ui/DialogDetailsView.java
@@ -80,10 +80,6 @@
         }
     }
 
-    public boolean isVisible() {
-        return mDialog.isShowing();
-    }
-
     private void setDetails(MediaDetails details) {
         mAdapter = new DetailsAdapter(details);
         String title = String.format(
@@ -173,8 +169,10 @@
                     default: {
                         Object valueObj = detail.getValue();
                         // This shouldn't happen, log its key to help us diagnose the problem.
-                        Utils.assertTrue(valueObj != null, "%s's value is Null",
-                                DetailsHelper.getDetailsName(context, detail.getKey()));
+                        if (valueObj == null) {
+                            Utils.fail("%s's value is Null",
+                                    DetailsHelper.getDetailsName(context, detail.getKey()));
+                        }
                         value = valueObj.toString();
                     }
                 }
diff --git a/src/com/android/gallery3d/ui/DisplayItem.java b/src/com/android/gallery3d/ui/DisplayItem.java
deleted file mode 100644
index 6b76ed0..0000000
--- a/src/com/android/gallery3d/ui/DisplayItem.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-public abstract class DisplayItem {
-
-    protected int mBoxWidth;
-    protected int mBoxHeight;
-
-    // setBox() specifies the box that the DisplayItem should render into. It
-    // should be called before first render(). It may be called again between
-    // render() calls to change the size of the box.
-    public void setBox(int width, int height) {
-        mBoxWidth = width;
-        mBoxHeight = height;
-    }
-
-    // Return values of render():
-    // RENDER_MORE_PASS: more pass is needed for this item
-    // RENDER_MORE_FRAME: need to render next frame (used for animation)
-    public static final int RENDER_MORE_PASS = 1;
-    public static final int RENDER_MORE_FRAME = 2;
-
-    public abstract int render(GLCanvas canvas, int pass);
-
-    public abstract long getIdentity();
-
-    public int getRotation() {
-        return 0;
-    }
-}
diff --git a/src/com/android/gallery3d/ui/DrawableTexture.java b/src/com/android/gallery3d/ui/DrawableTexture.java
deleted file mode 100644
index 5c3964d..0000000
--- a/src/com/android/gallery3d/ui/DrawableTexture.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-
-// DrawableTexture is a texture whose content is from a Drawable.
-public class DrawableTexture extends CanvasTexture {
-
-    private final Drawable mDrawable;
-
-    public DrawableTexture(Drawable drawable, int width, int height) {
-        super(width, height);
-        mDrawable = drawable;
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas, Bitmap backing) {
-        mDrawable.setBounds(0, 0, mWidth, mHeight);
-        mDrawable.draw(canvas);
-    }
-}
diff --git a/src/com/android/gallery3d/ui/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java
index b2d83f5..362b8fe 100644
--- a/src/com/android/gallery3d/ui/EdgeEffect.java
+++ b/src/com/android/gallery3d/ui/EdgeEffect.java
@@ -16,14 +16,14 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.R;
-
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Rect;
-import android.view.animation.AnimationUtils;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
+import com.android.gallery3d.R;
+
 // This is copied from android.widget.EdgeEffect with some small modifications:
 // (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
 // (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
@@ -179,7 +179,7 @@
      *                      back toward the edge reached to initiate the effect.
      */
     public void onPull(float deltaDistance) {
-        final long now = AnimationUtils.currentAnimationTimeMillis();
+        final long now = AnimationTime.get();
         if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
             return;
         }
@@ -244,7 +244,7 @@
         mGlowAlphaFinish = 0.f;
         mGlowScaleYFinish = 0.f;
 
-        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartTime = AnimationTime.get();
         mDuration = RECEDE_TIME;
     }
 
@@ -262,7 +262,7 @@
         mState = STATE_ABSORB;
         velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
 
-        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartTime = AnimationTime.get();
         mDuration = 0.1f + (velocity * 0.03f);
 
         // The edge should always be at least partially visible, regardless
@@ -343,7 +343,7 @@
     }
 
     private void update() {
-        final long time = AnimationUtils.currentAnimationTimeMillis();
+        final long time = AnimationTime.get();
         final float t = Math.min((time - mStartTime) / mDuration, 1.f);
 
         final float interp = mInterpolator.getInterpolation(t);
@@ -357,7 +357,7 @@
             switch (mState) {
                 case STATE_ABSORB:
                     mState = STATE_RECEDE;
-                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                    mStartTime = AnimationTime.get();
                     mDuration = RECEDE_TIME;
 
                     mEdgeAlphaStart = mEdgeAlpha;
@@ -373,7 +373,7 @@
                     break;
                 case STATE_PULL:
                     mState = STATE_PULL_DECAY;
-                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                    mStartTime = AnimationTime.get();
                     mDuration = PULL_DECAY_TIME;
 
                     mEdgeAlphaStart = mEdgeAlpha;
diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java
index db6a45c..bf97108 100644
--- a/src/com/android/gallery3d/ui/EdgeView.java
+++ b/src/com/android/gallery3d/ui/EdgeView.java
@@ -23,6 +23,7 @@
 public class EdgeView extends GLView {
     private static final String TAG = "EdgeView";
 
+    public static final int INVALID_DIRECTION = -1;
     public static final int TOP = 0;
     public static final int LEFT = 1;
     public static final int BOTTOM = 2;
diff --git a/src/com/android/gallery3d/ui/ExtTexture.java b/src/com/android/gallery3d/ui/ExtTexture.java
new file mode 100644
index 0000000..9319d8c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ExtTexture.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+// ExtTexture is a texture whose content comes from a external texture.
+// Before drawing, setSize() should be called.
+public class ExtTexture extends BasicTexture {
+
+    private static int[] sTextureId = new int[1];
+    private static float[] sCropRect = new float[4];
+    private int mTarget;
+
+    public ExtTexture(int target) {
+        GLId.glGenTextures(1, sTextureId, 0);
+        mId = sTextureId[0];
+        mTarget = target;
+    }
+
+    private void uploadToCanvas(GLCanvas canvas) {
+        GL11 gl = canvas.getGLInstance();
+
+        int width = getWidth();
+        int height = getHeight();
+        // Define a vertically flipped crop rectangle for OES_draw_texture.
+        // The four values in sCropRect are: left, bottom, width, and
+        // height. Negative value of width or height means flip.
+        sCropRect[0] = 0;
+        sCropRect[1] = height;
+        sCropRect[2] = width;
+        sCropRect[3] = -height;
+
+        // Set texture parameters.
+        gl.glBindTexture(mTarget, mId);
+        gl.glTexParameterfv(mTarget,
+                GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
+        gl.glTexParameteri(mTarget,
+                GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+        gl.glTexParameteri(mTarget,
+                GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+        gl.glTexParameterf(mTarget,
+                GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+        gl.glTexParameterf(mTarget,
+                GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+
+        setAssociatedCanvas(canvas);
+        mState = STATE_LOADED;
+    }
+
+    @Override
+    protected boolean onBind(GLCanvas canvas) {
+        if (!isLoaded()) {
+            uploadToCanvas(canvas);
+        }
+
+        return true;
+    }
+
+    @Override
+    public int getTarget() {
+        return mTarget;
+    }
+
+    public boolean isOpaque() {
+        return true;
+    }
+
+    @Override
+    public void yield() {
+        // we cannot free the texture because we have no backup.
+    }
+}
diff --git a/src/com/android/gallery3d/ui/FadeInTexture.java b/src/com/android/gallery3d/ui/FadeInTexture.java
index 1c44b5a..648bdcf 100644
--- a/src/com/android/gallery3d/ui/FadeInTexture.java
+++ b/src/com/android/gallery3d/ui/FadeInTexture.java
@@ -16,40 +16,20 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-
-import android.os.SystemClock;
-
 // FadeInTexture is a texture which begins with a color, then gradually animates
 // into a given texture.
-public class FadeInTexture implements Texture {
+public class FadeInTexture extends FadeTexture implements Texture {
+    @SuppressWarnings("unused")
     private static final String TAG = "FadeInTexture";
 
-    // The duration of the animation in milliseconds
-    private static final int DURATION = 180;
-
-    private final BasicTexture mTexture;
     private final int mColor;
-    private final long mStartTime;
-    private final int mWidth;
-    private final int mHeight;
-    private final boolean mIsOpaque;
-    private boolean mIsAnimating;
 
     public FadeInTexture(int color, BasicTexture texture) {
+        super(texture);
         mColor = color;
-        mTexture = texture;
-        mWidth = mTexture.getWidth();
-        mHeight = mTexture.getHeight();
-        mIsOpaque = mTexture.isOpaque();
-        mStartTime = now();
-        mIsAnimating = true;
     }
 
-    public void draw(GLCanvas canvas, int x, int y) {
-        draw(canvas, x, y, mWidth, mHeight);
-    }
-
+    @Override
     public void draw(GLCanvas canvas, int x, int y, int w, int h) {
         if (isAnimating()) {
             canvas.drawMixed(mTexture, mColor, getRatio(), x, y, w, h);
@@ -57,34 +37,4 @@
             mTexture.draw(canvas, x, y, w, h);
         }
     }
-
-    public boolean isOpaque() {
-        return mIsOpaque;
-    }
-
-    public int getWidth() {
-        return mWidth;
-    }
-
-    public int getHeight() {
-        return mHeight;
-    }
-
-    public boolean isAnimating() {
-        if (mIsAnimating) {
-            if (now() - mStartTime >= DURATION) {
-                mIsAnimating = false;
-            }
-        }
-        return mIsAnimating;
-    }
-
-    private float getRatio() {
-        float r = (float)(now() - mStartTime) / DURATION;
-        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
-    }
-
-    private long now() {
-        return SystemClock.uptimeMillis();
-    }
 }
diff --git a/src/com/android/gallery3d/ui/FadeOutTexture.java b/src/com/android/gallery3d/ui/FadeOutTexture.java
new file mode 100644
index 0000000..47e6acb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FadeOutTexture.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+// FadeOutTexture is a texture which begins with a given texture, then gradually animates
+// into fading out totally.
+public class FadeOutTexture extends FadeTexture {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FadeOutTexture";
+
+    public FadeOutTexture(BasicTexture texture) {
+        super(texture);
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+        if (isAnimating()) {
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.setAlpha(getRatio());
+            mTexture.draw(canvas, x, y, w, h);
+            canvas.restore();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/FadeTexture.java b/src/com/android/gallery3d/ui/FadeTexture.java
new file mode 100644
index 0000000..cbf5073
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FadeTexture.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.common.Utils;
+
+// FadeTexture is a texture which fades the given texture along the time.
+public abstract class FadeTexture implements Texture {
+    @SuppressWarnings("unused")
+    private static final String TAG = "FadeTexture";
+
+    // The duration of the fading animation in milliseconds
+    public static final int DURATION = 180;
+
+    protected final BasicTexture mTexture;
+    private final long mStartTime;
+    private final int mWidth;
+    private final int mHeight;
+    private final boolean mIsOpaque;
+    private boolean mIsAnimating;
+
+    public FadeTexture(BasicTexture texture) {
+        mTexture = texture;
+        mWidth = mTexture.getWidth();
+        mHeight = mTexture.getHeight();
+        mIsOpaque = mTexture.isOpaque();
+        mStartTime = now();
+        mIsAnimating = true;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y) {
+        draw(canvas, x, y, mWidth, mHeight);
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return mIsOpaque;
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    public boolean isAnimating() {
+        if (mIsAnimating) {
+            if (now() - mStartTime >= DURATION) {
+                mIsAnimating = false;
+            }
+        }
+        return mIsAnimating;
+    }
+
+    protected float getRatio() {
+        float r = (float)(now() - mStartTime) / DURATION;
+        return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+    }
+
+    private long now() {
+        return AnimationTime.get();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java
deleted file mode 100644
index e6ed49b..0000000
--- a/src/com/android/gallery3d/ui/FilmStripView.java
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.R;
-import com.android.gallery3d.anim.AlphaAnimation;
-import com.android.gallery3d.app.AlbumDataAdapter;
-import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.data.Path;
-
-import android.content.Context;
-import android.view.MotionEvent;
-import android.view.View.MeasureSpec;
-
-public class FilmStripView extends GLView implements ScrollBarView.Listener,
-        UserInteractionListener {
-    @SuppressWarnings("unused")
-    private static final String TAG = "FilmStripView";
-
-    private static final int HIDE_ANIMATION_DURATION = 300;  // 0.3 sec
-
-    public interface Listener {
-        // Returns false if it cannot jump to the specified index at this time.
-        boolean onSlotSelected(int slotIndex);
-    }
-
-    private int mTopMargin, mMidMargin, mBottomMargin;
-    private int mContentSize, mBarSize, mGripSize;
-    private AlbumView mAlbumView;
-    private ScrollBarView mScrollBarView;
-    private AlbumDataAdapter mAlbumDataAdapter;
-    private StripDrawer mStripDrawer;
-    private Listener mListener;
-    private UserInteractionListener mUIListener;
-    private NinePatchTexture mBackgroundTexture;
-
-    // The layout of FileStripView is
-    // topMargin
-    //             ----+----+
-    //            /    +----+--\
-    // contentSize     |    |   thumbSize
-    //            \    +----+--/
-    //             ----+----+
-    // midMargin
-    //             ----+----+
-    //            /    +----+--\
-    //     barSize     |    |   gripSize
-    //            \    +----+--/
-    //             ----+----+
-    // bottomMargin
-    public FilmStripView(GalleryActivity activity, MediaSet mediaSet,
-            int topMargin, int midMargin, int bottomMargin, int contentSize,
-            int thumbSize, int barSize, int gripSize, int gripWidth) {
-        mTopMargin = topMargin;
-        mMidMargin = midMargin;
-        mBottomMargin = bottomMargin;
-        mContentSize = contentSize;
-        mBarSize = barSize;
-        mGripSize = gripSize;
-
-        mStripDrawer = new StripDrawer((Context) activity);
-        SlotView.Spec spec = new SlotView.Spec();
-        spec.slotWidth = thumbSize;
-        spec.slotHeight = thumbSize;
-        mAlbumView = new AlbumView(activity, spec, thumbSize);
-        mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_NONE);
-        mAlbumView.setSelectionDrawer(mStripDrawer);
-        mAlbumView.setListener(new SlotView.SimpleListener() {
-            @Override
-            public void onDown(int index) {
-                FilmStripView.this.onDown(index);
-            }
-            @Override
-            public void onUp() {
-                FilmStripView.this.onUp();
-            }
-            @Override
-            public void onSingleTapUp(int slotIndex) {
-                FilmStripView.this.onSingleTapUp(slotIndex);
-            }
-            @Override
-            public void onLongTap(int slotIndex) {
-                FilmStripView.this.onLongTap(slotIndex);
-            }
-            @Override
-            public void onScrollPositionChanged(int position, int total) {
-                FilmStripView.this.onScrollPositionChanged(position, total);
-            }
-        });
-        mAlbumView.setUserInteractionListener(this);
-        mAlbumDataAdapter = new AlbumDataAdapter(activity, mediaSet);
-        addComponent(mAlbumView);
-        mScrollBarView = new ScrollBarView(activity.getAndroidContext(),
-                mGripSize, gripWidth);
-        mScrollBarView.setListener(this);
-        addComponent(mScrollBarView);
-
-        mAlbumView.setModel(mAlbumDataAdapter);
-        mBackgroundTexture = new NinePatchTexture(activity.getAndroidContext(),
-                R.drawable.navstrip_translucent);
-    }
-
-    public void setListener(Listener listener) {
-        mListener = listener;
-    }
-
-    public void setUserInteractionListener(UserInteractionListener listener) {
-        mUIListener = listener;
-    }
-
-    public void show() {
-        if (getVisibility() == GLView.VISIBLE) return;
-        startAnimation(null);
-        setVisibility(GLView.VISIBLE);
-    }
-
-    public void hide() {
-        if (getVisibility() == GLView.INVISIBLE) return;
-        AlphaAnimation animation = new AlphaAnimation(1, 0);
-        animation.setDuration(HIDE_ANIMATION_DURATION);
-        startAnimation(animation);
-        setVisibility(GLView.INVISIBLE);
-    }
-
-    @Override
-    protected void onVisibilityChanged(int visibility) {
-        super.onVisibilityChanged(visibility);
-        if (visibility == GLView.VISIBLE) {
-            onUserInteraction();
-        }
-    }
-
-    @Override
-    protected void onMeasure(int widthSpec, int heightSpec) {
-        int height = mTopMargin + mContentSize + mMidMargin + mBarSize + mBottomMargin;
-        MeasureHelper.getInstance(this)
-                .setPreferredContentSize(MeasureSpec.getSize(widthSpec), height)
-                .measure(widthSpec, heightSpec);
-    }
-
-    @Override
-    protected void onLayout(
-            boolean changed, int left, int top, int right, int bottom) {
-        if (!changed) return;
-        mAlbumView.layout(0, mTopMargin, right - left, mTopMargin + mContentSize);
-        int barStart = mTopMargin + mContentSize + mMidMargin;
-        mScrollBarView.layout(0, barStart, right - left, barStart + mBarSize);
-        int width = right - left;
-        int height = bottom - top;
-    }
-
-    @Override
-    protected boolean onTouch(MotionEvent event) {
-        // consume all touch events on the "gray area", so they don't go to
-        // the photo view below. (otherwise you can scroll the picture through
-        // it).
-        return true;
-    }
-
-    @Override
-    protected boolean dispatchTouchEvent(MotionEvent event) {
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-            case MotionEvent.ACTION_MOVE:
-                onUserInteractionBegin();
-                break;
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL:
-                onUserInteractionEnd();
-                break;
-        }
-
-        return super.dispatchTouchEvent(event);
-    }
-
-    @Override
-    protected void render(GLCanvas canvas) {
-        mBackgroundTexture.draw(canvas, 0, 0, getWidth(), getHeight());
-        super.render(canvas);
-    }
-
-    private void onDown(int index) {
-        MediaItem item = mAlbumDataAdapter.get(index);
-        Path path = (item == null) ? null : item.getPath();
-        mStripDrawer.setPressedPath(path);
-        mAlbumView.invalidate();
-    }
-
-    private void onUp() {
-        mStripDrawer.setPressedPath(null);
-        mAlbumView.invalidate();
-    }
-
-    private void onSingleTapUp(int slotIndex) {
-        if (mListener.onSlotSelected(slotIndex)) {
-            mAlbumView.setFocusIndex(slotIndex);
-        }
-    }
-
-    private void onLongTap(int slotIndex) {
-        onSingleTapUp(slotIndex);
-    }
-
-    private void onScrollPositionChanged(int position, int total) {
-        mScrollBarView.setContentPosition(position, total);
-    }
-
-    // Called by AlbumView
-    @Override
-    public void onUserInteractionBegin() {
-        mUIListener.onUserInteractionBegin();
-    }
-
-    // Called by AlbumView
-    @Override
-    public void onUserInteractionEnd() {
-        mUIListener.onUserInteractionEnd();
-    }
-
-    // Called by AlbumView
-    @Override
-    public void onUserInteraction() {
-        mUIListener.onUserInteraction();
-    }
-
-    // Called by ScrollBarView
-    @Override
-    public void onScrollBarPositionChanged(int position) {
-        mAlbumView.setScrollPosition(position);
-    }
-
-    public void setFocusIndex(int slotIndex) {
-        mAlbumView.setFocusIndex(slotIndex);
-        mAlbumView.makeSlotVisible(slotIndex);
-    }
-
-    public void setStartIndex(int slotIndex) {
-        mAlbumView.setStartIndex(slotIndex);
-    }
-
-    public void pause() {
-        mAlbumView.pause();
-        mAlbumDataAdapter.pause();
-    }
-
-    public void resume() {
-        mAlbumView.resume();
-        mAlbumDataAdapter.resume();
-    }
-}
diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java
index fbe27ce..1cddcdb 100644
--- a/src/com/android/gallery3d/ui/FlingScroller.java
+++ b/src/com/android/gallery3d/ui/FlingScroller.java
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
 
 // This is a customized version of Scroller, with a interface similar to
 // android.widget.Scroller. It does fling only, not scroll.
diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java
index 88c02f3..c12a9f7 100644
--- a/src/com/android/gallery3d/ui/GLCanvas.java
+++ b/src/com/android/gallery3d/ui/GLCanvas.java
@@ -37,14 +37,6 @@
     // Clear the drawing buffers. This should only be used by GLRoot.
     public void clearBuffer();
 
-    // This is the time value used to calculate the animation in the current
-    // frame. The "set" function should only called by GLRoot, and the
-    // "time" parameter must be nonnegative.
-    public void setCurrentAnimationTimeMillis(long time);
-    public long currentAnimationTimeMillis();
-
-    public void setBlendEnabled(boolean enabled);
-
     // Sets and gets the current alpha, alpha must be in [0, 1].
     public void setAlpha(float alpha);
     public float getAlpha();
@@ -54,26 +46,21 @@
 
     // Change the current transform matrix.
     public void translate(float x, float y, float z);
+    public void translate(float x, float y);
     public void scale(float sx, float sy, float sz);
     public void rotate(float angle, float x, float y, float z);
     public void multiplyMatrix(float[] mMatrix, int offset);
 
-    // Modifies the current clip with the specified rectangle.
-    // (current clip) = (current clip) intersect (specified rectangle).
-    // Returns true if the result clip is non-empty.
-    public boolean clipRect(int left, int top, int right, int bottom);
-
-    // Pushes the configuration state (matrix, alpha, and clip) onto
+    // Pushes the configuration state (matrix, and alpha) onto
     // a private stack.
-    public int save();
+    public void save();
 
     // Same as save(), but only save those specified in saveFlags.
-    public int save(int saveFlags);
+    public void save(int saveFlags);
 
     public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
-    public static final int SAVE_FLAG_CLIP = 0x01;
-    public static final int SAVE_FLAG_ALPHA = 0x02;
-    public static final int SAVE_FLAG_MATRIX = 0x04;
+    public static final int SAVE_FLAG_ALPHA = 0x01;
+    public static final int SAVE_FLAG_MATRIX = 0x02;
 
     // Pops from the top of the stack as current configuration state (matrix,
     // alpha, and clip). This call balances a previous call to save(), and is
@@ -98,26 +85,19 @@
     public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
             int uvBuffer, int indexBuffer, int indexCount);
 
-    // Draws a texture to the specified rectangle. The "alpha" parameter
-    // overrides the current drawing alpha value.
-    public void drawTexture(BasicTexture texture,
-            int x, int y, int width, int height, float alpha);
-
-    // Draws a the source rectangle part of the texture to the target rectangle.
+    // Draws the source rectangle part of the texture to the target rectangle.
     public void drawTexture(BasicTexture texture, RectF source, RectF target);
 
+    // Draw a texture with a specified texture transform.
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+                int x, int y, int w, int h);
+
     // Draw two textures to the specified rectangle. The actual texture used is
     // from * (1 - ratio) + to * ratio
     // The two textures must have the same size.
-    public void drawMixed(BasicTexture from, BasicTexture to,
-            float ratio, int x, int y, int w, int h);
-
     public void drawMixed(BasicTexture from, int toColor,
             float ratio, int x, int y, int w, int h);
 
-    // Return a texture copied from the specified rectangle.
-    public BasicTexture copyTexture(int x, int y, int width, int height);
-
     // Gets the underlying GL instance. This is used only when direct access to
     // GL is needed.
     public GL11 getGLInstance();
@@ -135,4 +115,10 @@
     // called in the GL thread.
     public void deleteRecycledResources();
 
+    // Dump statistics information and clear the counters. For debug only.
+    public void dumpStatisticsAndClear();
+
+    public void beginRenderTarget(RawTexture texture);
+
+    public void endRenderTarget();
 }
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
index 612c7c4..ff3e9e5 100644
--- a/src/com/android/gallery3d/ui/GLCanvasImpl.java
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -16,21 +16,22 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.IntArray;
-
-import android.graphics.Rect;
 import android.graphics.RectF;
 import android.opengl.GLU;
 import android.opengl.Matrix;
 
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.IntArray;
+
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.FloatBuffer;
-import java.util.Stack;
+import java.util.ArrayList;
+
 import javax.microedition.khronos.opengles.GL10;
 import javax.microedition.khronos.opengles.GL11;
 import javax.microedition.khronos.opengles.GL11Ext;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
 
 public class GLCanvasImpl implements GLCanvas {
     @SuppressWarnings("unused")
@@ -51,21 +52,19 @@
     private final float mMatrixValues[] = new float[16];
     private final float mTextureMatrixValues[] = new float[16];
 
-    // mapPoints needs 10 input and output numbers.
-    private final float mMapPointsBuffer[] = new float[10];
+    // The results of mapPoints are stored in this buffer, and the order is
+    // x1, y1, x2, y2.
+    private final float mMapPointsBuffer[] = new float[4];
 
     private final float mTextureColor[] = new float[4];
 
     private int mBoxCoords;
 
     private final GLState mGLState;
-
-    private long mAnimationTime;
+    private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>();
 
     private float mAlpha;
-    private final Rect mClipRect = new Rect();
-    private final Stack<ConfigState> mRestoreStack =
-            new Stack<ConfigState>();
+    private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
     private ConfigState mRecycledRestoreAction;
 
     private final RectF mDrawTextureSourceRect = new RectF();
@@ -73,8 +72,12 @@
     private final float[] mTempMatrix = new float[32];
     private final IntArray mUnboundTextures = new IntArray();
     private final IntArray mDeleteBuffers = new IntArray();
-    private int mHeight;
+    private int mScreenWidth;
+    private int mScreenHeight;
     private boolean mBlendEnabled = true;
+    private int mFrameBuffer[] = new int[1];
+
+    private RawTexture mTargetTexture;
 
     // Drawing statistics
     int mCountDrawLine;
@@ -91,7 +94,12 @@
 
     public void setSize(int width, int height) {
         Utils.assertTrue(width >= 0 && height >= 0);
-        mHeight = height;
+
+        if (mTargetTexture == null) {
+            mScreenWidth = width;
+            mScreenHeight = height;
+        }
+        mAlpha = 1.0f;
 
         GL11 gl = mGL;
         gl.glViewport(0, 0, width, height);
@@ -101,18 +109,14 @@
 
         gl.glMatrixMode(GL11.GL_MODELVIEW);
         gl.glLoadIdentity();
+
         float matrix[] = mMatrixValues;
-
         Matrix.setIdentityM(matrix, 0);
-        Matrix.translateM(matrix, 0, 0, mHeight, 0);
-        Matrix.scaleM(matrix, 0, 1, -1, 1);
-
-        mClipRect.set(0, 0, width, height);
-        gl.glScissor(0, 0, width, height);
-    }
-
-    public long currentAnimationTimeMillis() {
-        return mAnimationTime;
+        // to match the graphic coordinate system in android, we flip it vertically.
+        if (mTargetTexture == null) {
+            Matrix.translateM(matrix, 0, 0, height, 0);
+            Matrix.scaleM(matrix, 0, 1, -1, 1);
+        }
     }
 
     public void setAlpha(float alpha) {
@@ -120,15 +124,15 @@
         mAlpha = alpha;
     }
 
+    public float getAlpha() {
+        return mAlpha;
+    }
+
     public void multiplyAlpha(float alpha) {
         Utils.assertTrue(alpha >= 0 && alpha <= 1);
         mAlpha *= alpha;
     }
 
-    public float getAlpha() {
-        return mAlpha;
-    }
-
     private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
         return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
     }
@@ -142,7 +146,7 @@
         xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);
 
         int[] name = new int[1];
-        gl.glGenBuffers(1, name, 0);
+        GLId.glGenBuffers(1, name, 0);
         mBoxCoords = name[0];
 
         gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
@@ -159,8 +163,7 @@
         gl.glClientActiveTexture(GL11.GL_TEXTURE0);
         gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
 
-        // mMatrixValues will be initialized in setSize()
-        mAlpha = 1.0f;
+        // mMatrixValues and mAlpha will be initialized in setSize()
     }
 
     public void drawRect(float x, float y, float width, float height, GLPaint paint) {
@@ -168,10 +171,9 @@
 
         mGLState.setColorMode(paint.getColor(), mAlpha);
         mGLState.setLineWidth(paint.getLineWidth());
-        mGLState.setLineSmooth(paint.getAntiAlias());
 
         saveTransform();
-        translate(x, y, 0);
+        translate(x, y);
         scale(width, height, 1);
 
         gl.glLoadMatrixf(mMatrixValues, 0);
@@ -186,10 +188,9 @@
 
         mGLState.setColorMode(paint.getColor(), mAlpha);
         mGLState.setLineWidth(paint.getLineWidth());
-        mGLState.setLineSmooth(paint.getAntiAlias());
 
         saveTransform();
-        translate(x1, y1, 0);
+        translate(x1, y1);
         scale(x2 - x1, y2 - y1, 1);
 
         gl.glLoadMatrixf(mMatrixValues, 0);
@@ -204,7 +205,7 @@
         GL11 gl = mGL;
 
         saveTransform();
-        translate(x, y, 0);
+        translate(x, y);
         scale(width, height, 1);
 
         gl.glLoadMatrixf(mMatrixValues, 0);
@@ -218,11 +219,23 @@
         Matrix.translateM(mMatrixValues, 0, x, y, z);
     }
 
+    // This is a faster version of translate(x, y, z) because
+    // (1) we knows z = 0, (2) we inline the Matrix.translateM call,
+    // (3) we unroll the loop
+    public void translate(float x, float y) {
+        float[] m = mMatrixValues;
+        m[12] += m[0] * x + m[4] * y;
+        m[13] += m[1] * x + m[5] * y;
+        m[14] += m[2] * x + m[6] * y;
+        m[15] += m[3] * x + m[7] * y;
+    }
+
     public void scale(float sx, float sy, float sz) {
         Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
     }
 
     public void rotate(float angle, float x, float y, float z) {
+        if (angle == 0) return;
         float[] temp = mTempMatrix;
         Matrix.setRotateM(temp, 0, angle, x, y, z);
         Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);
@@ -239,7 +252,7 @@
         GL11 gl = mGL;
 
         saveTransform();
-        translate(x, y, 0);
+        translate(x, y);
         scale(width, height, 1);
 
         gl.glLoadMatrixf(mMatrixValues, 0);
@@ -263,7 +276,7 @@
         setTextureCoords(0, 0, 1, 1);
 
         saveTransform();
-        translate(x, y, 0);
+        translate(x, y);
 
         mGL.glLoadMatrixf(mMatrixValues, 0);
 
@@ -285,55 +298,26 @@
         mCountDrawMesh++;
     }
 
-    private float[] mapPoints(float matrix[], int x1, int y1, int x2, int y2) {
-        float[] point = mMapPointsBuffer;
-        int srcOffset = 6;
-        point[srcOffset] = x1;
-        point[srcOffset + 1] = y1;
-        point[srcOffset + 2] = 0;
-        point[srcOffset + 3] = 1;
+    // Transforms two points by the given matrix m. The result
+    // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.
+    private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) {
+        float[] r = mMapPointsBuffer;
 
-        int resultOffset = 0;
-        Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset);
-        point[resultOffset] /= point[resultOffset + 3];
-        point[resultOffset + 1] /= point[resultOffset + 3];
+        // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.
+        float x3 = m[0] * x1 + m[4] * y1 + m[12];
+        float y3 = m[1] * x1 + m[5] * y1 + m[13];
+        float w3 = m[3] * x1 + m[7] * y1 + m[15];
+        r[0] = x3 / w3;
+        r[1] = y3 / w3;
 
-        // map the second point
-        point[srcOffset] = x2;
-        point[srcOffset + 1] = y2;
-        resultOffset = 2;
-        Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset);
-        point[resultOffset] /= point[resultOffset + 3];
-        point[resultOffset + 1] /= point[resultOffset + 3];
+        // Same for x2 y2.
+        float x4 = m[0] * x2 + m[4] * y2 + m[12];
+        float y4 = m[1] * x2 + m[5] * y2 + m[13];
+        float w4 = m[3] * x2 + m[7] * y2 + m[15];
+        r[2] = x4 / w4;
+        r[3] = y4 / w4;
 
-        return point;
-    }
-
-    public boolean clipRect(int left, int top, int right, int bottom) {
-        float point[] = mapPoints(mMatrixValues, left, top, right, bottom);
-
-        // mMatrix could be a rotation matrix. In this case, we need to find
-        // the boundaries after rotation. (only handle 90 * n degrees)
-        if (point[0] > point[2]) {
-            left = (int) point[2];
-            right = (int) point[0];
-        } else {
-            left = (int) point[0];
-            right = (int) point[2];
-        }
-        if (point[1] > point[3]) {
-            top = (int) point[3];
-            bottom = (int) point[1];
-        } else {
-            top = (int) point[1];
-            bottom = (int) point[3];
-        }
-        Rect clip = mClipRect;
-
-        boolean intersect = clip.intersect(left, top, right, bottom);
-        if (!intersect) clip.set(0, 0, 0, 0);
-        mGL.glScissor(clip.left, clip.top, clip.width(), clip.height());
-        return intersect;
+        return r;
     }
 
     private void drawBoundTexture(
@@ -357,10 +341,10 @@
             // draw the rect from bottom-left to top-right
             float points[] = mapPoints(
                     mMatrixValues, x, y + height, x + width, y);
-            x = Math.round(points[0]);
-            y = Math.round(points[1]);
-            width = Math.round(points[2]) - x;
-            height = Math.round(points[3]) - y;
+            x = (int) (points[0] + 0.5f);
+            y = (int) (points[1] + 0.5f);
+            width = (int) (points[2] + 0.5f) - x;
+            height = (int) (points[3] + 0.5f) - y;
             if (width > 0 && height > 0) {
                 ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);
                 mCountTextureOES++;
@@ -373,11 +357,7 @@
         drawTexture(texture, x, y, width, height, mAlpha);
     }
 
-    public void setBlendEnabled(boolean enabled) {
-        mBlendEnabled = enabled;
-    }
-
-    public void drawTexture(BasicTexture texture,
+    private void drawTexture(BasicTexture texture,
             int x, int y, int width, int height, float alpha) {
         if (width <= 0 || height <= 0) return;
 
@@ -406,6 +386,16 @@
         textureRect(target.left, target.top, target.width(), target.height());
     }
 
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+            int x, int y, int w, int h) {
+        mGLState.setBlendEnabled(mBlendEnabled
+                && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
+        if (!bindTexture(texture)) return;
+        setTextureCoords(mTextureTransform);
+        mGLState.setTextureAlpha(mAlpha);
+        textureRect(x, y, w, h);
+    }
+
     // This function changes the source coordinate to the texture coordinates.
     // It also clips the source and target coordinates if it is beyond the
     // bound of the texture.
@@ -442,15 +432,11 @@
         drawMixed(from, toColor, ratio, x, y, w, h, mAlpha);
     }
 
-    public void drawMixed(BasicTexture from, BasicTexture to,
-            float ratio, int x, int y, int w, int h) {
-        drawMixed(from, to, ratio, x, y, w, h, mAlpha);
-    }
-
     private boolean bindTexture(BasicTexture texture) {
         if (!texture.onBind(this)) return false;
-        mGLState.setTexture2DEnabled(true);
-        mGL.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId());
+        int target = texture.getTarget();
+        mGLState.setTextureTarget(target);
+        mGL.glBindTexture(target, texture.getId());
         return true;
     }
 
@@ -464,8 +450,8 @@
 
     private void drawMixed(BasicTexture from, int toColor,
             float ratio, int x, int y, int width, int height, float alpha) {
-
-        if (ratio <= 0) {
+        // change from 0 to 0.01f to prevent getting divided by zero below
+        if (ratio <= 0.01f) {
             drawTexture(from, x, y, width, height, alpha);
             return;
         } else if (ratio >= 1) {
@@ -482,30 +468,26 @@
         //
         // The formula we want:
         //     alpha * ((1 - ratio) * from + ratio * to)
+        //
         // The formula that GL supports is in the form of:
-        //     combo * (modulate * from) + (1 - combo) * to
+        //     combo * from + (1 - combo) * to * scale
         //
-        // So, we have combo = 1 - alpha * ratio
-        //     and     modulate = alpha * (1f - ratio) / combo
+        // So, we have combo = alpha * (1 - ratio)
+        //     and     scale = alpha * ratio / (1 - combo)
         //
-        float comboRatio = 1 - alpha * ratio;
-
-        // handle the case that (1 - comboRatio) == 0
-        if (alpha < OPAQUE_ALPHA) {
-            mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio);
-        } else {
-            mGLState.setTextureAlpha(1f);
-        }
+        float combo = alpha * (1 - ratio);
+        float scale = alpha * ratio / (1 - combo);
 
         // Interpolate the RGB and alpha values between both textures.
         mGLState.setTexEnvMode(GL11.GL_COMBINE);
+
         // Specify the interpolation factor via the alpha component of
         // GL_TEXTURE_ENV_COLORs.
         // RGB component are get from toColor and will used as SRC1
-        float colorAlpha = (float) (toColor >>> 24) / (0xff * 0xff);
-        setTextureColor(((toColor >>> 16) & 0xff) * colorAlpha,
-                ((toColor >>> 8) & 0xff) * colorAlpha,
-                (toColor & 0xff) * colorAlpha, comboRatio);
+        float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
+        setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
+                ((toColor >>> 8) & 0xff) * colorScale,
+                (toColor & 0xff) * colorScale, combo);
         gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
 
         gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
@@ -527,84 +509,6 @@
         mGLState.setTexEnvMode(GL11.GL_REPLACE);
     }
 
-    private void drawMixed(BasicTexture from, BasicTexture to,
-            float ratio, int x, int y, int width, int height, float alpha) {
-
-        if (ratio <= 0) {
-            drawTexture(from, x, y, width, height, alpha);
-            return;
-        } else if (ratio >= 1) {
-            drawTexture(to, x, y, width, height, alpha);
-            return;
-        }
-
-        // In the current implementation the two textures must have the
-        // same size.
-        Utils.assertTrue(from.getWidth() == to.getWidth()
-                && from.getHeight() == to.getHeight());
-
-        mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
-                || !to.isOpaque() || alpha < OPAQUE_ALPHA));
-
-        final GL11 gl = mGL;
-        if (!bindTexture(from)) return;
-
-        //
-        // The formula we want:
-        //     alpha * ((1 - ratio) * from + ratio * to)
-        // The formula that GL supports is in the form of:
-        //     combo * (modulate * from) + (1 - combo) * to
-        //
-        // So, we have combo = 1 - alpha * ratio
-        //     and     modulate = alpha * (1f - ratio) / combo
-        //
-        float comboRatio = 1 - alpha * ratio;
-
-        // handle the case that (1 - comboRatio) == 0
-        if (alpha < OPAQUE_ALPHA) {
-            mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio);
-        } else {
-            mGLState.setTextureAlpha(1f);
-        }
-
-        gl.glActiveTexture(GL11.GL_TEXTURE1);
-        if (!bindTexture(to)) {
-            // Disable TEXTURE1.
-            gl.glDisable(GL11.GL_TEXTURE_2D);
-            // Switch back to the default texture unit.
-            gl.glActiveTexture(GL11.GL_TEXTURE0);
-            return;
-        }
-        gl.glEnable(GL11.GL_TEXTURE_2D);
-
-        // Interpolate the RGB and alpha values between both textures.
-        mGLState.setTexEnvMode(GL11.GL_COMBINE);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
-
-        // Specify the interpolation factor via the alpha component of
-        // GL_TEXTURE_ENV_COLORs.
-        // We don't use the RGB color, so just give them 0s.
-        setTextureColor(0, 0, 0, comboRatio);
-        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
-
-        // Wire up the interpolation factor for RGB.
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
-
-        // Wire up the interpolation factor for alpha.
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
-        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
-
-        // Draw the combined texture.
-        drawBoundTexture(to, x, y, width, height);
-
-        // Disable TEXTURE1.
-        gl.glDisable(GL11.GL_TEXTURE_2D);
-        // Switch back to the default texture unit.
-        gl.glActiveTexture(GL11.GL_TEXTURE0);
-    }
-
     // TODO: the code only work for 2D should get fixed for 3D or removed
     private static final int MSKEW_X = 4;
     private static final int MSKEW_Y = 1;
@@ -619,48 +523,13 @@
                 || matrix[MSCALE_Y] > eps;
     }
 
-    public BasicTexture copyTexture(int x, int y, int width, int height) {
-
-        if (isMatrixRotatedOrFlipped(mMatrixValues)) {
-            throw new IllegalArgumentException("cannot support rotated matrix");
-        }
-        float points[] = mapPoints(mMatrixValues, x, y + height, x + width, y);
-        x = (int) points[0];
-        y = (int) points[1];
-        width = (int) points[2] - x;
-        height = (int) points[3] - y;
-
-        GL11 gl = mGL;
-
-        RawTexture texture = RawTexture.newInstance(this);
-        gl.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId());
-        texture.setSize(width, height);
-
-        int[] cropRect = {0,  0, width, height};
-        gl.glTexParameteriv(GL11.GL_TEXTURE_2D,
-                GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0);
-        gl.glTexParameteri(GL11.GL_TEXTURE_2D,
-                GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
-        gl.glTexParameteri(GL11.GL_TEXTURE_2D,
-                GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
-        gl.glTexParameterf(GL11.GL_TEXTURE_2D,
-                GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
-        gl.glTexParameterf(GL11.GL_TEXTURE_2D,
-                GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
-        gl.glCopyTexImage2D(GL11.GL_TEXTURE_2D, 0,
-                GL11.GL_RGB, x, y, texture.getTextureWidth(),
-                texture.getTextureHeight(), 0);
-
-        return texture;
-    }
-
     private static class GLState {
 
         private final GL11 mGL;
 
         private int mTexEnvMode = GL11.GL_REPLACE;
         private float mTextureAlpha = 1.0f;
-        private boolean mTexture2DEnabled = true;
+        private int mTextureTarget = 0;
         private boolean mBlendEnabled = true;
         private float mLineWidth = 1.0f;
         private boolean mLineSmooth = false;
@@ -673,7 +542,6 @@
 
             // Enable used features
             gl.glEnable(GL11.GL_DITHER);
-            gl.glEnable(GL11.GL_SCISSOR_TEST);
 
             gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
             gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
@@ -705,16 +573,6 @@
             mGL.glLineWidth(width);
         }
 
-        public void setLineSmooth(boolean enabled) {
-            if (mLineSmooth == enabled) return;
-            mLineSmooth = enabled;
-            if (enabled) {
-                mGL.glEnable(GL11.GL_LINE_SMOOTH);
-            } else {
-                mGL.glDisable(GL11.GL_LINE_SMOOTH);
-            }
-        }
-
         public void setTextureAlpha(float alpha) {
             if (mTextureAlpha == alpha) return;
             mTextureAlpha = alpha;
@@ -735,7 +593,7 @@
             // again in setTextureAlpha(float) later.
             mTextureAlpha = -1.0f;
 
-            setTexture2DEnabled(false);
+            setTextureTarget(0);
 
             float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
             mGL.glColor4x(
@@ -745,13 +603,15 @@
                     Math.round(255 * prealpha));
         }
 
-        public void setTexture2DEnabled(boolean enabled) {
-            if (mTexture2DEnabled == enabled) return;
-            mTexture2DEnabled = enabled;
-            if (enabled) {
-                mGL.glEnable(GL11.GL_TEXTURE_2D);
-            } else {
-                mGL.glDisable(GL11.GL_TEXTURE_2D);
+        // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled.
+        public void setTextureTarget(int target) {
+            if (mTextureTarget == target) return;
+            if (mTextureTarget != 0) {
+                mGL.glDisable(mTextureTarget);
+            }
+            mTextureTarget = target;
+            if (mTextureTarget != 0) {
+                mGL.glEnable(mTextureTarget);
             }
         }
 
@@ -770,11 +630,6 @@
         return mGL;
     }
 
-    public void setCurrentAnimationTimeMillis(long time) {
-        Utils.assertTrue(time >= 0);
-        mAnimationTime = time;
-    }
-
     public void clearBuffer() {
         mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
     }
@@ -796,11 +651,17 @@
         mGL.glMatrixMode(GL11.GL_MODELVIEW);
     }
 
+    private void setTextureCoords(float[] mTextureTransform) {
+        mGL.glMatrixMode(GL11.GL_TEXTURE);
+        mGL.glLoadMatrixf(mTextureTransform, 0);
+        mGL.glMatrixMode(GL11.GL_MODELVIEW);
+    }
+
     // unloadTexture and deleteBuffer can be called from the finalizer thread,
     // so we synchronized on the mUnboundTextures object.
     public boolean unloadTexture(BasicTexture t) {
         synchronized (mUnboundTextures) {
-            if (!t.isLoaded(this)) return false;
+            if (!t.isLoaded()) return false;
             mUnboundTextures.add(t.mId);
             return true;
         }
@@ -816,23 +677,23 @@
         synchronized (mUnboundTextures) {
             IntArray ids = mUnboundTextures;
             if (ids.size() > 0) {
-                mGL.glDeleteTextures(ids.size(), ids.getInternalArray(), 0);
+                GLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0);
                 ids.clear();
             }
 
             ids = mDeleteBuffers;
             if (ids.size() > 0) {
-                mGL.glDeleteBuffers(ids.size(), ids.getInternalArray(), 0);
+                GLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0);
                 ids.clear();
             }
         }
     }
 
-    public int save() {
-        return save(SAVE_FLAG_ALL);
+    public void save() {
+        save(SAVE_FLAG_ALL);
     }
 
-    public int save(int saveFlags) {
+    public void save(int saveFlags) {
         ConfigState config = obtainRestoreConfig();
 
         if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
@@ -841,11 +702,6 @@
             config.mAlpha = -1;
         }
 
-        if ((saveFlags & SAVE_FLAG_CLIP) != 0) {
-            config.mRect.set(mClipRect);
-        } else {
-            config.mRect.left = Integer.MAX_VALUE;
-        }
 
         if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
             System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
@@ -853,13 +709,12 @@
             config.mMatrix[0] = Float.NEGATIVE_INFINITY;
         }
 
-        mRestoreStack.push(config);
-        return mRestoreStack.size() - 1;
+        mRestoreStack.add(config);
     }
 
     public void restore() {
         if (mRestoreStack.isEmpty()) throw new IllegalStateException();
-        ConfigState config = mRestoreStack.pop();
+        ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1);
         config.restore(this);
         freeRestoreConfig(config);
     }
@@ -880,18 +735,11 @@
 
     private static class ConfigState {
         float mAlpha;
-        Rect mRect = new Rect();
         float mMatrix[] = new float[16];
         ConfigState mNextFree;
 
         public void restore(GLCanvasImpl canvas) {
             if (mAlpha >= 0) canvas.setAlpha(mAlpha);
-            if (mRect.left != Integer.MAX_VALUE) {
-                Rect rect = mRect;
-                canvas.mClipRect.set(rect);
-                canvas.mGL.glScissor(
-                        rect.left, rect.top, rect.width(), rect.height());
-            }
             if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
                 System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
             }
@@ -918,4 +766,79 @@
     private void restoreTransform() {
         System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);
     }
+
+    private void setRenderTarget(RawTexture texture) {
+        GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL;
+
+        if (mTargetTexture == null && texture != null) {
+            GLId.glGenBuffers(1, mFrameBuffer, 0);
+            gl11ep.glBindFramebufferOES(
+                    GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]);
+        }
+        if (mTargetTexture != null && texture  == null) {
+            gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0);
+            gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0);
+        }
+
+        mTargetTexture = texture;
+        if (texture == null) {
+            setSize(mScreenWidth, mScreenHeight);
+        } else {
+            setSize(texture.getWidth(), texture.getHeight());
+
+            if (!texture.isLoaded()) texture.prepare(this);
+
+            gl11ep.glFramebufferTexture2DOES(
+                    GL11ExtensionPack.GL_FRAMEBUFFER_OES,
+                    GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,
+                    GL11.GL_TEXTURE_2D, texture.getId(), 0);
+
+            checkFramebufferStatus(gl11ep);
+        }
+    }
+
+    @Override
+    public void endRenderTarget() {
+        RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1);
+        setRenderTarget(texture);
+        restore(); // restore matrix and alpha
+    }
+
+    @Override
+    public void beginRenderTarget(RawTexture texture) {
+        save(); // save matrix and alpha
+        mTargetStack.add(mTargetTexture);
+        setRenderTarget(texture);
+    }
+
+    private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) {
+        int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES);
+        if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) {
+            String msg = "";
+            switch (status) {
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
+                    msg = "FRAMEBUFFER_FORMATS";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
+                    msg = "FRAMEBUFFER_ATTACHMENT";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
+                    msg = "FRAMEBUFFER_MISSING_ATTACHMENT";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES:
+                    msg = "FRAMEBUFFER_DRAW_BUFFER";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES:
+                    msg = "FRAMEBUFFER_READ_BUFFER";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES:
+                    msg = "FRAMEBUFFER_UNSUPPORTED";
+                    break;
+                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
+                    msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
+                    break;
+            }
+            throw new RuntimeException(msg + ":" + Integer.toHexString(status));
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/ui/GLId.java b/src/com/android/gallery3d/ui/GLId.java
new file mode 100644
index 0000000..689cf19
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLId.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+// This mimics corresponding GL functions.
+public class GLId {
+    static int sNextId = 1;
+
+    public synchronized static void glGenTextures(int n, int[] textures, int offset) {
+        while (n-- > 0) {
+            textures[offset + n] = sNextId++;
+        }
+    }
+
+    public synchronized static void glGenBuffers(int n, int[] buffers, int offset) {
+        while (n-- > 0) {
+            buffers[offset + n] = sNextId++;
+        }
+    }
+
+    public synchronized static void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
+        gl.glDeleteTextures(n, textures, offset);
+    }
+
+    public synchronized static void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
+        gl.glDeleteBuffers(n, buffers, offset);
+    }
+
+    public synchronized static void glDeleteFramebuffers(
+            GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
+        gl11ep.glDeleteFramebuffersOES(n, buffers, offset);
+    }
+}
diff --git a/src/com/android/gallery3d/ui/GLPaint.java b/src/com/android/gallery3d/ui/GLPaint.java
index 9f7b6f1..eb75cc5 100644
--- a/src/com/android/gallery3d/ui/GLPaint.java
+++ b/src/com/android/gallery3d/ui/GLPaint.java
@@ -20,20 +20,9 @@
 
 
 public class GLPaint {
-    public static final int FLAG_ANTI_ALIAS = 0x01;
-
-    private int mFlags = 0;
     private float mLineWidth = 1f;
     private int mColor = 0;
 
-    public int getFlags() {
-        return mFlags;
-    }
-
-    public void setFlags(int flags) {
-        mFlags = flags;
-    }
-
     public void setColor(int color) {
         mColor = color;
     }
@@ -50,16 +39,4 @@
     public float getLineWidth() {
         return mLineWidth;
     }
-
-    public void setAntiAlias(boolean enabled) {
-        if (enabled) {
-            mFlags |= FLAG_ANTI_ALIAS;
-        } else {
-            mFlags &= ~FLAG_ANTI_ALIAS;
-        }
-    }
-
-    public boolean getAntiAlias(){
-        return (mFlags & FLAG_ANTI_ALIAS) != 0;
-    }
 }
diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java
index 24e5794..1651b43 100644
--- a/src/com/android/gallery3d/ui/GLRoot.java
+++ b/src/com/android/gallery3d/ui/GLRoot.java
@@ -16,22 +16,33 @@
 
 package com.android.gallery3d.ui;
 
+import android.graphics.Matrix;
+
 import com.android.gallery3d.anim.CanvasAnimation;
 
 public interface GLRoot {
 
+    // Listener will be called when GL is idle AND before each frame.
+    // Mainly used for uploading textures.
     public static interface OnGLIdleListener {
-        public boolean onGLIdle(GLRoot root, GLCanvas canvas);
+        public boolean onGLIdle(
+                GLCanvas canvas, boolean renderRequested);
     }
 
     public void addOnGLIdleListener(OnGLIdleListener listener);
     public void registerLaunchedAnimation(CanvasAnimation animation);
     public void requestRender();
     public void requestLayoutContentPane();
-    public boolean hasStencil();
 
     public void lockRenderThread();
     public void unlockRenderThread();
 
     public void setContentPane(GLView content);
+    public void setOrientationSource(OrientationSource source);
+    public int getDisplayRotation();
+    public int getCompensation();
+    public Matrix getCompensationMatrix();
+    public void freeze();
+    public void unfreeze();
+    public void setLightsOutMode(boolean enabled);
 }
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
index f2140bf..99ed8cb 100644
--- a/src/com/android/gallery3d/ui/GLRootView.java
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -16,24 +16,28 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.anim.CanvasAnimation;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.GalleryUtils;
-
-import android.app.Activity;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.PixelFormat;
-import android.graphics.Rect;
 import android.opengl.GLSurfaceView;
 import android.os.Process;
 import android.os.SystemClock;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.Profile;
+
+import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.LinkedList;
+import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.ReentrantLock;
+
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 import javax.microedition.khronos.opengles.GL11;
@@ -59,37 +63,50 @@
 
     private static final boolean DEBUG_DRAWING_STAT = false;
 
+    private static final boolean DEBUG_PROFILE = false;
+    private static final boolean DEBUG_PROFILE_SLOW_ONLY = false;
+
     private static final int FLAG_INITIALIZED = 1;
     private static final int FLAG_NEED_LAYOUT = 2;
 
     private GL11 mGL;
-    private GLCanvasImpl mCanvas;
-
+    private GLCanvas mCanvas;
     private GLView mContentView;
-    private DisplayMetrics mDisplayMetrics;
+
+    private OrientationSource mOrientationSource;
+    // mCompensation is the difference between the UI orientation on GLCanvas
+    // and the framework orientation. See OrientationManager for details.
+    private int mCompensation;
+    // mCompensationMatrix maps the coordinates of touch events. It is kept sync
+    // with mCompensation.
+    private Matrix mCompensationMatrix = new Matrix();
+    private int mDisplayRotation;
+
+    // The value which will become mCompensation in next layout.
+    private int mPendingCompensation;
 
     private int mFlags = FLAG_NEED_LAYOUT;
     private volatile boolean mRenderRequested = false;
 
-    private Rect mClipRect = new Rect();
-    private int mClipRetryCount = 0;
-
     private final GalleryEGLConfigChooser mEglConfigChooser =
             new GalleryEGLConfigChooser();
 
     private final ArrayList<CanvasAnimation> mAnimations =
             new ArrayList<CanvasAnimation>();
 
-    private final LinkedList<OnGLIdleListener> mIdleListeners =
-            new LinkedList<OnGLIdleListener>();
+    private final ArrayDeque<OnGLIdleListener> mIdleListeners =
+            new ArrayDeque<OnGLIdleListener>();
 
     private final IdleRunner mIdleRunner = new IdleRunner();
 
     private final ReentrantLock mRenderLock = new ReentrantLock();
+    private final Condition mFreezeCondition =
+            mRenderLock.newCondition();
+    private boolean mFreeze;
 
-    private static final int TARGET_FRAME_TIME = 16;
     private long mLastDrawFinishTime;
     private boolean mInDownState = false;
+    private boolean mFirstDraw = true;
 
     public GLRootView(Context context) {
         this(context, null);
@@ -104,16 +121,7 @@
         getHolder().setFormat(PixelFormat.RGB_565);
 
         // Uncomment this to enable gl error check.
-        //setDebugFlags(DEBUG_CHECK_GL_ERROR);
-    }
-
-    public GalleryEGLConfigChooser getEGLConfigChooser() {
-        return mEglConfigChooser;
-    }
-
-    @Override
-    public boolean hasStencil() {
-        return getEGLConfigChooser().getStencilBits() > 0;
+        // setDebugFlags(DEBUG_CHECK_GL_ERROR);
     }
 
     @Override
@@ -155,10 +163,6 @@
         }
     }
 
-    public GLView getContentPane() {
-        return mContentView;
-    }
-
     @Override
     public void requestRender() {
         if (DEBUG_INVALIDATE) {
@@ -190,11 +194,45 @@
 
     private void layoutContentPane() {
         mFlags &= ~FLAG_NEED_LAYOUT;
-        int width = getWidth();
-        int height = getHeight();
-        Log.i(TAG, "layout content pane " + width + "x" + height);
-        if (mContentView != null && width != 0 && height != 0) {
-            mContentView.layout(0, 0, width, height);
+
+        int w = getWidth();
+        int h = getHeight();
+        int displayRotation = 0;
+        int compensation = 0;
+
+        // Get the new orientation values
+        if (mOrientationSource != null) {
+            displayRotation = mOrientationSource.getDisplayRotation();
+            compensation = mOrientationSource.getCompensation();
+        } else {
+            displayRotation = 0;
+            compensation = 0;
+        }
+
+        if (mCompensation != compensation) {
+            mCompensation = compensation;
+            if (mCompensation % 180 != 0) {
+                mCompensationMatrix.setRotate(mCompensation);
+                // move center to origin before rotation
+                mCompensationMatrix.preTranslate(-w / 2, -h / 2);
+                // align with the new origin after rotation
+                mCompensationMatrix.postTranslate(h / 2, w / 2);
+            } else {
+                mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2);
+            }
+        }
+        mDisplayRotation = displayRotation;
+
+        // Do the actual layout.
+        if (mCompensation % 180 != 0) {
+            int tmp = w;
+            w = h;
+            h = tmp;
+        }
+        Log.i(TAG, "layout content pane " + w + "x" + h
+                + " (compensation " + mCompensation + ")");
+        if (mContentView != null && w != 0 && h != 0) {
+            mContentView.layout(0, 0, w, h);
         }
         // Uncomment this to dump the view hierarchy.
         //mContentView.dumpTree("");
@@ -217,12 +255,19 @@
             // The GL Object has changed
             Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
         }
-        mGL = gl;
-        mCanvas = new GLCanvasImpl(gl);
-        if (!DEBUG_FPS) {
-            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
-        } else {
+        mRenderLock.lock();
+        try {
+            mGL = gl;
+            mCanvas = new GLCanvasImpl(gl);
+            BasicTexture.invalidateAllTextures();
+        } finally {
+            mRenderLock.unlock();
+        }
+
+        if (DEBUG_FPS || DEBUG_PROFILE) {
             setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+        } else {
+            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
         }
     }
 
@@ -237,13 +282,14 @@
                 + ", gl10: " + gl1.toString());
         Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
         GalleryUtils.setRenderThread();
+        if (DEBUG_PROFILE) {
+            Log.d(TAG, "Start profiling");
+            Profile.enable(20);  // take a sample every 20ms
+        }
         GL11 gl = (GL11) gl1;
         Utils.assertTrue(mGL == gl);
 
         mCanvas.setSize(width, height);
-
-        mClipRect.set(0, 0, width, height);
-        mClipRetryCount = 2;
     }
 
     private void outputFps() {
@@ -261,21 +307,52 @@
 
     @Override
     public void onDrawFrame(GL10 gl) {
+        AnimationTime.update();
+        long t0;
+        if (DEBUG_PROFILE_SLOW_ONLY) {
+            Profile.hold();
+            t0 = System.nanoTime();
+        }
         mRenderLock.lock();
+
+        while (mFreeze) {
+            mFreezeCondition.awaitUninterruptibly();
+        }
+
         try {
             onDrawFrameLocked(gl);
         } finally {
             mRenderLock.unlock();
         }
-        long end = SystemClock.uptimeMillis();
 
-        if (mLastDrawFinishTime != 0) {
-            long wait = mLastDrawFinishTime + TARGET_FRAME_TIME - end;
-            if (wait > 0) {
-                SystemClock.sleep(wait);
+        // We put a black cover View in front of the SurfaceView and hide it
+        // after the first draw. This prevents the SurfaceView being transparent
+        // before the first draw.
+        if (mFirstDraw) {
+            mFirstDraw = false;
+            post(new Runnable() {
+                    public void run() {
+                        View root = getRootView();
+                        View cover = root.findViewById(R.id.gl_root_cover);
+                        cover.setVisibility(GONE);
+                    }
+                });
+        }
+
+        if (DEBUG_PROFILE_SLOW_ONLY) {
+            long t = System.nanoTime();
+            long durationInMs = (t - mLastDrawFinishTime) / 1000000;
+            long durationDrawInMs = (t - t0) / 1000000;
+            mLastDrawFinishTime = t;
+
+            if (durationInMs > 34) {  // 34ms -> we skipped at least 2 frames
+                Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" +
+                        durationInMs + ") -----");
+                Profile.commit();
+            } else {
+                Profile.drop();
             }
         }
-        mLastDrawFinishTime = SystemClock.uptimeMillis();
     }
 
     private void onDrawFrameLocked(GL10 gl) {
@@ -291,22 +368,15 @@
 
         if ((mFlags & FLAG_NEED_LAYOUT) != 0) layoutContentPane();
 
-        // OpenGL seems having a bug causing us not being able to reset the
-        // scissor box in "onSurfaceChanged()". We have to do it in the second
-        // onDrawFrame().
-        if (mClipRetryCount > 0) {
-            --mClipRetryCount;
-            Rect clip = mClipRect;
-            gl.glScissor(clip.left, clip.top, clip.width(), clip.height());
-        }
-
-        mCanvas.setCurrentAnimationTimeMillis(SystemClock.uptimeMillis());
+        mCanvas.save(GLCanvas.SAVE_FLAG_ALL);
+        rotateCanvas(-mCompensation);
         if (mContentView != null) {
            mContentView.render(mCanvas);
         }
+        mCanvas.restore();
 
         if (!mAnimations.isEmpty()) {
-            long now = SystemClock.uptimeMillis();
+            long now = AnimationTime.get();
             for (int i = 0, n = mAnimations.size(); i < n; i++) {
                 mAnimations.get(i).setStartTime(now);
             }
@@ -318,9 +388,7 @@
         }
 
         synchronized (mIdleListeners) {
-            if (!mRenderRequested && !mIdleListeners.isEmpty()) {
-                mIdleRunner.enable();
-            }
+            if (!mIdleListeners.isEmpty()) mIdleRunner.enable();
         }
 
         if (DEBUG_INVALIDATE) {
@@ -333,8 +401,25 @@
         }
     }
 
+    private void rotateCanvas(int degrees) {
+        if (degrees == 0) return;
+        int w = getWidth();
+        int h = getHeight();
+        int cx = w / 2;
+        int cy = h / 2;
+        mCanvas.translate(cx, cy);
+        mCanvas.rotate(degrees, 0, 0, 1);
+        if (degrees % 180 != 0) {
+            mCanvas.translate(-cy, -cx);
+        } else {
+            mCanvas.translate(-cx, -cy);
+        }
+    }
+
     @Override
     public boolean dispatchTouchEvent(MotionEvent event) {
+        if (!isEnabled()) return false;
+
         int action = event.getAction();
         if (action == MotionEvent.ACTION_CANCEL
                 || action == MotionEvent.ACTION_UP) {
@@ -342,6 +427,11 @@
         } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
             return false;
         }
+
+        if (mCompensation != 0) {
+            event.transform(mCompensationMatrix);
+        }
+
         mRenderLock.lock();
         try {
             // If this has been detached from root, we don't need to handle event
@@ -356,19 +446,6 @@
         }
     }
 
-    public DisplayMetrics getDisplayMetrics() {
-        if (mDisplayMetrics == null) {
-            mDisplayMetrics = new DisplayMetrics();
-            ((Activity) getContext()).getWindowManager()
-                    .getDefaultDisplay().getMetrics(mDisplayMetrics);
-        }
-        return mDisplayMetrics;
-    }
-
-    public GLCanvas getCanvas() {
-        return mCanvas;
-    }
-
     private class IdleRunner implements Runnable {
         // true if the idle runner is in the queue
         private boolean mActive = false;
@@ -378,19 +455,18 @@
             OnGLIdleListener listener;
             synchronized (mIdleListeners) {
                 mActive = false;
-                if (mRenderRequested) return;
                 if (mIdleListeners.isEmpty()) return;
                 listener = mIdleListeners.removeFirst();
             }
             mRenderLock.lock();
             try {
-                if (!listener.onGLIdle(GLRootView.this, mCanvas)) return;
+                if (!listener.onGLIdle(mCanvas, mRenderRequested)) return;
             } finally {
                 mRenderLock.unlock();
             }
             synchronized (mIdleListeners) {
                 mIdleListeners.addLast(listener);
-                enable();
+                if (!mRenderRequested) enable();
             }
         }
 
@@ -411,4 +487,98 @@
     public void unlockRenderThread() {
         mRenderLock.unlock();
     }
+
+    @Override
+    public void onPause() {
+        unfreeze();
+        super.onPause();
+        if (DEBUG_PROFILE) {
+            Log.d(TAG, "Stop profiling");
+            Profile.disableAll();
+            Profile.dumpToFile("/sdcard/gallery.prof");
+            Profile.reset();
+        }
+    }
+
+    @Override
+    public void setOrientationSource(OrientationSource source) {
+        mOrientationSource = source;
+    }
+
+    @Override
+    public int getDisplayRotation() {
+        return mDisplayRotation;
+    }
+
+    @Override
+    public int getCompensation() {
+        return mCompensation;
+    }
+
+    @Override
+    public Matrix getCompensationMatrix() {
+        return mCompensationMatrix;
+    }
+
+    @Override
+    public void freeze() {
+        mRenderLock.lock();
+        mFreeze = true;
+        mRenderLock.unlock();
+    }
+
+    @Override
+    public void unfreeze() {
+        mRenderLock.lock();
+        mFreeze = false;
+        mFreezeCondition.signalAll();
+        mRenderLock.unlock();
+    }
+
+    @Override
+    public void setLightsOutMode(boolean enabled) {
+        int flags = enabled
+                ? SYSTEM_UI_FLAG_LOW_PROFILE
+                | SYSTEM_UI_FLAG_FULLSCREEN
+                | SYSTEM_UI_FLAG_LAYOUT_STABLE
+                : 0;
+        setSystemUiVisibility(flags);
+    }
+
+    // We need to unfreeze in the following methods and in onPause().
+    // These methods will wait on GLThread. If we have freezed the GLRootView,
+    // the GLThread will wait on main thread to call unfreeze and cause dead
+    // lock.
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+        unfreeze();
+        super.surfaceChanged(holder, format, w, h);
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        unfreeze();
+        super.surfaceCreated(holder);
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        unfreeze();
+        super.surfaceDestroyed(holder);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        unfreeze();
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            unfreeze();
+        } finally {
+            super.finalize();
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
index 7491a6f..bb71312 100644
--- a/src/com/android/gallery3d/ui/GLView.java
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -16,13 +16,13 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.anim.CanvasAnimation;
-import com.android.gallery3d.common.Utils;
-
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.view.MotionEvent;
 
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.common.Utils;
+
 import java.util.ArrayList;
 
 // GLView is a UI component. It can render to a GLCanvas and accept touch
@@ -229,12 +229,12 @@
         int xoffset = component.mBounds.left - mScrollX;
         int yoffset = component.mBounds.top - mScrollY;
 
-        canvas.translate(xoffset, yoffset, 0);
+        canvas.translate(xoffset, yoffset);
 
         CanvasAnimation anim = component.mAnimation;
         if (anim != null) {
             canvas.save(anim.getCanvasSaveFlags());
-            if (anim.calculate(canvas.currentAnimationTimeMillis())) {
+            if (anim.calculate(AnimationTime.get())) {
                 invalidate();
             } else {
                 component.mAnimation = null;
@@ -243,7 +243,7 @@
         }
         component.render(canvas);
         if (anim != null) canvas.restore();
-        canvas.translate(-xoffset, -yoffset, 0);
+        canvas.translate(-xoffset, -yoffset);
     }
 
     protected boolean onTouch(MotionEvent event) {
@@ -303,23 +303,14 @@
         return mPaddings;
     }
 
-    public void setPaddings(Rect paddings) {
-        mPaddings.set(paddings);
-    }
-
-    public void setPaddings(int left, int top, int right, int bottom) {
-        mPaddings.set(left, top, right, bottom);
-    }
-
     public void layout(int left, int top, int right, int bottom) {
         boolean sizeChanged = setBounds(left, top, right, bottom);
-        if (sizeChanged) {
-            mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
-            onLayout(true, left, top, right, bottom);
-        } else if ((mViewFlags & FLAG_LAYOUT_REQUESTED)!= 0) {
-            mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
-            onLayout(false, left, top, right, bottom);
-        }
+        mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+        // We call onLayout no matter sizeChanged is true or not because the
+        // orientation may change without changing the size of the View (for
+        // example, rotate the device by 180 degrees), and we want to handle
+        // orientation change in onLayout.
+        onLayout(sizeChanged, left, top, right, bottom);
     }
 
     private boolean setBounds(int left, int top, int right, int bottom) {
diff --git a/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java
index 1d50d43..0d5643f 100644
--- a/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java
+++ b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java
@@ -30,7 +30,6 @@
 class GalleryEGLConfigChooser implements EGLConfigChooser {
 
     private static final String TAG = "GalleryEGLConfigChooser";
-    private int mStencilBits;
 
     private final int mConfigSpec[] = new int[] {
             EGL10.EGL_RED_SIZE, 5,
@@ -40,10 +39,6 @@
             EGL10.EGL_NONE
     };
 
-    public int getStencilBits() {
-        return mStencilBits;
-    }
-
     public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
         int[] numConfig = new int[1];
         if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, numConfig)) {
@@ -94,7 +89,6 @@
         if (result == null) result = configs[0];
         egl.eglGetConfigAttrib(
                 display, result, EGL10.EGL_STENCIL_SIZE, value);
-        mStencilBits = value[0];
         logConfig(egl, display, result);
         return result;
     }
diff --git a/src/com/android/gallery3d/ui/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java
new file mode 100644
index 0000000..4a17d43
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GestureRecognizer.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector, and DownUpDetector.
+public class GestureRecognizer {
+    private static final String TAG = "GestureRecognizer";
+
+    public interface Listener {
+        boolean onSingleTapUp(float x, float y);
+        boolean onDoubleTap(float x, float y);
+        boolean onScroll(float dx, float dy);
+        boolean onFling(float velocityX, float velocityY);
+        boolean onScaleBegin(float focusX, float focusY);
+        boolean onScale(float focusX, float focusY, float scale);
+        void onScaleEnd();
+        void onDown();
+        void onUp();
+    }
+
+    private final GestureDetector mGestureDetector;
+    private final ScaleGestureDetector mScaleDetector;
+    private final DownUpDetector mDownUpDetector;
+    private final Listener mListener;
+
+    public GestureRecognizer(Context context, Listener listener) {
+        mListener = listener;
+        mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+                null, true /* ignoreMultitouch */);
+        mScaleDetector = new ScaleGestureDetector(
+                context, new MyScaleListener());
+        mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+    }
+
+    public void onTouchEvent(MotionEvent event) {
+        mGestureDetector.onTouchEvent(event);
+        mScaleDetector.onTouchEvent(event);
+        mDownUpDetector.onTouchEvent(event);
+    }
+
+    public boolean isDown() {
+        return mDownUpDetector.isDown();
+    }
+
+    public void cancelScale() {
+        long now = SystemClock.uptimeMillis();
+        MotionEvent cancelEvent = MotionEvent.obtain(
+                now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+        mScaleDetector.onTouchEvent(cancelEvent);
+        cancelEvent.recycle();
+    }
+
+    private class MyGestureListener
+                extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            return mListener.onSingleTapUp(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            return mListener.onDoubleTap(e.getX(), e.getY());
+        }
+
+        @Override
+        public boolean onScroll(
+                MotionEvent e1, MotionEvent e2, float dx, float dy) {
+            return mListener.onScroll(dx, dy);
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                float velocityY) {
+            return mListener.onFling(velocityX, velocityY);
+        }
+    }
+
+    private class MyScaleListener
+            extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            return mListener.onScaleBegin(
+                    detector.getFocusX(), detector.getFocusY());
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            return mListener.onScale(detector.getFocusX(),
+                    detector.getFocusY(), detector.getScaleFactor());
+        }
+
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            mListener.onScaleEnd();
+        }
+    }
+
+    private class MyDownUpListener implements DownUpDetector.DownUpListener {
+        @Override
+        public void onDown(MotionEvent e) {
+            mListener.onDown();
+        }
+
+        @Override
+        public void onUp(MotionEvent e) {
+            mListener.onUp();
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/GridDrawer.java b/src/com/android/gallery3d/ui/GridDrawer.java
deleted file mode 100644
index e8e072d..0000000
--- a/src/com/android/gallery3d/ui/GridDrawer.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.R;
-import com.android.gallery3d.data.Path;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.text.Layout;
-
-public class GridDrawer extends IconDrawer {
-    private Texture mImportLabel;
-    private int mGridWidth;
-    private final SelectionManager mSelectionManager;
-    private final Context mContext;
-    private final int IMPORT_FONT_SIZE = 14;
-    private final int IMPORT_FONT_COLOR = Color.WHITE;
-    private final int IMPORT_LABEL_MARGIN = 10;
-    private boolean mSelectionMode;
-
-    public GridDrawer(Context context, SelectionManager selectionManager) {
-        super(context);
-        mContext = context;
-        mSelectionManager = selectionManager;
-    }
-
-    @Override
-    public void prepareDrawing() {
-        mSelectionMode = mSelectionManager.inSelectionMode();
-    }
-
-    @Override
-    public void draw(GLCanvas canvas, Texture content, int width,
-            int height, int rotation, Path path,
-            int dataSourceType, int mediaType, boolean isPanorama,
-            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
-
-        int x = -width / 2;
-        int y = -height / 2;
-
-        drawWithRotation(canvas, content, x, y, width, height, rotation);
-
-        if (((rotation / 90) & 0x01) == 1) {
-            int temp = width;
-            width = height;
-            height = temp;
-            x = -width / 2;
-            y = -height / 2;
-        }
-
-        drawMediaTypeOverlay(canvas, mediaType, isPanorama, x, y, width, height);
-        drawLabelBackground(canvas, width, height, labelBackgroundHeight);
-        drawIcon(canvas, width, height, dataSourceType);
-        if (dataSourceType == DATASOURCE_TYPE_MTP) {
-            drawImportLabel(canvas, width, height);
-        }
-
-        if (mSelectionManager.isPressedPath(path)) {
-            drawPressedFrame(canvas, x, y, width, height);
-        } else if (mSelectionMode && mSelectionManager.isItemSelected(path)) {
-            drawSelectedFrame(canvas, x, y, width, height);
-        }
-    }
-
-    // Draws the "click to import" label at the center of the frame
-    private void drawImportLabel(GLCanvas canvas, int width, int height) {
-        if (mImportLabel == null || mGridWidth != width) {
-            mGridWidth = width;
-            mImportLabel = MultiLineTexture.newInstance(
-                    mContext.getString(R.string.click_import),
-                    width - 2 * IMPORT_LABEL_MARGIN,
-                    IMPORT_FONT_SIZE, IMPORT_FONT_COLOR,
-                    Layout.Alignment.ALIGN_CENTER);
-        }
-        int w = mImportLabel.getWidth();
-        int h = mImportLabel.getHeight();
-        mImportLabel.draw(canvas, -w / 2, -h / 2);
-    }
-
-    @Override
-    public void drawFocus(GLCanvas canvas, int width, int height) {
-    }
-}
diff --git a/src/com/android/gallery3d/ui/HighlightDrawer.java b/src/com/android/gallery3d/ui/HighlightDrawer.java
deleted file mode 100644
index f6a4695..0000000
--- a/src/com/android/gallery3d/ui/HighlightDrawer.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.R;
-import com.android.gallery3d.data.Path;
-
-import android.content.Context;
-
-public class HighlightDrawer extends IconDrawer {
-    private SelectionManager mSelectionManager;
-    private Path mHighlightItem;
-
-    public HighlightDrawer(Context context, SelectionManager selectionManager) {
-        super(context);
-        mSelectionManager = selectionManager;
-    }
-
-    public void setHighlightItem(Path item) {
-        mHighlightItem = item;
-    }
-
-    @Override
-    public void draw(GLCanvas canvas, Texture content, int width,
-            int height, int rotation, Path path,
-            int dataSourceType, int mediaType, boolean isPanorama,
-            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
-        int x = -width / 2;
-        int y = -height / 2;
-
-        drawWithRotation(canvas, content, x, y, width, height, rotation);
-
-        if (((rotation / 90) & 0x01) == 1) {
-            int temp = width;
-            width = height;
-            height = temp;
-            x = -width / 2;
-            y = -height / 2;
-        }
-
-        drawMediaTypeOverlay(canvas, mediaType, isPanorama, x, y, width, height);
-        drawLabelBackground(canvas, width, height, labelBackgroundHeight);
-        drawIcon(canvas, width, height, dataSourceType);
-
-        if (mSelectionManager.isPressedPath(path)) {
-            drawPressedFrame(canvas, x, y, width, height);
-        } else if (path == mHighlightItem) {
-            drawSelectedFrame(canvas, x, y, width, height);
-        }
-    }
-}
diff --git a/src/com/android/gallery3d/ui/Icon.java b/src/com/android/gallery3d/ui/Icon.java
deleted file mode 100644
index c710859..0000000
--- a/src/com/android/gallery3d/ui/Icon.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.content.Context;
-import android.graphics.Rect;
-
-public class Icon extends GLView {
-    private final BasicTexture mIcon;
-
-    // The width and height requested by the user.
-    private int mReqWidth;
-    private int mReqHeight;
-
-    public Icon(Context context, int iconId, int width, int height) {
-        this(context, new ResourceTexture(context, iconId), width, height);
-    }
-
-    public Icon(Context context, BasicTexture icon, int width, int height) {
-        mIcon = icon;
-        mReqWidth = width;
-        mReqHeight = height;
-    }
-
-    @Override
-    protected void onMeasure(int widthSpec, int heightSpec) {
-        MeasureHelper.getInstance(this)
-                .setPreferredContentSize(mReqWidth, mReqHeight)
-                .measure(widthSpec, heightSpec);
-    }
-
-    @Override
-    protected void render(GLCanvas canvas) {
-        Rect p = mPaddings;
-
-        int width = getWidth() - p.left - p.right;
-        int height = getHeight() - p.top - p.bottom;
-
-        // Draw the icon in the center of the space
-        int xoffset = p.left + (width - mReqWidth) / 2;
-        int yoffset = p.top + (height - mReqHeight) / 2;
-
-        mIcon.draw(canvas, xoffset, yoffset, mReqWidth, mReqHeight);
-    }
-}
diff --git a/src/com/android/gallery3d/ui/IconDrawer.java b/src/com/android/gallery3d/ui/IconDrawer.java
deleted file mode 100644
index 25440bc..0000000
--- a/src/com/android/gallery3d/ui/IconDrawer.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.R;
-import com.android.gallery3d.data.MediaObject;
-
-import android.content.Context;
-
-public abstract class IconDrawer extends SelectionDrawer {
-    private static final String TAG = "IconDrawer";
-    private static final int LABEL_BACKGROUND_COLOR = 0x99000000;  // 60% black
-
-    private final ResourceTexture mLocalSetIcon;
-    private final ResourceTexture mCameraIcon;
-    private final ResourceTexture mPicasaIcon;
-    private final ResourceTexture mMtpIcon;
-    private final NinePatchTexture mFramePressed;
-    private final NinePatchTexture mFrameSelected;
-    private final NinePatchTexture mDarkStrip;
-    private final NinePatchTexture mPanoramaBorder;
-    private final Texture mVideoOverlay;
-    private final Texture mVideoPlayIcon;
-    private final int mIconSize;
-
-    public static class IconDimension {
-        int x;
-        int y;
-        int width;
-        int height;
-    }
-
-    public IconDrawer(Context context) {
-        mLocalSetIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_folder);
-        mCameraIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_camera);
-        mPicasaIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_picasa);
-        mMtpIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_ptp);
-        mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb);
-        mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play);
-        mPanoramaBorder = new NinePatchTexture(context, R.drawable.ic_pan_thumb);
-        mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
-        mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
-        mDarkStrip = new NinePatchTexture(context, R.drawable.dark_strip);
-        mIconSize = context.getResources().getDimensionPixelSize(
-                R.dimen.albumset_icon_size);
-    }
-
-    @Override
-    public void prepareDrawing() {
-    }
-
-    protected IconDimension drawIcon(GLCanvas canvas, int width, int height,
-            int dataSourceType) {
-        ResourceTexture icon = getIcon(dataSourceType);
-
-        if (icon != null) {
-            IconDimension id = getIconDimension(icon, width, height);
-            icon.draw(canvas, id.x, id.y, id.width, id.height);
-            return id;
-        }
-        return null;
-    }
-
-    protected ResourceTexture getIcon(int dataSourceType) {
-        ResourceTexture icon = null;
-        switch (dataSourceType) {
-            case DATASOURCE_TYPE_LOCAL:
-                icon = mLocalSetIcon;
-                break;
-            case DATASOURCE_TYPE_PICASA:
-                icon = mPicasaIcon;
-                break;
-            case DATASOURCE_TYPE_CAMERA:
-                icon = mCameraIcon;
-                break;
-            case DATASOURCE_TYPE_MTP:
-                icon = mMtpIcon;
-                break;
-            default:
-                break;
-        }
-
-        return icon;
-    }
-
-    protected IconDimension getIconDimension(ResourceTexture icon, int width,
-            int height) {
-        IconDimension id = new IconDimension();
-        float scale = (float) mIconSize / icon.getWidth();
-        id.width = Math.round(scale * icon.getWidth());
-        id.height = Math.round(scale * icon.getHeight());
-        id.x = -width / 2;
-        id.y = (height + 1) / 2 - id.height;
-        return id;
-    }
-
-    protected void drawMediaTypeOverlay(GLCanvas canvas, int mediaType,
-            boolean isPanorama, int x, int y, int width, int height) {
-        if (mediaType == MediaObject.MEDIA_TYPE_VIDEO) {
-            drawVideoOverlay(canvas, x, y, width, height);
-        }
-        if (isPanorama) {
-            drawPanoramaBorder(canvas, x, y, width, height);
-        }
-    }
-
-    protected void drawVideoOverlay(GLCanvas canvas, int x, int y,
-            int width, int height) {
-        // Scale the video overlay to the height of the thumbnail and put it
-        // on the left side.
-        float scale = (float) height / mVideoOverlay.getHeight();
-        int w = Math.round(scale * mVideoOverlay.getWidth());
-        int h = Math.round(scale * mVideoOverlay.getHeight());
-        mVideoOverlay.draw(canvas, x, y, w, h);
-
-        int side = Math.min(width, height) / 6;
-        mVideoPlayIcon.draw(canvas, -side / 2, -side / 2, side, side);
-    }
-
-    protected void drawPanoramaBorder(GLCanvas canvas, int x, int y,
-            int width, int height) {
-        float scale = (float) width / mPanoramaBorder.getWidth();
-        int w = Math.round(scale * mPanoramaBorder.getWidth());
-        int h = Math.round(scale * mPanoramaBorder.getHeight());
-        // draw at the top
-        mPanoramaBorder.draw(canvas, x, y, w, h);
-        // draw at the bottom
-        mPanoramaBorder.draw(canvas, x, y + width - h, w, h);
-    }
-
-    protected void drawLabelBackground(GLCanvas canvas, int width, int height,
-            int drawLabelBackground) {
-        int x = -width / 2;
-        int y = (height + 1) / 2 - drawLabelBackground;
-        drawFrame(canvas, mDarkStrip, x, y, width, drawLabelBackground);
-    }
-
-    protected void drawPressedFrame(GLCanvas canvas, int x, int y, int width,
-            int height) {
-        drawFrame(canvas, mFramePressed, x, y, width, height);
-    }
-
-    protected void drawSelectedFrame(GLCanvas canvas, int x, int y, int width,
-            int height) {
-        drawFrame(canvas, mFrameSelected, x, y, width, height);
-    }
-
-    @Override
-    public void drawFocus(GLCanvas canvas, int width, int height) {
-    }
-}
diff --git a/src/com/android/gallery3d/ui/ImportCompleteListener.java b/src/com/android/gallery3d/ui/ImportCompleteListener.java
index 5c52ea1..63d6167 100644
--- a/src/com/android/gallery3d/ui/ImportCompleteListener.java
+++ b/src/com/android/gallery3d/ui/ImportCompleteListener.java
@@ -16,16 +16,14 @@
 
 package com.android.gallery3d.ui;
 
+import android.os.Bundle;
+import android.widget.Toast;
+
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.AlbumPage;
 import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.data.Path;
 import com.android.gallery3d.util.MediaSetUtils;
 
-import android.content.Context;
-import android.os.Bundle;
-import android.widget.Toast;
-
 public class ImportCompleteListener implements MenuExecutor.ProgressListener {
     private GalleryActivity mActivity;
 
@@ -33,6 +31,7 @@
         mActivity = galleryActivity;
     }
 
+    @Override
     public void onProgressComplete(int result) {
         int message;
         if (result == MenuExecutor.EXECUTION_RESULT_SUCCESS) {
@@ -44,6 +43,7 @@
         Toast.makeText(mActivity.getAndroidContext(), message, Toast.LENGTH_LONG).show();
     }
 
+    @Override
     public void onProgressUpdate(int index) {
     }
 
@@ -54,4 +54,11 @@
         mActivity.getStateManager().startState(AlbumPage.class, data);
     }
 
+    @Override
+    public void onConfirmDialogDismissed(boolean confirmed) {
+    }
+
+    @Override
+    public void onConfirmDialogShown() {
+    }
 }
diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
index e75fe9a..ba31fc6 100644
--- a/src/com/android/gallery3d/ui/ManageCacheDrawer.java
+++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
@@ -16,13 +16,16 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.Path;
-
 import android.content.Context;
 
-public class ManageCacheDrawer extends IconDrawer {
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class ManageCacheDrawer extends AlbumSetSlotRenderer {
     private final ResourceTexture mCheckedItem;
     private final ResourceTexture mUnCheckedItem;
     private final SelectionManager mSelectionManager;
@@ -33,12 +36,16 @@
     private final int mCachePinSize;
     private final int mCachePinMargin;
 
-    public ManageCacheDrawer(Context context, SelectionManager selectionManager,
-            int cachePinSize, int cachePinMargin) {
-        super(context);
-        mCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_on_holo_dark);
-        mUnCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_off_holo_dark);
-        mLocalAlbumIcon = new ResourceTexture(context, R.drawable.btn_make_offline_disabled_on_holo_dark);
+    public ManageCacheDrawer(GalleryActivity activity, SelectionManager selectionManager,
+            SlotView slotView, LabelSpec labelSpec, int cachePinSize, int cachePinMargin) {
+        super(activity, selectionManager, slotView, labelSpec);
+        Context context = (Context) activity;
+        mCheckedItem = new ResourceTexture(
+                context, R.drawable.btn_make_offline_normal_on_holo_dark);
+        mUnCheckedItem = new ResourceTexture(
+                context, R.drawable.btn_make_offline_normal_off_holo_dark);
+        mLocalAlbumIcon = new ResourceTexture(
+                context, R.drawable.btn_make_offline_disabled_on_holo_dark);
         String cachingLabel = context.getString(R.string.caching_label);
         mCachingText = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
         mSelectionManager = selectionManager;
@@ -46,61 +53,42 @@
         mCachePinMargin = cachePinMargin;
     }
 
-    @Override
-    public void prepareDrawing() {
-    }
-
     private static boolean isLocal(int dataSourceType) {
-        return dataSourceType != DATASOURCE_TYPE_PICASA;
+        return dataSourceType != DataSourceType.TYPE_PICASA;
     }
 
     @Override
-    public void draw(GLCanvas canvas, Texture content, int width,
-            int height, int rotation, Path path,
-            int dataSourceType, int mediaType, boolean isPanorama,
-            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
+    public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        AlbumSetEntry entry = mDataWindow.get(index);
 
-        boolean selected = mSelectionManager.isItemSelected(path);
+        boolean wantCache = entry.cacheFlag == MediaSet.CACHE_FLAG_FULL;
+        boolean isCaching = wantCache && (
+                entry.cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL);
+        boolean selected = mSelectionManager.isItemSelected(entry.setPath);
         boolean chooseToCache = wantCache ^ selected;
-        boolean available = isLocal(dataSourceType) || chooseToCache;
+        boolean available = isLocal(entry.sourceType) || chooseToCache;
 
-        int x = -width / 2;
-        int y = -height / 2;
+        int renderRequestFlags = 0;
 
         if (!available) {
             canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
             canvas.multiplyAlpha(0.6f);
         }
+        renderRequestFlags |= renderContent(canvas, entry, width, height);
+        if (!available) canvas.restore();
 
-        drawWithRotation(canvas, content, x, y, width, height, rotation);
+        renderRequestFlags |= renderLabel(canvas, entry, width, height);
 
-        if (!available) {
-            canvas.restore();
-        }
+        drawCachingPin(canvas, entry.setPath,
+                entry.sourceType, isCaching, chooseToCache, width, height);
 
-        if (((rotation / 90) & 0x01) == 1) {
-            int temp = width;
-            width = height;
-            height = temp;
-            x = -width / 2;
-            y = -height / 2;
-        }
-
-        drawMediaTypeOverlay(canvas, mediaType, isPanorama, x, y, width, height);
-        drawLabelBackground(canvas, width, height, labelBackgroundHeight);
-        drawIcon(canvas, width, height, dataSourceType);
-        drawCachingPin(canvas, path, dataSourceType, isCaching, chooseToCache,
-                width, height);
-
-        if (mSelectionManager.isPressedPath(path)) {
-            drawPressedFrame(canvas, x, y, width, height);
-        }
+        renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+        return renderRequestFlags;
     }
 
     private void drawCachingPin(GLCanvas canvas, Path path, int dataSourceType,
             boolean isCaching, boolean chooseToCache, int width, int height) {
-
-        ResourceTexture icon = null;
+        ResourceTexture icon;
         if (isLocal(dataSourceType)) {
             icon = mLocalAlbumIcon;
         } else if (chooseToCache) {
@@ -109,26 +97,16 @@
             icon = mUnCheckedItem;
         }
 
-        int w = mCachePinSize;
-        int h = mCachePinSize;
-        int right = (width + 1) / 2;
-        int bottom = (height + 1) / 2;
-        int x = right - w - mCachePinMargin;
-        int y = bottom - h - mCachePinMargin;
-
-        icon.draw(canvas, x, y, w, h);
+        // show the icon in right bottom
+        int s = mCachePinSize;
+        int m = mCachePinMargin;
+        icon.draw(canvas, width - m - s, height - s, s, s);
 
         if (isCaching) {
-            int textWidth = mCachingText.getWidth();
-            int textHeight = mCachingText.getHeight();
-            // Align the center of the text to the center of the pin icon
-            x = right - mCachePinMargin - (textWidth + mCachePinSize) / 2;
-            y = bottom - textHeight;
-            mCachingText.draw(canvas, x, y);
+            int w = mCachingText.getWidth();
+            int h = mCachingText.getHeight();
+            // Show the caching text in bottom center
+            mCachingText.draw(canvas, (width - w) / 2, height - h);
         }
     }
-
-    @Override
-    public void drawFocus(GLCanvas canvas, int width, int height) {
-    }
 }
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
index 918feea..7de07e8 100644
--- a/src/com/android/gallery3d/ui/MenuExecutor.java
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -17,14 +17,17 @@
 package com.android.gallery3d.ui;
 
 import android.app.Activity;
+import android.app.AlertDialog;
 import android.app.ProgressDialog;
 import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.Message;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.widget.Toast;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.CropImage;
@@ -75,6 +78,8 @@
     }
 
     public interface ProgressListener {
+        public void onConfirmDialogShown();
+        public void onConfirmDialogDismissed(boolean confirmed);
         public void onProgressUpdate(int index);
         public void onProgressComplete(int result);
     }
@@ -172,10 +177,15 @@
         return ids.get(0);
     }
 
-    public boolean onMenuClicked(MenuItem menuItem, ProgressListener listener) {
-        int title;
+    private Intent getIntentBySingleSelectedPath(String action) {
         DataManager manager = mActivity.getDataManager();
-        int action = menuItem.getItemId();
+        Path path = getSingleSelectedPath();
+        String mimeType = getMimeType(manager.getMediaType(path));
+        return new Intent(action).setDataAndType(manager.getContentUri(path), mimeType);
+    }
+
+    private void onMenuClicked(int action, ProgressListener listener) {
+        int title;
         switch (action) {
             case R.id.action_select_all:
                 if (mSelectionManager.inSelectAllMode()) {
@@ -183,29 +193,28 @@
                 } else {
                     mSelectionManager.selectAll();
                 }
-                return true;
+                return;
             case R.id.action_crop: {
-                Path path = getSingleSelectedPath();
-                String mimeType = getMimeType(manager.getMediaType(path));
-                Intent intent = new Intent(CropImage.ACTION_CROP)
-                        .setDataAndType(manager.getContentUri(path), mimeType);
+                Intent intent = getIntentBySingleSelectedPath(CropImage.ACTION_CROP);
                 ((Activity) mActivity).startActivity(intent);
-                return true;
+                return;
+            }
+            case R.id.action_edit: {
+                Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_EDIT)
+                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                ((Activity) mActivity).startActivity(Intent.createChooser(intent, null));
+                return;
             }
             case R.id.action_setas: {
-                Path path = getSingleSelectedPath();
-                int type = manager.getMediaType(path);
-                Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
-                String mimeType = getMimeType(type);
-                intent.setDataAndType(manager.getContentUri(path), mimeType);
-                intent.putExtra("mimeType", mimeType);
-                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_ATTACH_DATA)
+                        .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                intent.putExtra("mimeType", intent.getType());
                 Activity activity = (Activity) mActivity;
                 activity.startActivity(Intent.createChooser(
                         intent, activity.getString(R.string.set_as)));
-                return true;
+                return;
             }
-            case R.id.action_confirm_delete:
+            case R.id.action_delete:
                 title = R.string.delete;
                 break;
             case R.id.action_rotate_cw:
@@ -217,17 +226,62 @@
             case R.id.action_show_on_map:
                 title = R.string.show_on_map;
                 break;
-            case R.id.action_edit:
-                title = R.string.edit;
-                break;
             case R.id.action_import:
                 title = R.string.Import;
                 break;
             default:
-                return false;
+                return;
         }
         startAction(action, title, listener);
-        return true;
+    }
+
+    private class ConfirmDialogListener implements OnClickListener, OnCancelListener {
+        private final int mActionId;
+        private final ProgressListener mListener;
+
+        public ConfirmDialogListener(int actionId, ProgressListener listener) {
+            mActionId = actionId;
+            mListener = listener;
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == DialogInterface.BUTTON_POSITIVE) {
+                if (mListener != null) {
+                    mListener.onConfirmDialogDismissed(true);
+                }
+                onMenuClicked(mActionId, mListener);
+            } else {
+                if (mListener != null) {
+                    mListener.onConfirmDialogDismissed(false);
+                }
+            }
+        }
+
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            if (mListener != null) {
+                mListener.onConfirmDialogDismissed(false);
+            }
+        }
+    }
+
+    public void onMenuClicked(MenuItem menuItem, String confirmMsg,
+            final ProgressListener listener) {
+        final int action = menuItem.getItemId();
+
+        if (confirmMsg != null) {
+            if (listener != null) listener.onConfirmDialogShown();
+            ConfirmDialogListener cdl = new ConfirmDialogListener(action, listener);
+            new AlertDialog.Builder(mActivity.getAndroidContext())
+                    .setMessage(confirmMsg)
+                    .setOnCancelListener(cdl)
+                    .setPositiveButton(R.string.ok, cdl)
+                    .setNegativeButton(R.string.cancel, cdl)
+                    .create().show();
+        } else {
+            onMenuClicked(action, listener);
+        }
     }
 
     public void startAction(int action, int title, ProgressListener listener) {
@@ -257,7 +311,7 @@
         long startTime = System.currentTimeMillis();
 
         switch (cmd) {
-            case R.id.action_confirm_delete:
+            case R.id.action_delete:
                 manager.delete(path);
                 break;
             case R.id.action_rotate_cw:
@@ -291,23 +345,6 @@
                 result = obj.Import();
                 break;
             }
-            case R.id.action_edit: {
-                Activity activity = (Activity) mActivity;
-                MediaItem item = (MediaItem) manager.getMediaObject(path);
-                try {
-                    activity.startActivity(Intent.createChooser(
-                            new Intent(Intent.ACTION_EDIT)
-                                    .setDataAndType(item.getContentUri(), item.getMimeType())
-                                    .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION),
-                            null));
-                } catch (Throwable t) {
-                    Log.w(TAG, "failed to start edit activity: ", t);
-                    Toast.makeText(activity,
-                            activity.getString(R.string.activity_not_found),
-                            Toast.LENGTH_SHORT).show();
-                }
-                break;
-            }
             default:
                 throw new AssertionError();
         }
@@ -352,4 +389,3 @@
         }
     }
 }
-
diff --git a/src/com/android/gallery3d/ui/NinePatchTexture.java b/src/com/android/gallery3d/ui/NinePatchTexture.java
index 15b057a..fa0e9cd 100644
--- a/src/com/android/gallery3d/ui/NinePatchTexture.java
+++ b/src/com/android/gallery3d/ui/NinePatchTexture.java
@@ -16,18 +16,17 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Rect;
 
+import com.android.gallery3d.common.Utils;
+
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.FloatBuffer;
-import java.util.LinkedHashMap;
-import java.util.Map;
+
 import javax.microedition.khronos.opengles.GL11;
 
 // NinePatchTexture is a texture backed by a NinePatch resource.
@@ -39,8 +38,8 @@
     @SuppressWarnings("unused")
     private static final String TAG = "NinePatchTexture";
     private NinePatchChunk mChunk;
-    private MyCacheMap<Long, NinePatchInstance> mInstanceCache =
-            new MyCacheMap<Long, NinePatchInstance>();
+    private SmallCache<NinePatchInstance> mInstanceCache
+            = new SmallCache<NinePatchInstance>();
 
     public NinePatchTexture(Context context, int resId) {
         super(context, resId);
@@ -77,39 +76,77 @@
         return mChunk;
     }
 
-    private static class MyCacheMap<K, V> extends LinkedHashMap<K, V> {
-        private int CACHE_SIZE = 16;
-        private V mJustRemoved;
+    // This is a simple cache for a small number of things. Linear search
+    // is used because the cache is small. It also tries to remove less used
+    // item when the cache is full by moving the often-used items to the front.
+    private static class SmallCache<V> {
+        private static final int CACHE_SIZE = 16;
+        private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
+        private int[] mKey = new int[CACHE_SIZE];
+        private V[] mValue = (V[]) new Object[CACHE_SIZE];
+        private int mCount;  // number of items in this cache
 
-        public MyCacheMap() {
-            super(4, 0.75f, true);
-        }
-
-        @Override
-        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
-            if (size() > CACHE_SIZE) {
-                mJustRemoved = eldest.getValue();
-                return true;
+        // Puts a value into the cache. If the cache is full, also returns
+        // a less used item, otherwise returns null.
+        public V put(int key, V value) {
+            if (mCount == CACHE_SIZE) {
+                V old = mValue[CACHE_SIZE - 1];  // remove the last item
+                mKey[CACHE_SIZE - 1] = key;
+                mValue[CACHE_SIZE - 1] = value;
+                return old;
+            } else {
+                mKey[mCount] = key;
+                mValue[mCount] = value;
+                mCount++;
+                return null;
             }
-            return false;
         }
 
-        public V getJustRemoved() {
-            V result = mJustRemoved;
-            mJustRemoved = null;
-            return result;
+        public V get(int key) {
+            for (int i = 0; i < mCount; i++) {
+                if (mKey[i] == key) {
+                    // Move the accessed item one position to the front, so it
+                    // will less likely to be removed when cache is full. Only
+                    // do this if the cache is starting to get full.
+                    if (mCount > CACHE_SIZE_START_MOVE && i > 0) {
+                        int tmpKey = mKey[i];
+                        mKey[i] = mKey[i - 1];
+                        mKey[i - 1] = tmpKey;
+
+                        V tmpValue = mValue[i];
+                        mValue[i] = mValue[i - 1];
+                        mValue[i - 1] = tmpValue;
+                    }
+                    return mValue[i];
+                }
+            }
+            return null;
+        }
+
+        public void clear() {
+            for (int i = 0; i < mCount; i++) {
+                mValue[i] = null;  // make sure it's can be garbage-collected.
+            }
+            mCount = 0;
+        }
+
+        public int size() {
+            return mCount;
+        }
+
+        public V valueAt(int i) {
+            return mValue[i];
         }
     }
 
     private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
-        long key = w;
-        key = (key << 32) | h;
+        int key = w;
+        key = (key << 16) | h;
         NinePatchInstance instance = mInstanceCache.get(key);
 
         if (instance == null) {
             instance = new NinePatchInstance(this, w, h);
-            mInstanceCache.put(key, instance);
-            NinePatchInstance removed = mInstanceCache.getJustRemoved();
+            NinePatchInstance removed = mInstanceCache.put(key, instance);
             if (removed != null) {
                 removed.recycle(canvas);
             }
@@ -120,7 +157,7 @@
 
     @Override
     public void draw(GLCanvas canvas, int x, int y, int w, int h) {
-        if (!isLoaded(canvas)) {
+        if (!isLoaded()) {
             mInstanceCache.clear();
         }
 
@@ -132,9 +169,11 @@
     @Override
     public void recycle() {
         super.recycle();
-        GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get();
+        GLCanvas canvas = mCanvasRef;
         if (canvas == null) return;
-        for (NinePatchInstance instance : mInstanceCache.values()) {
+        int n = mInstanceCache.size();
+        for (int i = 0; i < n; i++) {
+            NinePatchInstance instance = mInstanceCache.valueAt(i);
             instance.recycle(canvas);
         }
         mInstanceCache.clear();
@@ -359,7 +398,7 @@
     private void prepareBuffers(GLCanvas canvas) {
         mBufferNames = new int[3];
         GL11 gl = canvas.getGLInstance();
-        gl.glGenBuffers(3, mBufferNames, 0);
+        GLId.glGenBuffers(3, mBufferNames, 0);
 
         gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[0]);
         gl.glBufferData(GL11.GL_ARRAY_BUFFER,
diff --git a/src/com/android/gallery3d/ui/OnSelectedListener.java b/src/com/android/gallery3d/ui/OrientationSource.java
similarity index 78%
rename from src/com/android/gallery3d/ui/OnSelectedListener.java
rename to src/com/android/gallery3d/ui/OrientationSource.java
index 2cc5809..e13ce1c 100644
--- a/src/com/android/gallery3d/ui/OnSelectedListener.java
+++ b/src/com/android/gallery3d/ui/OrientationSource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010 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.
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.ui;
 
-public interface OnSelectedListener {
-    public void onSelected(GLView source);
+public interface OrientationSource {
+    public int getDisplayRotation();
+    public int getCompensation();
 }
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
index ecc4150..3b67a04 100644
--- a/src/com/android/gallery3d/ui/Paper.java
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -16,17 +16,12 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.ui.PositionRepository.Position;
-import com.android.gallery3d.util.GalleryUtils;
-
+import android.graphics.Rect;
 import android.opengl.Matrix;
-import android.os.SystemClock;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
-import javax.microedition.khronos.opengles.GL11;
-import javax.microedition.khronos.opengles.GL11ExtensionPack;
+import com.android.gallery3d.common.Utils;
 
 // This class does the overscroll effect.
 class Paper {
@@ -70,11 +65,10 @@
         mHeight = height;
     }
 
-    public float[] getTransform(Position target, Position base,
-            float scrollX, float scrollY) {
+    public float[] getTransform(Rect rect, float scrollX) {
         float left = mAnimationLeft.getValue();
         float right = mAnimationRight.getValue();
-        float screenX = target.x - scrollX;
+        float screenX = rect.centerX() - scrollX;
         // We linearly interpolate the value [left, right] for the screenX
         // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside
         // the screen, we still get some transform.
@@ -87,10 +81,9 @@
         float degrees =
                 (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45;
         Matrix.setIdentityM(mMatrix, 0);
-        Matrix.translateM(mMatrix, 0, mMatrix, 0, base.x, base.y, base.z);
+        Matrix.translateM(mMatrix, 0, mMatrix, 0, rect.centerX(), rect.centerY(), 0);
         Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0);
-        Matrix.translateM(mMatrix, 0, mMatrix, 0,
-                target.x - base.x, target.y - base.y, target.z - base.z);
+        Matrix.translateM(mMatrix, 0, mMatrix, 0, -rect.width() / 2, -rect.height() / 2, 0);
         return mMatrix;
     }
 }
@@ -113,7 +106,6 @@
     private final Interpolator mInterpolator;
 
     private int mState;
-    private long mAnimationStartTime;
     private float mValue;
 
     private float mValueStart;
@@ -185,6 +177,6 @@
     }
 
     private long now() {
-        return SystemClock.uptimeMillis();
+        return AnimationTime.get();
     }
 }
diff --git a/src/com/android/gallery3d/ui/PhotoFallbackEffect.java b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
new file mode 100644
index 0000000..3ca09ab
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSlotRenderer.SlotFilter;
+
+import java.util.ArrayList;
+
+public class PhotoFallbackEffect extends Animation implements SlotFilter {
+
+    private static final int ANIM_DURATION = 300;
+    private static final Interpolator ANIM_INTERPOLATE = new DecelerateInterpolator(1.5f);
+
+    public static class Entry {
+        public int index;
+        public Path path;
+        public Rect source;
+        public Rect dest;
+        public RawTexture texture;
+
+        public Entry(Path path, Rect source, RawTexture texture) {
+            this.path = path;
+            this.source = source;
+            this.texture = texture;
+        }
+    }
+
+    public interface PositionProvider {
+        public Rect getPosition(int index);
+        public int getItemIndex(Path path);
+    }
+
+    private RectF mSource = new RectF();
+    private RectF mTarget = new RectF();
+    private float mProgress;
+    private PositionProvider mPositionProvider;
+
+    private ArrayList<Entry> mList = new ArrayList<Entry>();
+
+    public PhotoFallbackEffect() {
+        setDuration(ANIM_DURATION);
+        setInterpolator(ANIM_INTERPOLATE);
+    }
+
+    public void addEntry(Path path, Rect rect, RawTexture texture) {
+        mList.add(new Entry(path, rect, texture));
+    }
+
+    public Entry getEntry(Path path) {
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.path == path) return entry;
+        }
+        return null;
+    }
+
+    public boolean draw(GLCanvas canvas) {
+        boolean more = calculate(AnimationTime.get());
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.index < 0) continue;
+            entry.dest = mPositionProvider.getPosition(entry.index);
+            drawEntry(canvas, entry);
+        }
+        return more;
+    }
+
+    private void drawEntry(GLCanvas canvas, Entry entry) {
+        if (!entry.texture.isLoaded()) return;
+
+        int w = entry.texture.getWidth();
+        int h = entry.texture.getHeight();
+
+        Rect s = entry.source;
+        Rect d = entry.dest;
+
+        // the following calculation is based on d.width() == d.height()
+
+        float p = mProgress;
+
+        float fullScale = (float) d.height() / Math.min(s.width(), s.height());
+        float scale = fullScale * p + 1 * (1 - p);
+
+        float cx = d.centerX() * p + s.centerX() * (1 - p);
+        float cy = d.centerY() * p + s.centerY() * (1 - p);
+
+        float ch = s.height() * scale;
+        float cw = s.width() * scale;
+
+        if (w > h) {
+            // draw the center part
+            mTarget.set(cx - ch / 2, cy - ch / 2, cx + ch / 2, cy + ch / 2);
+            mSource.set((w - h) / 2, 0, (w + h) / 2, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(1 - p);
+
+            // draw the left part
+            mTarget.set(cx - cw / 2, cy - ch / 2, cx - ch / 2, cy + ch / 2);
+            mSource.set(0, 0, (w - h) / 2, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            // draw the right part
+            mTarget.set(cx + ch / 2, cy - ch / 2, cx + cw / 2, cy + ch / 2);
+            mSource.set((w + h) / 2, 0, w, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.restore();
+        } else {
+            // draw the center part
+            mTarget.set(cx - cw / 2, cy - cw / 2, cx + cw / 2, cy + cw / 2);
+            mSource.set(0, (h - w) / 2, w, (h + w) / 2);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(1 - p);
+
+            // draw the upper part
+            mTarget.set(cx - cw / 2, cy - ch / 2, cx + cw / 2, cy - cw / 2);
+            mSource.set(0, 0, w, (h - w) / 2);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            // draw the bottom part
+            mTarget.set(cx - cw / 2, cy + cw / 2, cx + cw / 2, cy + ch / 2);
+            mSource.set(0, (w + h) / 2, w, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.restore();
+        }
+    }
+
+    @Override
+    protected void onCalculate(float progress) {
+        mProgress = progress;
+    }
+
+    public void setPositionProvider(PositionProvider provider) {
+        mPositionProvider = provider;
+        if (mPositionProvider != null) {
+            for (int i = 0, n = mList.size(); i < n; ++i) {
+                Entry entry = mList.get(i);
+                entry.index = mPositionProvider.getItemIndex(entry.path);
+            }
+        }
+    }
+
+    @Override
+    public boolean acceptSlot(int index) {
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.index == index) return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 217a290..a7ecd06 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -16,59 +16,112 @@
 
 package com.android.gallery3d.ui;
 
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Message;
+import android.view.MotionEvent;
+import android.view.animation.AccelerateInterpolator;
+
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.PositionRepository.Position;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.Message;
-import android.os.SystemClock;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.view.animation.AccelerateInterpolator;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.util.RangeArray;
 
 public class PhotoView extends GLView {
     @SuppressWarnings("unused")
     private static final String TAG = "PhotoView";
+    private static final int PLACEHOLDER_COLOR = 0xFF222222;
 
     public static final int INVALID_SIZE = -1;
+    public static final long INVALID_DATA_VERSION =
+            MediaObject.INVALID_DATA_VERSION;
 
-    private static final int MSG_TRANSITION_COMPLETE = 1;
-    private static final int MSG_SHOW_LOADING = 2;
-    private static final int MSG_CANCEL_EXTRA_SCALING = 3;
+    public static class Size {
+        public int width;
+        public int height;
+    }
 
-    private static final long DELAY_SHOW_LOADING = 250; // 250ms;
+    public interface Model extends TileImageView.Model {
+        public int getCurrentIndex();
+        public void moveTo(int index);
 
-    private static final int TRANS_NONE = 0;
-    private static final int TRANS_SWITCH_NEXT = 3;
-    private static final int TRANS_SWITCH_PREVIOUS = 4;
+        // Returns the size for the specified picture. If the size information is
+        // not avaiable, width = height = 0.
+        public void getImageSize(int offset, Size size);
 
-    public static final int TRANS_SLIDE_IN_RIGHT = 1;
-    public static final int TRANS_SLIDE_IN_LEFT = 2;
-    public static final int TRANS_OPEN_ANIMATION = 5;
+        // Returns the media item for the specified picture.
+        public MediaItem getMediaItem(int offset);
 
-    private static final int LOADING_INIT = 0;
-    private static final int LOADING_TIMEOUT = 1;
-    private static final int LOADING_COMPLETE = 2;
-    private static final int LOADING_FAIL = 3;
+        // Returns the rotation for the specified picture.
+        public int getImageRotation(int offset);
 
-    private static final int ENTRY_PREVIOUS = 0;
-    private static final int ENTRY_NEXT = 1;
+        // This amends the getScreenNail() method of TileImageView.Model to get
+        // ScreenNail at previous (negative offset) or next (positive offset)
+        // positions. Returns null if the specified ScreenNail is unavailable.
+        public ScreenNail getScreenNail(int offset);
 
-    private static final int IMAGE_GAP = 96;
-    private static final int SWITCH_THRESHOLD = 256;
+        // Set this to true if we need the model to provide full images.
+        public void setNeedFullImage(boolean enabled);
+
+        // Returns true if the item is the Camera preview.
+        public boolean isCamera(int offset);
+
+        // Returns true if the item is the Panorama.
+        public boolean isPanorama(int offset);
+
+        // Returns true if the item is a Video.
+        public boolean isVideo(int offset);
+
+        public static final int LOADING_INIT = 0;
+        public static final int LOADING_COMPLETE = 1;
+        public static final int LOADING_FAIL = 2;
+
+        public int getLoadingState(int offset);
+    }
+
+    public interface Listener {
+        public void onSingleTapUp(int x, int y);
+        public void lockOrientation();
+        public void unlockOrientation();
+        public void onFullScreenChanged(boolean full);
+        public void onActionBarAllowed(boolean allowed);
+        public void onActionBarWanted();
+        public void onCurrentImageUpdated();
+    }
+
+    // The rules about orientation locking:
+    //
+    // (1) We need to lock the orientation if we are in page mode camera
+    // preview, so there is no (unwanted) rotation animation when the user
+    // rotates the device.
+    //
+    // (2) We need to unlock the orientation if we want to show the action bar
+    // because the action bar follows the system orientation.
+    //
+    // The rules about action bar:
+    //
+    // (1) If we are in film mode, we don't show action bar.
+    //
+    // (2) If we go from camera to gallery with capture animation, we show
+    // action bar.
+    private static final int MSG_CANCEL_EXTRA_SCALING = 2;
+    private static final int MSG_SWITCH_FOCUS = 3;
+    private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
+
+    private static final int MOVE_THRESHOLD = 256;
     private static final float SWIPE_THRESHOLD = 300f;
 
     private static final float DEFAULT_TEXT_SIZE = 20;
     private static float TRANSITION_SCALE_FACTOR = 0.74f;
+    private static final int ICON_RATIO = 6;
+
+    // whether we want to apply card deck effect in page mode.
+    private static final boolean CARD_EFFECT = true;
 
     // Used to calculate the scaling factor for the fading animation.
     private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
@@ -77,51 +130,57 @@
     private AccelerateInterpolator mAlphaInterpolator =
             new AccelerateInterpolator(0.9f);
 
-    public interface PhotoTapListener {
-        public void onSingleTapUp(int x, int y);
-    }
+    // We keep this many previous ScreenNails. (also this many next ScreenNails)
+    public static final int SCREEN_NAIL_MAX = 3;
 
-    // the previous/next image entries
-    private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2];
+    // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
+    // SCREEN_NAIL_MAX.
+    private final RangeArray<Picture> mPictures =
+            new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
 
-    private final ScaleGestureDetector mScaleDetector;
-    private final GestureDetector mGestureDetector;
-    private final DownUpDetector mDownUpDetector;
-
-    private PhotoTapListener mPhotoTapListener;
-
+    private final MyGestureListener mGestureListener;
+    private final GestureRecognizer mGestureRecognizer;
     private final PositionController mPositionController;
 
+    private Listener mListener;
     private Model mModel;
     private StringTexture mLoadingText;
     private StringTexture mNoThumbnailText;
-    private int mTransitionMode = TRANS_NONE;
-    private final TileImageView mTileView;
+    private TileImageView mTileView;
     private EdgeView mEdgeView;
     private Texture mVideoPlayIcon;
 
-    private boolean mShowVideoPlayIcon;
-    private ProgressSpinner mLoadingSpinner;
-
     private SynchronizedHandler mHandler;
 
-    private int mLoadingState = LOADING_COMPLETE;
-
-    private int mImageRotation;
-
-    private Path mOpenedItemPath;
-    private GalleryActivity mActivity;
     private Point mImageCenter = new Point();
     private boolean mCancelExtraScalingPending;
+    private boolean mFilmMode = false;
+    private int mDisplayRotation = 0;
+    private int mCompensation = 0;
+    private boolean mFullScreenCamera;
+    private Rect mCameraRelativeFrame = new Rect();
+    private Rect mCameraRect = new Rect();
+
+    // [mPrevBound, mNextBound] is the range of index for all pictures in the
+    // model, if we assume the index of current focused picture is 0.  So if
+    // there are some previous pictures, mPrevBound < 0, and if there are some
+    // next pictures, mNextBound > 0.
+    private int mPrevBound;
+    private int mNextBound;
+
+    // This variable prevents us doing snapback until its values goes to 0. This
+    // happens if the user gesture is still in progress or we are in a capture
+    // animation.
+    private int mHolding;
+    private static final int HOLD_TOUCH_DOWN = 1;
+    private static final int HOLD_CAPTURE_ANIMATION = 2;
 
     public PhotoView(GalleryActivity activity) {
-        mActivity = activity;
         mTileView = new TileImageView(activity);
         addComponent(mTileView);
         Context context = activity.getAndroidContext();
         mEdgeView = new EdgeView(context);
         addComponent(mEdgeView);
-        mLoadingSpinner = new ProgressSpinner(context);
         mLoadingText = StringTexture.newInstance(
                 context.getString(R.string.loading),
                 DEFAULT_TEXT_SIZE, Color.WHITE);
@@ -129,438 +188,609 @@
                 context.getString(R.string.no_thumbnail),
                 DEFAULT_TEXT_SIZE, Color.WHITE);
 
-        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
-            @Override
-            public void handleMessage(Message message) {
-                switch (message.what) {
-                    case MSG_TRANSITION_COMPLETE: {
-                        onTransitionComplete();
-                        break;
+        mHandler = new MyHandler(activity.getGLRoot());
+
+        mGestureListener = new MyGestureListener();
+        mGestureRecognizer = new GestureRecognizer(context, mGestureListener);
+
+        mPositionController = new PositionController(context,
+                new PositionController.Listener() {
+                    public void invalidate() {
+                        PhotoView.this.invalidate();
                     }
-                    case MSG_SHOW_LOADING: {
-                        if (mLoadingState == LOADING_INIT) {
-                            // We don't need the opening animation
-                            mOpenedItemPath = null;
-
-                            mLoadingSpinner.startAnimation();
-                            mLoadingState = LOADING_TIMEOUT;
-                            invalidate();
-                        }
-                        break;
+                    public boolean isHolding() {
+                        return mHolding != 0;
                     }
-                    case MSG_CANCEL_EXTRA_SCALING: {
-                        cancelScaleGesture();
-                        mPositionController.setExtraScalingRange(false);
-                        mCancelExtraScalingPending = false;
-                        break;
+                    public void onPull(int offset, int direction) {
+                        mEdgeView.onPull(offset, direction);
                     }
-                    default: throw new AssertionError(message.what);
-                }
-            }
-        };
-
-        mGestureDetector = new GestureDetector(context,
-                new MyGestureListener(), null, true /* ignoreMultitouch */);
-        mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener());
-        mDownUpDetector = new DownUpDetector(new MyDownUpListener());
-
-        for (int i = 0, n = mScreenNails.length; i < n; ++i) {
-            mScreenNails[i] = new ScreenNailEntry();
-        }
-
-        mPositionController = new PositionController(this, context, mEdgeView);
+                    public void onRelease() {
+                        mEdgeView.onRelease();
+                    }
+                    public void onAbsorb(int velocity, int direction) {
+                        mEdgeView.onAbsorb(velocity, direction);
+                    }
+                });
         mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            if (i == 0) {
+                mPictures.put(i, new FullPicture());
+            } else {
+                mPictures.put(i, new ScreenNailPicture(i));
+            }
+        }
     }
 
-
     public void setModel(Model model) {
-        if (mModel == model) return;
         mModel = model;
-        mTileView.setModel(model);
-        if (model != null) notifyOnNewImage();
+        mTileView.setModel(mModel);
     }
 
-    public void setPhotoTapListener(PhotoTapListener listener) {
-        mPhotoTapListener = listener;
-    }
-
-    private void setTileViewPosition(int centerX, int centerY, float scale) {
-        TileImageView t = mTileView;
-
-        // Calculate the move-out progress value.
-        RectF bounds = mPositionController.getImageBounds();
-        int left = Math.round(bounds.left);
-        int right = Math.round(bounds.right);
-        int width = getWidth();
-        float progress = calculateMoveOutProgress(left, right, width);
-        progress = Utils.clamp(progress, -1f, 1f);
-
-        // We only want to apply the fading animation if the scrolling movement
-        // is to the right.
-        if (progress < 0) {
-            if (right - left < width) {
-                // If the picture is narrower than the view, keep it at the center
-                // of the view.
-                centerX = mPositionController.getImageWidth() / 2;
-            } else {
-                // If the picture is wider than the view (it's zoomed-in), keep
-                // the left edge of the object align the the left edge of the view.
-                centerX = Math.round(width / 2f / scale);
-            }
-            scale *= getScrollScale(progress);
-            t.setAlpha(getScrollAlpha(progress));
+    class MyHandler extends SynchronizedHandler {
+        public MyHandler(GLRoot root) {
+            super(root);
         }
 
-        // set the position of the tile view
-        int inverseX = mPositionController.getImageWidth() - centerX;
-        int inverseY = mPositionController.getImageHeight() - centerY;
-        int rotation = mImageRotation;
-        switch (rotation) {
-            case 0: t.setPosition(centerX, centerY, scale, 0); break;
-            case 90: t.setPosition(centerY, inverseX, scale, 90); break;
-            case 180: t.setPosition(inverseX, inverseY, scale, 180); break;
-            case 270: t.setPosition(inverseY, centerX, scale, 270); break;
-            default: throw new IllegalArgumentException(String.valueOf(rotation));
-        }
-    }
-
-    public void setPosition(int centerX, int centerY, float scale) {
-        setTileViewPosition(centerX, centerY, scale);
-        layoutScreenNails();
-    }
-
-    private void updateScreenNailEntry(int which, ImageData data) {
-        if (mTransitionMode == TRANS_SWITCH_NEXT
-                || mTransitionMode == TRANS_SWITCH_PREVIOUS) {
-            // ignore screen nail updating during switching
-            return;
-        }
-        ScreenNailEntry entry = mScreenNails[which];
-        if (data == null) {
-            entry.set(false, null, 0);
-        } else {
-            entry.set(true, data.bitmap, data.rotation);
-        }
-    }
-
-    // -1 previous, 0 current, 1 next
-    public void notifyImageInvalidated(int which) {
-        switch (which) {
-            case -1: {
-                updateScreenNailEntry(
-                        ENTRY_PREVIOUS, mModel.getPreviousImage());
-                layoutScreenNails();
-                invalidate();
-                break;
-            }
-            case 1: {
-                updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
-                layoutScreenNails();
-                invalidate();
-                break;
-            }
-            case 0: {
-                // mImageWidth and mImageHeight will get updated
-                mTileView.notifyModelInvalidated();
-                mTileView.setAlpha(1.0f);
-
-                mImageRotation = mModel.getImageRotation();
-                if (((mImageRotation / 90) & 1) == 0) {
-                    mPositionController.setImageSize(
-                            mTileView.mImageWidth, mTileView.mImageHeight);
-                } else {
-                    mPositionController.setImageSize(
-                            mTileView.mImageHeight, mTileView.mImageWidth);
+        @Override
+        public void handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_CANCEL_EXTRA_SCALING: {
+                    mGestureRecognizer.cancelScale();
+                    mPositionController.setExtraScalingRange(false);
+                    mCancelExtraScalingPending = false;
+                    break;
                 }
-                updateLoadingState();
-                break;
+                case MSG_SWITCH_FOCUS: {
+                    switchFocus();
+                    break;
+                }
+                case MSG_CAPTURE_ANIMATION_DONE: {
+                    // message.arg1 is the offset parameter passed to
+                    // switchWithCaptureAnimation().
+                    captureAnimationDone(message.arg1);
+                    break;
+                }
+                default: throw new AssertionError(message.what);
             }
         }
+    };
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Data/Image change notifications
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
+        mPrevBound = prevBound;
+        mNextBound = nextBound;
+
+        // Move the boxes
+        mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
+                mModel.isCamera(0));
+
+        // Update the ScreenNails.
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            mPictures.get(i).reload();
+        }
+
+        invalidate();
     }
 
-    private void updateLoadingState() {
-        // Possible transitions of mLoadingState:
-        //        INIT --> TIMEOUT, COMPLETE, FAIL
-        //     TIMEOUT --> COMPLETE, FAIL, INIT
-        //    COMPLETE --> INIT
-        //        FAIL --> INIT
-        if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) {
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mLoadingState = LOADING_COMPLETE;
-        } else if (mModel.isFailedToLoad()) {
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mLoadingState = LOADING_FAIL;
-        } else if (mLoadingState != LOADING_INIT) {
-            mLoadingState = LOADING_INIT;
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mHandler.sendEmptyMessageDelayed(
-                    MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
+    public void notifyImageChange(int index) {
+        if (index == 0) {
+            mListener.onCurrentImageUpdated();
         }
-    }
-
-    public void notifyModelInvalidated() {
-        if (mModel == null) {
-            updateScreenNailEntry(ENTRY_PREVIOUS, null);
-            updateScreenNailEntry(ENTRY_NEXT, null);
-        } else {
-            updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage());
-            updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
-        }
-        layoutScreenNails();
-
-        if (mModel == null) {
-            mTileView.notifyModelInvalidated();
-            mTileView.setAlpha(1.0f);
-            mImageRotation = 0;
-            mPositionController.setImageSize(0, 0);
-            updateLoadingState();
-        } else {
-            notifyImageInvalidated(0);
-        }
-    }
-
-    @Override
-    protected boolean onTouch(MotionEvent event) {
-        mGestureDetector.onTouchEvent(event);
-        mScaleDetector.onTouchEvent(event);
-        mDownUpDetector.onTouchEvent(event);
-        return true;
+        mPictures.get(index).reload();
+        invalidate();
     }
 
     @Override
     protected void onLayout(
             boolean changeSize, int left, int top, int right, int bottom) {
-        mTileView.layout(left, top, right, bottom);
-        mEdgeView.layout(left, top, right, bottom);
+        int w = right - left;
+        int h = bottom - top;
+        mTileView.layout(0, 0, w, h);
+        mEdgeView.layout(0, 0, w, h);
+
+        GLRoot root = getGLRoot();
+        int displayRotation = root.getDisplayRotation();
+        int compensation = root.getCompensation();
+        if (mDisplayRotation != displayRotation
+                || mCompensation != compensation) {
+            mDisplayRotation = displayRotation;
+            mCompensation = compensation;
+
+            // We need to change the size and rotation of the Camera ScreenNail,
+            // but we don't want it to animate because the size doen't actually
+            // change in the eye of the user.
+            for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+                Picture p = mPictures.get(i);
+                if (p.isCamera()) {
+                    p.forceSize();
+                }
+            }
+        }
+
+        updateCameraRect();
+        mPositionController.setConstrainedFrame(mCameraRect);
         if (changeSize) {
             mPositionController.setViewSize(getWidth(), getHeight());
-            for (ScreenNailEntry entry : mScreenNails) {
-                entry.updateDrawingSize();
-            }
         }
     }
 
-    private static int gapToSide(int imageWidth, int viewWidth) {
-        return Math.max(0, (viewWidth - imageWidth) / 2);
-    }
-
-    /*
-     * Here is how we layout the screen nails
-     *
-     *  previous            current           next
-     *  ___________       ________________     __________
-     * |  _______  |     |   __________   |   |  ______  |
-     * | |       | |     |  |   right->|  |   | |      | |
-     * | |       |<-------->|<--left   |  |   | |      | |
-     * | |_______| |  |  |  |__________|  |   | |______| |
-     * |___________|  |  |________________|   |__________|
-     *                |  <--> gapToSide()
-     *                |
-     * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide)
-     */
-    private void layoutScreenNails() {
-        int width = getWidth();
-        int height = getHeight();
-
-        // Use the image width in AC, since we may fake the size if the
-        // image is unavailable
-        RectF bounds = mPositionController.getImageBounds();
-        int left = Math.round(bounds.left);
-        int right = Math.round(bounds.right);
-        int gap = gapToSide(right - left, width);
-
-        // layout the previous image
-        ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS];
-
-        if (entry.isEnabled()) {
-            entry.layoutRightEdgeAt(left - (
-                    IMAGE_GAP + Math.max(gap, entry.gapToSide())));
-        }
-
-        // layout the next image
-        entry = mScreenNails[ENTRY_NEXT];
-        if (entry.isEnabled()) {
-            entry.layoutLeftEdgeAt(right + (
-                    IMAGE_GAP + Math.max(gap, entry.gapToSide())));
-        }
-    }
-
-    @Override
-    protected void render(GLCanvas canvas) {
-        PositionController p = mPositionController;
-        boolean drawScreenNail = (mTransitionMode != TRANS_SLIDE_IN_LEFT
-                && mTransitionMode != TRANS_SLIDE_IN_RIGHT
-                && mTransitionMode != TRANS_OPEN_ANIMATION);
-
-        // Draw the next photo
-        if (drawScreenNail) {
-            ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
-            if (nextNail.mVisible) nextNail.draw(canvas, true);
-        }
-
-        // Draw the current photo
-        if (mLoadingState == LOADING_COMPLETE) {
-            super.render(canvas);
-        }
-
-        // If the photo is loaded, draw the message/icon at the center of it,
-        // otherwise draw the message/icon at the center of the view.
-        if (mLoadingState == LOADING_COMPLETE) {
-            mTileView.getImageCenter(mImageCenter);
-            renderMessage(canvas, mImageCenter.x, mImageCenter.y);
-        } else {
-            renderMessage(canvas, getWidth() / 2, getHeight() / 2);
-        }
-
-        // Draw the previous photo
-        if (drawScreenNail) {
-            ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
-            if (prevNail.mVisible) prevNail.draw(canvas, false);
-        }
-
-        if (mPositionController.advanceAnimation()) invalidate();
-    }
-
-    private void renderMessage(GLCanvas canvas, int x, int y) {
-        // Draw the progress spinner and the text below it
-        //
-        // (x, y) is where we put the center of the spinner.
-        // s is the size of the video play icon, and we use s to layout text
-        // because we want to keep the text at the same place when the video
-        // play icon is shown instead of the spinner.
+    // Update the camera rectangle due to layout change or camera relative frame
+    // change.
+    private void updateCameraRect() {
+        // Get the width and height in framework orientation because the given
+        // mCameraRelativeFrame is in that coordinates.
         int w = getWidth();
         int h = getHeight();
-        int s = Math.min(getWidth(), getHeight()) / 6;
+        if (mCompensation % 180 != 0) {
+            int tmp = w;
+            w = h;
+            h = tmp;
+        }
+        int l = mCameraRelativeFrame.left;
+        int t = mCameraRelativeFrame.top;
+        int r = mCameraRelativeFrame.right;
+        int b = mCameraRelativeFrame.bottom;
 
-        if (mLoadingState == LOADING_TIMEOUT) {
-            StringTexture m = mLoadingText;
-            ProgressSpinner r = mLoadingSpinner;
-            r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2);
-            m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
-            invalidate(); // we need to keep the spinner rotating
-        } else if (mLoadingState == LOADING_FAIL) {
-            StringTexture m = mNoThumbnailText;
-            m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+        // Now convert it to the coordinates we are using.
+        switch (mCompensation) {
+            case 0: mCameraRect.set(l, t, r, b); break;
+            case 90: mCameraRect.set(h - b, l, h - t, r); break;
+            case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
+            case 270: mCameraRect.set(t, w - r, b, w - l); break;
         }
 
-        // Draw the video play icon (in the place where the spinner was)
-        if (mShowVideoPlayIcon
-                && mLoadingState != LOADING_INIT
-                && mLoadingState != LOADING_TIMEOUT) {
-            mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
-        }
+        Log.d(TAG, "compensation = " + mCompensation
+                + ", CameraRelativeFrame = " + mCameraRelativeFrame
+                + ", mCameraRect = " + mCameraRect);
     }
 
-    private void stopCurrentSwipingIfNeeded() {
-        // Enable fast sweeping
-        if (mTransitionMode == TRANS_SWITCH_NEXT) {
-            mTransitionMode = TRANS_NONE;
-            mPositionController.stopAnimation();
-            switchToNextImage();
-        } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) {
-            mTransitionMode = TRANS_NONE;
-            mPositionController.stopAnimation();
-            switchToPreviousImage();
-        }
+    public void setCameraRelativeFrame(Rect frame) {
+        mCameraRelativeFrame.set(frame);
+        updateCameraRect();
+        // Originally we do
+        //     mPositionController.setConstrainedFrame(mCameraRect);
+        // here, but it is moved to a parameter of the setImageSize() call, so
+        // it can be updated atomically with the CameraScreenNail's size change.
     }
 
-    private boolean swipeImages(float velocity) {
-        if (mTransitionMode != TRANS_NONE
-                && mTransitionMode != TRANS_SWITCH_NEXT
-                && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false;
-
-        ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
-        ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
-
-        int width = getWidth();
-
-        // If we are at the edge of the current photo and the sweeping velocity
-        // exceeds the threshold, switch to next / previous image.
-        PositionController controller = mPositionController;
-        boolean isMinimal = controller.isAtMinimalScale();
-
-        if (velocity < -SWIPE_THRESHOLD &&
-                (isMinimal || controller.isAtRightEdge())) {
-            stopCurrentSwipingIfNeeded();
-            if (next.isEnabled()) {
-                mTransitionMode = TRANS_SWITCH_NEXT;
-                controller.startHorizontalSlide(next.mOffsetX - width / 2);
-                return true;
-            }
-        } else if (velocity > SWIPE_THRESHOLD &&
-                (isMinimal || controller.isAtLeftEdge())) {
-            stopCurrentSwipingIfNeeded();
-            if (prev.isEnabled()) {
-                mTransitionMode = TRANS_SWITCH_PREVIOUS;
-                controller.startHorizontalSlide(prev.mOffsetX - width / 2);
-                return true;
-            }
-        }
-
-        return false;
+    // Returns the rotation we need to do to the camera texture before drawing
+    // it to the canvas, assuming the camera texture is correct when the device
+    // is in its natural orientation.
+    private int getCameraRotation() {
+        return (mCompensation - mDisplayRotation + 360) % 360;
     }
 
-    public boolean snapToNeighborImage() {
-        if (mTransitionMode != TRANS_NONE) return false;
-
-        ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
-        ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
-
-        int width = getWidth();
-        PositionController controller = mPositionController;
-
-        RectF bounds = controller.getImageBounds();
-        int left = Math.round(bounds.left);
-        int right = Math.round(bounds.right);
-        int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width);
-
-        // If we have moved the picture a lot, switching.
-        if (next.isEnabled() && threshold < width - right) {
-            mTransitionMode = TRANS_SWITCH_NEXT;
-            controller.startHorizontalSlide(next.mOffsetX - width / 2);
-            return true;
-        }
-        if (prev.isEnabled() && threshold < left) {
-            mTransitionMode = TRANS_SWITCH_PREVIOUS;
-            controller.startHorizontalSlide(prev.mOffsetX - width / 2);
-            return true;
-        }
-
-        return false;
+    private int getPanoramaRotation() {
+        return mCompensation;
     }
 
-    private boolean mIgnoreUpEvent = false;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Pictures
+    ////////////////////////////////////////////////////////////////////////////
 
-    private class MyGestureListener
-            extends GestureDetector.SimpleOnGestureListener {
-        @Override
-        public boolean onScroll(
-                MotionEvent e1, MotionEvent e2, float dx, float dy) {
-            if (mTransitionMode != TRANS_NONE) return true;
+    private interface Picture {
+        void reload();
+        void draw(GLCanvas canvas, Rect r);
+        void setScreenNail(ScreenNail s);
+        boolean isCamera();  // whether the picture is a camera preview
+        void forceSize();  // called when mCompensation changes
+    };
 
-            ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
-            ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
+    class FullPicture implements Picture {
+        private int mRotation;
+        private boolean mIsCamera;
+        private boolean mIsPanorama;
+        private boolean mIsVideo;
+        private int mLoadingState = Model.LOADING_INIT;
+        private boolean mWasCameraCenter;
+        private int mWidth, mHeight;
 
-            mPositionController.startScroll(dx, dy, next.isEnabled(),
-                    prev.isEnabled());
-            return true;
+        public void FullPicture(TileImageView tileView) {
+            mTileView = tileView;
         }
 
         @Override
-        public boolean onSingleTapUp(MotionEvent e) {
-            if (mPhotoTapListener != null) {
-                mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY());
-            }
-            return true;
+        public void reload() {
+            // mImageWidth and mImageHeight will get updated
+            mTileView.notifyModelInvalidated();
+
+            mIsCamera = mModel.isCamera(0);
+            mIsPanorama = mModel.isPanorama(0);
+            mIsVideo = mModel.isVideo(0);
+            mLoadingState = mModel.getLoadingState(0);
+            setScreenNail(mModel.getScreenNail(0));
+            setSize();
+        }
+
+        private void setSize() {
+            updateSize();
+            mPositionController.setImageSize(0, mWidth, mHeight,
+                    mIsCamera ? mCameraRect : null);
         }
 
         @Override
-        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
-                float velocityY) {
-            if (swipeImages(velocityX)) {
+        public void forceSize() {
+            updateSize();
+            mPositionController.forceImageSize(0, mWidth, mHeight);
+        }
+
+        private void updateSize() {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera) {
+                mRotation = getCameraRotation();
+            } else {
+                mRotation = mModel.getImageRotation(0);
+            }
+
+            int w = mTileView.mImageWidth;
+            int h = mTileView.mImageHeight;
+            mWidth = getRotated(mRotation, w, h);
+            mHeight = getRotated(mRotation, h, w);
+        }
+
+        @Override
+        public void draw(GLCanvas canvas, Rect r) {
+            drawTileView(canvas, r);
+
+            // We want to have the following transitions:
+            // (1) Move camera preview out of its place: switch to film mode
+            // (2) Move camera preview into its place: switch to page mode
+            // The extra mWasCenter check makes sure (1) does not apply if in
+            // page mode, we move _to_ the camera preview from another picture.
+
+            // Holdings except touch-down prevent the transitions.
+            if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
+
+            boolean isCenter = mPositionController.isCenter();
+            boolean isCameraCenter = mIsCamera && isCenter;
+
+            if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) {
+                // Temporary disabled to de-emphasize filmstrip.
+                // setFilmMode(true);
+            } else if (!mWasCameraCenter && isCameraCenter && mFilmMode) {
+                setFilmMode(false);
+            }
+
+            if (isCameraCenter && !mFilmMode) {
+                // Move into camera in page mode, lock
+                mListener.lockOrientation();
+            }
+
+            mWasCameraCenter = isCameraCenter;
+        }
+
+        @Override
+        public void setScreenNail(ScreenNail s) {
+            mTileView.setScreenNail(s);
+        }
+
+        @Override
+        public boolean isCamera() {
+            return mIsCamera;
+        }
+
+        private void drawTileView(GLCanvas canvas, Rect r) {
+            float imageScale = mPositionController.getImageScale();
+            int viewW = getWidth();
+            int viewH = getHeight();
+            float cx = r.exactCenterX();
+            float cy = r.exactCenterY();
+            float scale = 1f;  // the scaling factor due to card effect
+
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+            float filmRatio = mPositionController.getFilmRatio();
+            boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
+                    && filmRatio != 1f && !mPictures.get(-1).isCamera()
+                    && !mPositionController.inOpeningAnimation();
+            if (wantsCardEffect) {
+                // Calculate the move-out progress value.
+                int left = r.left;
+                int right = r.right;
+                float progress = calculateMoveOutProgress(left, right, viewW);
+                progress = Utils.clamp(progress, -1f, 1f);
+
+                // We only want to apply the fading animation if the scrolling
+                // movement is to the right.
+                if (progress < 0) {
+                    scale = getScrollScale(progress);
+                    float alpha = getScrollAlpha(progress);
+                    scale = interpolate(filmRatio, scale, 1f);
+                    alpha = interpolate(filmRatio, alpha, 1f);
+
+                    imageScale *= scale;
+                    canvas.multiplyAlpha(alpha);
+
+                    float cxPage;  // the cx value in page mode
+                    if (right - left <= viewW) {
+                        // If the picture is narrower than the view, keep it at
+                        // the center of the view.
+                        cxPage = viewW / 2f;
+                    } else {
+                        // If the picture is wider than the view (it's
+                        // zoomed-in), keep the left edge of the object align
+                        // the the left edge of the view.
+                        cxPage = (right - left) * scale / 2f;
+                    }
+                    cx = interpolate(filmRatio, cxPage, cx);
+                }
+            }
+
+            // Draw the tile view.
+            setTileViewPosition(cx, cy, viewW, viewH, imageScale);
+            PhotoView.super.render(canvas);
+
+            // Draw the play video icon and the message.
+            canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
+            int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
+            }
+
+            // Draw a debug indicator showing which picture has focus (index ==
+            // 0).
+            //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
+
+            canvas.restore();
+        }
+
+        // Set the position of the tile view
+        private void setTileViewPosition(float cx, float cy,
+                int viewW, int viewH, float scale) {
+            // Find out the bitmap coordinates of the center of the view
+            int imageW = mPositionController.getImageWidth();
+            int imageH = mPositionController.getImageHeight();
+            int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
+            int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
+
+            int inverseX = imageW - centerX;
+            int inverseY = imageH - centerY;
+            int x, y;
+            switch (mRotation) {
+                case 0: x = centerX; y = centerY; break;
+                case 90: x = centerY; y = inverseX; break;
+                case 180: x = inverseX; y = inverseY; break;
+                case 270: x = inverseY; y = centerX; break;
+                default:
+                    throw new RuntimeException(String.valueOf(mRotation));
+            }
+            mTileView.setPosition(x, y, scale, mRotation);
+        }
+    }
+
+    private class ScreenNailPicture implements Picture {
+        private int mIndex;
+        private int mRotation;
+        private ScreenNail mScreenNail;
+        private Size mSize = new Size();
+        private boolean mIsCamera;
+        private boolean mIsPanorama;
+        private boolean mIsVideo;
+        private int mLoadingState = Model.LOADING_INIT;
+        private int mWidth, mHeight;
+
+        public ScreenNailPicture(int index) {
+            mIndex = index;
+        }
+
+        @Override
+        public void reload() {
+            mIsCamera = mModel.isCamera(mIndex);
+            mIsPanorama = mModel.isPanorama(mIndex);
+            mIsVideo = mModel.isVideo(mIndex);
+            mLoadingState = mModel.getLoadingState(mIndex);
+            setScreenNail(mModel.getScreenNail(mIndex));
+            setSize();
+        }
+
+        @Override
+        public void draw(GLCanvas canvas, Rect r) {
+            if (mScreenNail == null) {
+                // Draw a placeholder rectange if there should be a picture in
+                // this position (but somehow there isn't).
+                if (mIndex >= mPrevBound && mIndex <= mNextBound) {
+                    drawPlaceHolder(canvas, r);
+                }
+                return;
+            }
+            if (r.left >= getWidth() || r.right <= 0 ||
+                    r.top >= getHeight() || r.bottom <= 0) {
+                mScreenNail.noDraw();
+                return;
+            }
+
+            float filmRatio = mPositionController.getFilmRatio();
+            boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
+                    && filmRatio != 1f && !mPictures.get(0).isCamera();
+            int w = getWidth();
+            int cx = wantsCardEffect
+                    ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
+                    : r.centerX();
+            int cy = r.centerY();
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.translate(cx, cy);
+            if (wantsCardEffect) {
+                float progress = (float) (w / 2 - r.centerX()) / w;
+                progress = Utils.clamp(progress, -1, 1);
+                float alpha = getScrollAlpha(progress);
+                float scale = getScrollScale(progress);
+                alpha = interpolate(filmRatio, alpha, 1f);
+                scale = interpolate(filmRatio, scale, 1f);
+                canvas.multiplyAlpha(alpha);
+                canvas.scale(scale, scale, 1);
+            }
+            if (mRotation != 0) {
+                canvas.rotate(mRotation, 0, 0, 1);
+            }
+            int drawW = getRotated(mRotation, r.width(), r.height());
+            int drawH = getRotated(mRotation, r.height(), r.width());
+            mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
+            if (isScreenNailAnimating()) {
+                invalidate();
+            }
+            int s = Math.min(drawW, drawH);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
+            }
+            canvas.restore();
+        }
+
+        private boolean isScreenNailAnimating() {
+            return (mScreenNail instanceof BitmapScreenNail)
+                    && ((BitmapScreenNail) mScreenNail).isAnimating();
+        }
+
+        @Override
+        public void setScreenNail(ScreenNail s) {
+            mScreenNail = s;
+        }
+
+        private void setSize() {
+            updateSize();
+            mPositionController.setImageSize(mIndex, mWidth, mHeight, null);
+        }
+
+        @Override
+        public void forceSize() {
+            updateSize();
+            mPositionController.forceImageSize(mIndex, mWidth, mHeight);
+        }
+
+        private void updateSize() {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera) {
+                mRotation = getCameraRotation();
+            } else {
+                mRotation = mModel.getImageRotation(mIndex);
+            }
+
+            int w = 0, h = 0;
+            if (mScreenNail != null) {
+                w = mScreenNail.getWidth();
+                h = mScreenNail.getHeight();
+            } else if (mModel != null) {
+                // If we don't have ScreenNail available, we can still try to
+                // get the size information of it.
+                mModel.getImageSize(mIndex, mSize);
+                w = mSize.width;
+                h = mSize.height;
+            }
+
+            mWidth = getRotated(mRotation, w, h);
+            mHeight = getRotated(mRotation, h, w);
+        }
+
+        @Override
+        public boolean isCamera() {
+            return mIsCamera;
+        }
+    }
+
+    // Draw a gray placeholder in the specified rectangle.
+    private void drawPlaceHolder(GLCanvas canvas, Rect r) {
+        canvas.fillRect(r.left, r.top, r.width(), r.height(), PLACEHOLDER_COLOR);
+    }
+
+    // Draw the video play icon (in the place where the spinner was)
+    private void drawVideoPlayIcon(GLCanvas canvas, int side) {
+        int s = side / ICON_RATIO;
+        // Draw the video play icon at the center
+        mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
+    }
+
+    // Draw the "no thumbnail" message
+    private void drawLoadingFailMessage(GLCanvas canvas) {
+        StringTexture m = mNoThumbnailText;
+        m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
+    }
+
+    private static int getRotated(int degree, int original, int theother) {
+        return (degree % 180 == 0) ? original : theother;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Gestures Handling
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected boolean onTouch(MotionEvent event) {
+        mGestureRecognizer.onTouchEvent(event);
+        return true;
+    }
+
+    private class MyGestureListener implements GestureRecognizer.Listener {
+        private boolean mIgnoreUpEvent = false;
+        // If we can change mode for this scale gesture.
+        private boolean mCanChangeMode;
+        // If we have changed the film mode in this scaling gesture.
+        private boolean mModeChanged;
+        // If this scaling gesture should be ignored.
+        private boolean mIgnoreScalingGesture;
+        // whether the down action happened while the view is scrolling.
+        private boolean mDownInScrolling;
+        // If we should ignore all gestures other than onSingleTapUp.
+        private boolean mIgnoreSwipingGesture;
+
+        @Override
+        public boolean onSingleTapUp(float x, float y) {
+            // We do this in addition to onUp() because we want the snapback of
+            // setFilmMode to happen.
+            mHolding &= ~HOLD_TOUCH_DOWN;
+
+            if (mFilmMode && !mDownInScrolling) {
+                switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
+                setFilmMode(false);
                 mIgnoreUpEvent = true;
-            } else if (mTransitionMode != TRANS_NONE) {
-                // do nothing
+                return true;
+            }
+
+            if (mListener != null) {
+                // Do the inverse transform of the touch coordinates.
+                Matrix m = getGLRoot().getCompensationMatrix();
+                Matrix inv = new Matrix();
+                m.invert(inv);
+                float[] pts = new float[] {x, y};
+                inv.mapPoints(pts);
+                mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onDoubleTap(float x, float y) {
+            if (mIgnoreSwipingGesture) return true;
+            if (mPictures.get(0).isCamera()) return false;
+            PositionController controller = mPositionController;
+            float scale = controller.getImageScale();
+            // onDoubleTap happened on the second ACTION_DOWN.
+            // We need to ignore the next UP event.
+            mIgnoreUpEvent = true;
+            if (scale <= 1.0f || controller.isAtMinimalScale()) {
+                controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f));
+            } else {
+                controller.resetToFullView();
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onScroll(float dx, float dy) {
+            if (mIgnoreSwipingGesture) return true;
+            mPositionController.startScroll(-dx, -dy);
+            return true;
+        }
+
+        @Override
+        public boolean onFling(float velocityX, float velocityY) {
+            if (mIgnoreSwipingGesture) return true;
+            if (swipeImages(velocityX, velocityY)) {
+                mIgnoreUpEvent = true;
             } else if (mPositionController.fling(velocityX, velocityY)) {
                 mIgnoreUpEvent = true;
             }
@@ -568,279 +798,439 @@
         }
 
         @Override
-        public boolean onDoubleTap(MotionEvent e) {
-            if (mTransitionMode != TRANS_NONE) return true;
-            PositionController controller = mPositionController;
-            float scale = controller.getCurrentScale();
-            // onDoubleTap happened on the second ACTION_DOWN.
-            // We need to ignore the next UP event.
-            mIgnoreUpEvent = true;
-            if (scale <= 1.0f || controller.isAtMinimalScale()) {
-                controller.zoomIn(
-                        e.getX(), e.getY(), Math.max(1.5f, scale * 1.5f));
-            } else {
-                controller.resetToFullView();
+        public boolean onScaleBegin(float focusX, float focusY) {
+            if (mIgnoreSwipingGesture) return true;
+            // We ignore the scaling gesture if it is a camera preview.
+            mIgnoreScalingGesture = mPictures.get(0).isCamera();
+            if (mIgnoreScalingGesture) {
+                return true;
             }
+            mPositionController.beginScale(focusX, focusY);
+            // We can change mode if we are in film mode, or we are in page
+            // mode and at minimal scale.
+            mCanChangeMode = mFilmMode
+                    || mPositionController.isAtMinimalScale();
+            mModeChanged = false;
             return true;
         }
-    }
-
-    private class MyScaleListener
-            extends ScaleGestureDetector.SimpleOnScaleGestureListener {
 
         @Override
-        public boolean onScale(ScaleGestureDetector detector) {
-            float scale = detector.getScaleFactor();
-            if (Float.isNaN(scale) || Float.isInfinite(scale)
-                    || mTransitionMode != TRANS_NONE) return true;
-            boolean outOfRange = mPositionController.scaleBy(scale,
-                    detector.getFocusX(), detector.getFocusY());
-            if (outOfRange) {
-                if (!mCancelExtraScalingPending) {
-                    mHandler.sendEmptyMessageDelayed(
-                            MSG_CANCEL_EXTRA_SCALING, 700);
-                    mPositionController.setExtraScalingRange(true);
-                    mCancelExtraScalingPending = true;
+        public boolean onScale(float focusX, float focusY, float scale) {
+            if (mIgnoreSwipingGesture) return true;
+            if (mIgnoreScalingGesture) return true;
+            if (mModeChanged) return true;
+            if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
+
+            // We wait for the scale change accumulated to a large enough change
+            // before reacting to it. Otherwise we may mistakenly treat a
+            // zoom-in gesture as zoom-out or vice versa.
+            if (scale > 0.99f && scale < 1.01f) return false;
+
+            int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
+
+            // If mode changes, we treat this scaling gesture has ended.
+            if (mCanChangeMode) {
+                if ((outOfRange < 0 && !mFilmMode) ||
+                        (outOfRange > 0 && mFilmMode)) {
+                    stopExtraScalingIfNeeded();
+
+                    // Removing the touch down flag allows snapback to happen
+                    // for film mode change.
+                    mHolding &= ~HOLD_TOUCH_DOWN;
+                    setFilmMode(!mFilmMode);
+
+                    // We need to call onScaleEnd() before setting mModeChanged
+                    // to true.
+                    onScaleEnd();
+                    mModeChanged = true;
+                    return true;
                 }
+           }
+
+            if (outOfRange != 0) {
+                startExtraScalingIfNeeded();
             } else {
-                if (mCancelExtraScalingPending) {
-                    mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
-                    mPositionController.setExtraScalingRange(false);
-                    mCancelExtraScalingPending = false;
-                }
+                stopExtraScalingIfNeeded();
             }
             return true;
         }
 
         @Override
-        public boolean onScaleBegin(ScaleGestureDetector detector) {
-            if (mTransitionMode != TRANS_NONE) return false;
-            mPositionController.beginScale(
-                detector.getFocusX(), detector.getFocusY());
-            return true;
-        }
-
-        @Override
-        public void onScaleEnd(ScaleGestureDetector detector) {
+        public void onScaleEnd() {
+            if (mIgnoreSwipingGesture) return;
+            if (mIgnoreScalingGesture) return;
+            if (mModeChanged) return;
             mPositionController.endScale();
-            snapToNeighborImage();
         }
-    }
 
-    private void cancelScaleGesture() {
-        long now = SystemClock.uptimeMillis();
-        MotionEvent cancelEvent = MotionEvent.obtain(
-                now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
-        mScaleDetector.onTouchEvent(cancelEvent);
-        cancelEvent.recycle();
-    }
-
-    public boolean jumpTo(int index) {
-        if (mTransitionMode != TRANS_NONE) return false;
-        mModel.jumpTo(index);
-        return true;
-    }
-
-    public void notifyOnNewImage() {
-        mPositionController.setImageSize(0, 0);
-    }
-
-    public void startSlideInAnimation(int direction) {
-        PositionController a = mPositionController;
-        a.stopAnimation();
-        switch (direction) {
-            case TRANS_SLIDE_IN_LEFT:
-            case TRANS_SLIDE_IN_RIGHT: {
-                mTransitionMode = direction;
-                a.startSlideInAnimation(direction);
-                break;
+        private void startExtraScalingIfNeeded() {
+            if (!mCancelExtraScalingPending) {
+                mHandler.sendEmptyMessageDelayed(
+                        MSG_CANCEL_EXTRA_SCALING, 700);
+                mPositionController.setExtraScalingRange(true);
+                mCancelExtraScalingPending = true;
             }
-            default: throw new IllegalArgumentException(String.valueOf(direction));
-        }
-    }
-
-    private class MyDownUpListener implements DownUpDetector.DownUpListener {
-        public void onDown(MotionEvent e) {
         }
 
-        public void onUp(MotionEvent e) {
+        private void stopExtraScalingIfNeeded() {
+            if (mCancelExtraScalingPending) {
+                mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
+                mPositionController.setExtraScalingRange(false);
+                mCancelExtraScalingPending = false;
+            }
+        }
+
+        @Override
+        public void onDown() {
+            if (mIgnoreSwipingGesture) return;
+
+            mHolding |= HOLD_TOUCH_DOWN;
+
+            if (mFilmMode && mPositionController.isScrolling()) {
+                mDownInScrolling = true;
+                mPositionController.stopScrolling();
+            } else {
+                mDownInScrolling = false;
+            }
+        }
+
+        @Override
+        public void onUp() {
+            if (mIgnoreSwipingGesture) return;
+
+            mHolding &= ~HOLD_TOUCH_DOWN;
             mEdgeView.onRelease();
 
             if (mIgnoreUpEvent) {
                 mIgnoreUpEvent = false;
                 return;
             }
-            if (!snapToNeighborImage() && mTransitionMode == TRANS_NONE) {
-                mPositionController.up();
+
+            snapback();
+        }
+
+        public void setSwipingEnabled(boolean enabled) {
+            mIgnoreSwipingGesture = !enabled;
+        }
+    }
+
+    public void setSwipingEnabled(boolean enabled) {
+        mGestureListener.setSwipingEnabled(enabled);
+    }
+
+    private void setFilmMode(boolean enabled) {
+        if (mFilmMode == enabled) return;
+        mFilmMode = enabled;
+        mPositionController.setFilmMode(mFilmMode);
+        mModel.setNeedFullImage(!enabled);
+        mListener.onActionBarAllowed(!enabled);
+
+        // Move into camera in page mode, lock
+        if (!enabled && mPictures.get(0).isCamera()) {
+            mListener.lockOrientation();
+        }
+    }
+
+    public boolean getFilmMode() {
+        return mFilmMode;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Framework events
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void pause() {
+        mPositionController.skipAnimation();
+        mTileView.freeTextures();
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            mPictures.get(i).setScreenNail(null);
+        }
+    }
+
+    public void resume() {
+        mTileView.prepareTextures();
+    }
+
+    // move to the camera preview and show controls after resume
+    public void resetToFirstPicture() {
+        mModel.moveTo(0);
+        setFilmMode(false);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Rendering
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        // Check if the camera preview occupies the full screen.
+        boolean full = !mFilmMode && mPictures.get(0).isCamera()
+                && mPositionController.isCenter()
+                && mPositionController.isAtMinimalScale();
+        if (full != mFullScreenCamera) {
+            mFullScreenCamera = full;
+            mListener.onFullScreenChanged(full);
+        }
+
+        // Determine how many photos we need to draw in addition to the center
+        // one.
+        int neighbors;
+        if (mFullScreenCamera) {
+            neighbors = 0;
+        } else {
+            // In page mode, we draw only one previous/next photo. But if we are
+            // doing capture animation, we want to draw all photos.
+            boolean inPageMode = (mPositionController.getFilmRatio() == 0f);
+            boolean inCaptureAnimation =
+                    ((mHolding & HOLD_CAPTURE_ANIMATION) != 0);
+            if (inPageMode && !inCaptureAnimation) {
+                neighbors = 1;
+            } else {
+                neighbors = SCREEN_NAIL_MAX;
+            }
+        }
+
+        // Draw photos from back to front
+        for (int i = neighbors; i >= -neighbors; i--) {
+            Rect r = mPositionController.getPosition(i);
+            mPictures.get(i).draw(canvas, r);
+        }
+
+        mPositionController.advanceAnimation();
+        checkFocusSwitching();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Film mode focus switching
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Runs in GL thread.
+    private void checkFocusSwitching() {
+        if (!mFilmMode) return;
+        if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return;
+        if (switchPosition() != 0) {
+            mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS);
+        }
+    }
+
+    // Runs in main thread.
+    private void switchFocus() {
+        if (mHolding != 0) return;
+        switch (switchPosition()) {
+            case -1:
+                switchToPrevImage();
+                break;
+            case 1:
+                switchToNextImage();
+                break;
+        }
+    }
+
+    // Returns -1 if we should switch focus to the previous picture, +1 if we
+    // should switch to the next, 0 otherwise.
+    private int switchPosition() {
+        Rect curr = mPositionController.getPosition(0);
+        int center = getWidth() / 2;
+
+        if (curr.left > center && mPrevBound < 0) {
+            Rect prev = mPositionController.getPosition(-1);
+            int currDist = curr.left - center;
+            int prevDist = center - prev.right;
+            if (prevDist < currDist) {
+                return -1;
+            }
+        } else if (curr.right < center && mNextBound > 0) {
+            Rect next = mPositionController.getPosition(1);
+            int currDist = center - curr.right;
+            int nextDist = next.left - center;
+            if (nextDist < currDist) {
+                return 1;
+            }
+        }
+
+        return 0;
+    }
+
+    // Switch to the previous or next picture if the hit position is inside
+    // one of their boxes. This runs in main thread.
+    private void switchToHitPicture(int x, int y) {
+        if (mPrevBound < 0) {
+            Rect r = mPositionController.getPosition(-1);
+            if (r.right >= x) {
+                slideToPrevPicture();
+                return;
+            }
+        }
+
+        if (mNextBound > 0) {
+            Rect r = mPositionController.getPosition(1);
+            if (r.left <= x) {
+                slideToNextPicture();
+                return;
             }
         }
     }
 
+    ////////////////////////////////////////////////////////////////////////////
+    //  Page mode focus switching
+    //
+    //  We slide image to the next one or the previous one in two cases: 1: If
+    //  the user did a fling gesture with enough velocity.  2 If the user has
+    //  moved the picture a lot.
+    ////////////////////////////////////////////////////////////////////////////
+
+    private boolean swipeImages(float velocityX, float velocityY) {
+        if (mFilmMode) return false;
+
+        // Avoid swiping images if we're possibly flinging to view the
+        // zoomed in picture vertically.
+        PositionController controller = mPositionController;
+        boolean isMinimal = controller.isAtMinimalScale();
+        int edges = controller.getImageAtEdges();
+        if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
+            if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
+                    || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
+                return false;
+
+        // If we are at the edge of the current photo and the sweeping velocity
+        // exceeds the threshold, slide to the next / previous image.
+        if (velocityX < -SWIPE_THRESHOLD && (isMinimal
+                || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
+            return slideToNextPicture();
+        } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
+                || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
+            return slideToPrevPicture();
+        }
+
+        return false;
+    }
+
+    private void snapback() {
+        if (mHolding != 0) return;
+        if (!snapToNeighborImage()) {
+            mPositionController.snapback();
+        }
+    }
+
+    private boolean snapToNeighborImage() {
+        if (mFilmMode) return false;
+
+        Rect r = mPositionController.getPosition(0);
+        int viewW = getWidth();
+        int threshold = MOVE_THRESHOLD + gapToSide(r.width(), viewW);
+
+        // If we have moved the picture a lot, switching.
+        if (viewW - r.right > threshold) {
+            return slideToNextPicture();
+        } else if (r.left > threshold) {
+            return slideToPrevPicture();
+        }
+
+        return false;
+    }
+
+    private boolean slideToNextPicture() {
+        if (mNextBound <= 0) return false;
+        switchToNextImage();
+        mPositionController.startHorizontalSlide();
+        return true;
+    }
+
+    private boolean slideToPrevPicture() {
+        if (mPrevBound >= 0) return false;
+        switchToPrevImage();
+        mPositionController.startHorizontalSlide();
+        return true;
+    }
+
+    private static int gapToSide(int imageWidth, int viewWidth) {
+        return Math.max(0, (viewWidth - imageWidth) / 2);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Focus switching
+    ////////////////////////////////////////////////////////////////////////////
+
     private void switchToNextImage() {
-        // We update the texture here directly to prevent texture uploading.
-        ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
-        ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
-        mTileView.invalidateTiles();
-        if (prevNail.mTexture != null) prevNail.mTexture.recycle();
-        prevNail.mTexture = mTileView.mBackupImage;
-        mTileView.mBackupImage = nextNail.mTexture;
-        nextNail.mTexture = null;
-        mModel.next();
+        mModel.moveTo(mModel.getCurrentIndex() + 1);
     }
 
-    private void switchToPreviousImage() {
-        // We update the texture here directly to prevent texture uploading.
-        ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
-        ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
-        mTileView.invalidateTiles();
-        if (nextNail.mTexture != null) nextNail.mTexture.recycle();
-        nextNail.mTexture = mTileView.mBackupImage;
-        mTileView.mBackupImage = prevNail.mTexture;
-        nextNail.mTexture = null;
-        mModel.previous();
+    private void switchToPrevImage() {
+        mModel.moveTo(mModel.getCurrentIndex() - 1);
     }
 
-    public void notifyTransitionComplete() {
-        mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE);
+    private void switchToFirstImage() {
+        mModel.moveTo(0);
     }
 
-    private void onTransitionComplete() {
-        int mode = mTransitionMode;
-        mTransitionMode = TRANS_NONE;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Opening Animation
+    ////////////////////////////////////////////////////////////////////////////
 
-        if (mModel == null) return;
-        if (mode == TRANS_SWITCH_NEXT) {
+    public void setOpenAnimationRect(Rect rect) {
+        mPositionController.setOpenAnimationRect(rect);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Capture Animation
+    ////////////////////////////////////////////////////////////////////////////
+
+    public boolean switchWithCaptureAnimation(int offset) {
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            return switchWithCaptureAnimationLocked(offset);
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    private boolean switchWithCaptureAnimationLocked(int offset) {
+        if (mHolding != 0) return true;
+        if (offset == 1) {
+            if (mNextBound <= 0) return false;
+            // Temporary disable action bar until the capture animation is done.
+            if (!mFilmMode) mListener.onActionBarAllowed(false);
             switchToNextImage();
-        } else if (mode == TRANS_SWITCH_PREVIOUS) {
-            switchToPreviousImage();
-        }
-    }
+            mPositionController.startCaptureAnimationSlide(-1);
+        } else if (offset == -1) {
+            if (mPrevBound >= 0) return false;
+            if (mFilmMode) setFilmMode(false);
 
-    public boolean isDown() {
-        return mDownUpDetector.isDown();
-    }
-
-    public static interface Model extends TileImageView.Model {
-        public void next();
-        public void previous();
-        public void jumpTo(int index);
-        public int getImageRotation();
-
-        // Return null if the specified image is unavailable.
-        public ImageData getNextImage();
-        public ImageData getPreviousImage();
-    }
-
-    public static class ImageData {
-        public int rotation;
-        public Bitmap bitmap;
-
-        public ImageData(Bitmap bitmap, int rotation) {
-            this.bitmap = bitmap;
-            this.rotation = rotation;
-        }
-    }
-
-    private static int getRotated(int degree, int original, int theother) {
-        return ((degree / 90) & 1) == 0 ? original : theother;
-    }
-
-    private class ScreenNailEntry {
-        private boolean mVisible;
-        private boolean mEnabled;
-
-        private int mRotation;
-        private int mDrawWidth;
-        private int mDrawHeight;
-        private int mOffsetX;
-
-        private BitmapTexture mTexture;
-
-        public void set(boolean enabled, Bitmap bitmap, int rotation) {
-            mEnabled = enabled;
-            mRotation = rotation;
-            if (bitmap == null) {
-                if (mTexture != null) mTexture.recycle();
-                mTexture = null;
-            } else {
-                if (mTexture != null) {
-                    if (mTexture.getBitmap() != bitmap) {
-                        mTexture.recycle();
-                        mTexture = new BitmapTexture(bitmap);
-                    }
-                } else {
-                    mTexture = new BitmapTexture(bitmap);
-                }
-                updateDrawingSize();
+            // If we are too far away from the first image (so that we don't
+            // have all the ScreenNails in-between), we go directly without
+            // animation.
+            if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) {
+                switchToFirstImage();
+                mPositionController.skipToFinalPosition();
+                return true;
             }
+
+            switchToFirstImage();
+            mPositionController.startCaptureAnimationSlide(1);
+        } else {
+            return false;
         }
-
-        public void layoutRightEdgeAt(int x) {
-            mVisible = x > 0;
-            mOffsetX = x - getRotated(
-                    mRotation, mDrawWidth, mDrawHeight) / 2;
-        }
-
-        public void layoutLeftEdgeAt(int x) {
-            mVisible = x < getWidth();
-            mOffsetX = x + getRotated(
-                    mRotation, mDrawWidth, mDrawHeight) / 2;
-        }
-
-        public int gapToSide() {
-            return ((mRotation / 90) & 1) != 0
-                    ? PhotoView.gapToSide(mDrawHeight, getWidth())
-                    : PhotoView.gapToSide(mDrawWidth, getWidth());
-        }
-
-        public void updateDrawingSize() {
-            if (mTexture == null) return;
-
-            int width = mTexture.getWidth();
-            int height = mTexture.getHeight();
-
-            // Calculate the initial scale that will used by PositionController
-            // (usually fit-to-screen)
-            float s = ((mRotation / 90) & 0x01) == 0
-                    ? mPositionController.getMinimalScale(width, height)
-                    : mPositionController.getMinimalScale(height, width);
-
-            mDrawWidth = Math.round(width * s);
-            mDrawHeight = Math.round(height * s);
-        }
-
-        public boolean isEnabled() {
-            return mEnabled;
-        }
-
-        public void draw(GLCanvas canvas, boolean applyFadingAnimation) {
-            if (mTexture == null) return;
-
-            int w = getWidth();
-            int x = applyFadingAnimation ? w / 2 : mOffsetX;
-            int y = getHeight() / 2;
-            int flags = GLCanvas.SAVE_FLAG_MATRIX;
-
-            if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA;
-            canvas.save(flags);
-            canvas.translate(x, y, 0);
-            if (applyFadingAnimation) {
-                float progress = (float) (x - mOffsetX) / w;
-                float alpha = getScrollAlpha(progress);
-                float scale = getScrollScale(progress);
-                canvas.multiplyAlpha(alpha);
-                canvas.scale(scale, scale, 1);
-            }
-            if (mRotation != 0) {
-                canvas.rotate(mRotation, 0, 0, 1);
-            }
-            canvas.translate(-x, -y, 0);
-            mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
-                    mDrawWidth, mDrawHeight);
-            canvas.restore();
-        }
+        mHolding |= HOLD_CAPTURE_ANIMATION;
+        Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
+        mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
+        return true;
     }
 
+    private void captureAnimationDone(int offset) {
+        mHolding &= ~HOLD_CAPTURE_ANIMATION;
+        if (offset == 1 && !mFilmMode) {
+            // Now the capture animation is done, enable the action bar.
+            mListener.onActionBarAllowed(true);
+            mListener.onActionBarWanted();
+        }
+        snapback();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Card deck effect calculation
+    ////////////////////////////////////////////////////////////////////////////
+
     // Returns the scrolling progress value for an object moving out of a
     // view. The progress value measures how much the object has moving out of
     // the view. The object currently displays in [left, right), and the view is
@@ -859,10 +1249,15 @@
         // If the object width is smaller than the view width,
         //      |....view....|
         //                   |<-->|      progress = -1 when left = viewWidth
+        //          |<-->|               progress = 0 when left = viewWidth / 2 - w / 2
         // |<-->|                        progress = 1 when left = -w
-        // So progress = 1 - 2 * (left + w) / (viewWidth + w)
         if (w < viewWidth) {
-            return 1f - 2f * (left + w) / (viewWidth + w);
+            int zx = viewWidth / 2 - w / 2;
+            if (left > zx) {
+                return -(left - zx) / (float) (viewWidth - zx);  // progress = (0, -1]
+            } else {
+                return (left - zx) / (float) (-w - zx);  // progress = [0, 1]
+            }
         }
 
         // If the object width is larger than the view width,
@@ -917,44 +1312,49 @@
         }
     }
 
-    public void pause() {
-        mPositionController.skipAnimation();
-        mTransitionMode = TRANS_NONE;
-        mTileView.freeTextures();
-        for (ScreenNailEntry entry : mScreenNails) {
-            entry.set(false, null, 0);
+    // Returns an interpolated value for the page/film transition.
+    // When ratio = 0, the result is from.
+    // When ratio = 1, the result is to.
+    private static float interpolate(float ratio, float from, float to) {
+        return from + (to - from) * ratio * ratio;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Simple public utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public Rect getPhotoRect(int index) {
+        return mPositionController.getPosition(index);
+    }
+
+    public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
+        Rect location = new Rect();
+        Utils.assertTrue(root.getBoundsOf(this, location));
+
+        Rect fullRect = bounds();
+        PhotoFallbackEffect effect = new PhotoFallbackEffect();
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            MediaItem item = mModel.getMediaItem(i);
+            if (item == null) continue;
+            ScreenNail sc = mModel.getScreenNail(i);
+            if (!(sc instanceof BitmapScreenNail)
+                    || ((BitmapScreenNail) sc).isShowingPlaceholder()) continue;
+
+            // Now, sc is BitmapScreenNail and is not showing placeholder
+            Rect rect = new Rect(getPhotoRect(i));
+            if (!Rect.intersects(fullRect, rect)) continue;
+            rect.offset(location.left, location.top);
+
+            RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true);
+            canvas.beginRenderTarget(texture);
+            sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight());
+            canvas.endRenderTarget();
+            effect.addEntry(item.getPath(), rect, texture);
         }
-    }
-
-    public void resume() {
-        mTileView.prepareTextures();
-    }
-
-    public void setOpenedItem(Path itemPath) {
-        mOpenedItemPath = itemPath;
-    }
-
-    public void showVideoPlayIcon(boolean show) {
-        mShowVideoPlayIcon = show;
-    }
-
-    // Returns the position saved by the previous page.
-    public Position retrieveSavedPosition() {
-        if (mOpenedItemPath != null) {
-            Position position = PositionRepository
-                    .getInstance(mActivity).get(Long.valueOf(
-                    System.identityHashCode(mOpenedItemPath)));
-            mOpenedItemPath = null;
-            return position;
-        }
-        return null;
-    }
-
-    public void openAnimationStarted() {
-        mTransitionMode = TRANS_OPEN_ANIMATION;
-    }
-
-    public boolean isInTransition() {
-        return mTransitionMode != TRANS_NONE;
+        return effect;
     }
 }
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index 2068446..65334d5 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -16,353 +16,555 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.PositionRepository.Position;
-import com.android.gallery3d.util.GalleryUtils;
-
 import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.RectF;
-import android.os.Message;
-import android.os.SystemClock;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.widget.Scroller;
+import android.graphics.Rect;
+import android.util.Log;
+import android.widget.OverScroller;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.RangeIntArray;
 
 class PositionController {
     private static final String TAG = "PositionController";
-    private long mAnimationStartTime = NO_ANIMATION;
+
+    public static final int IMAGE_AT_LEFT_EDGE = 1;
+    public static final int IMAGE_AT_RIGHT_EDGE = 2;
+    public static final int IMAGE_AT_TOP_EDGE = 4;
+    public static final int IMAGE_AT_BOTTOM_EDGE = 8;
+
+    public static final int CAPTURE_ANIMATION_TIME = 700;
+
+    // Special values for animation time.
     private static final long NO_ANIMATION = -1;
     private static final long LAST_ANIMATION = -2;
 
-    private int mAnimationKind;
-    private float mAnimationDuration;
-    private final static int ANIM_KIND_SCROLL = 0;
-    private final static int ANIM_KIND_SCALE = 1;
-    private final static int ANIM_KIND_SNAPBACK = 2;
-    private final static int ANIM_KIND_SLIDE = 3;
-    private final static int ANIM_KIND_ZOOM = 4;
-    private final static int ANIM_KIND_FLING = 5;
+    private static final int ANIM_KIND_SCROLL = 0;
+    private static final int ANIM_KIND_SCALE = 1;
+    private static final int ANIM_KIND_SNAPBACK = 2;
+    private static final int ANIM_KIND_SLIDE = 3;
+    private static final int ANIM_KIND_ZOOM = 4;
+    private static final int ANIM_KIND_OPENING = 5;
+    private static final int ANIM_KIND_FLING = 6;
+    private static final int ANIM_KIND_CAPTURE = 7;
 
     // Animation time in milliseconds. The order must match ANIM_KIND_* above.
-    private final static int ANIM_TIME[] = {
+    private static final int ANIM_TIME[] = {
         0,    // ANIM_KIND_SCROLL
         50,   // ANIM_KIND_SCALE
         600,  // ANIM_KIND_SNAPBACK
         400,  // ANIM_KIND_SLIDE
         300,  // ANIM_KIND_ZOOM
+        400,  // ANIM_KIND_OPENING
         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
+        CAPTURE_ANIMATION_TIME,  // ANIM_KIND_CAPTURE
     };
 
     // We try to scale up the image to fill the screen. But in order not to
     // scale too much for small icons, we limit the max up-scaling factor here.
     private static final float SCALE_LIMIT = 4;
-    private static final int sHorizontalSlack = GalleryUtils.dpToPixel(12);
 
-    private static final float SCALE_MIN_EXTRA = 0.6f;
+    // For user's gestures, we give a temporary extra scaling range which goes
+    // above or below the usual scaling limits.
+    private static final float SCALE_MIN_EXTRA = 0.7f;
     private static final float SCALE_MAX_EXTRA = 1.4f;
 
-    private PhotoView mViewer;
-    private EdgeView mEdgeView;
-    private int mImageW, mImageH;
-    private int mViewW, mViewH;
-
-    // The X, Y are the coordinate on bitmap which shows on the center of
-    // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
-    // values used currently.
-    private int mCurrentX, mFromX, mToX;
-    private int mCurrentY, mFromY, mToY;
-    private float mCurrentScale, mFromScale, mToScale;
-
-    // The focus point of the scaling gesture (in bitmap coordinates).
-    private int mFocusBitmapX;
-    private int mFocusBitmapY;
-    private boolean mInScale;
-
-    // The minimum and maximum scale we allow.
-    private float mScaleMin, mScaleMax = SCALE_LIMIT;
+    // Setting this true makes the extra scaling range permanent (until this is
+    // set to false again).
     private boolean mExtraScalingRange = false;
 
-    // This is used by the fling animation
-    private FlingScroller mScroller;
+    // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
+    private boolean mFilmMode = false;
 
-    // The bound of the stable region, see the comments above
-    // calculateStableBound() for details.
+    // These are the limits for width / height of the picture in film mode.
+    private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
+    private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
+    private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
+    private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
+
+    // In addition to the focused box (index == 0). We also keep information
+    // about this many boxes on each side.
+    private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
+
+    private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
+    private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
+
+    private Listener mListener;
+    private volatile Rect mOpenAnimationRect;
+
+    // Use a large enough value, so we won't see the gray shadown in the beginning.
+    private int mViewW = 1200;
+    private int mViewH = 1200;
+
+    // A scaling guesture is in progress.
+    private boolean mInScale;
+    // The focus point of the scaling gesture, relative to the center of the
+    // picture in bitmap pixels.
+    private float mFocusX, mFocusY;
+
+    // whether there is a previous/next picture.
+    private boolean mHasPrev, mHasNext;
+
+    // This is used by the fling animation (page mode).
+    private FlingScroller mPageScroller;
+
+    // This is used by the fling animation (film mode).
+    private OverScroller mFilmScroller;
+
+    // The bound of the stable region that the focused box can stay, see the
+    // comments above calculateStableBound() for details.
     private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
 
-    // Assume the image size is the same as view size before we know the actual
-    // size of image.
-    private boolean mUseViewSize = true;
+    // Constrained frame is a rectangle that the focused box should fit into if
+    // it is constrained. It has two effects:
+    //
+    // (1) In page mode, if the focused box is constrained, scaling for the
+    // focused box is adjusted to fit into the constrained frame, instead of the
+    // whole view.
+    //
+    // (2) In page mode, if the focused box is constrained, the mPlatform's
+    // default center (mDefaultX/Y) is moved to the center of the constrained
+    // frame, instead of the view center.
+    //
+    private Rect mConstrainedFrame = new Rect();
 
-    private RectF mTempRect = new RectF();
-    private float[] mTempPoints = new float[8];
+    // Whether the focused box is constrained.
+    //
+    // Our current program's first call to moveBox() sets constrained = true, so
+    // we set the initial value of this variable to true, and we will not see
+    // see unwanted transition animation.
+    private boolean mConstrained = true;
 
-    public PositionController(PhotoView viewer, Context context,
-            EdgeView edgeView) {
-        mViewer = viewer;
-        mEdgeView = edgeView;
-        mScroller = new FlingScroller();
+    //
+    //  ___________________________________________________________
+    // |   _____       _____       _____       _____       _____   |
+    // |  |     |     |     |     |     |     |     |     |     |  |
+    // |  | Box |     | Box |     | Box*|     | Box |     | Box |  |
+    // |  |_____|.....|_____|.....|_____|.....|_____|.....|_____|  |
+    // |          Gap         Gap         Gap         Gap          |
+    // |___________________________________________________________|
+    //
+    //                       <--  Platform  -->
+    //
+    // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY)
+
+    private Platform mPlatform = new Platform();
+    private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+    // The gap at the right of a Box i is at index i. The gap at the left of a
+    // Box i is at index i - 1.
+    private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+    private FilmRatio mFilmRatio = new FilmRatio();
+
+    // These are only used during moveBox().
+    private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+    private RangeArray<Gap> mTempGaps =
+        new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+
+    // The output of the PositionController. Available throught getPosition().
+    private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
+
+    public interface Listener {
+        void invalidate();
+        boolean isHolding();
+
+        // EdgeView
+        void onPull(int offset, int direction);
+        void onRelease();
+        void onAbsorb(int velocity, int direction);
     }
 
-    public void setImageSize(int width, int height) {
+    public PositionController(Context context, Listener listener) {
+        mListener = listener;
+        mPageScroller = new FlingScroller();
+        mFilmScroller = new OverScroller(context,
+                null /* default interpolator */, false /* no flywheel */);
 
-        // If no image available, use view size.
-        if (width == 0 || height == 0) {
-            mUseViewSize = true;
-            mImageW = mViewW;
-            mImageH = mViewH;
-            mCurrentX = mImageW / 2;
-            mCurrentY = mImageH / 2;
-            mCurrentScale = 1;
-            mScaleMin = 1;
-            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
-            return;
+        // Initialize the areas.
+        initPlatform();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mBoxes.put(i, new Box());
+            initBox(i);
+            mRects.put(i, new Rect());
         }
-
-        mUseViewSize = false;
-
-        float ratio = Math.min(
-                (float) mImageW / width, (float) mImageH / height);
-
-        // See the comment above translate() for details.
-        mCurrentX = translate(mCurrentX, mImageW, width, ratio);
-        mCurrentY = translate(mCurrentY, mImageH, height, ratio);
-        mCurrentScale = mCurrentScale * ratio;
-
-        mFromX = translate(mFromX, mImageW, width, ratio);
-        mFromY = translate(mFromY, mImageH, height, ratio);
-        mFromScale = mFromScale * ratio;
-
-        mToX = translate(mToX, mImageW, width, ratio);
-        mToY = translate(mToY, mImageH, height, ratio);
-        mToScale = mToScale * ratio;
-
-        mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
-        mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
-
-        mImageW = width;
-        mImageH = height;
-
-        mScaleMin = getMinimalScale(mImageW, mImageH);
-
-        // Start animation from the saved position if we have one.
-        Position position = mViewer.retrieveSavedPosition();
-        if (position != null) {
-            // The animation starts from 240 pixels and centers at the image
-            // at the saved position.
-            float scale = 240f / Math.min(width, height);
-            mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
-            mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
-            mCurrentScale = scale;
-            mViewer.openAnimationStarted();
-            startSnapback();
-        } else if (mAnimationStartTime == NO_ANIMATION) {
-            mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mGaps.put(i, new Gap());
+            initGap(i);
         }
-        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
     }
 
+    public void setOpenAnimationRect(Rect r) {
+        mOpenAnimationRect = r;
+    }
+
+    public void setViewSize(int viewW, int viewH) {
+        if (viewW == mViewW && viewH == mViewH) return;
+
+        boolean wasMinimal = isAtMinimalScale();
+
+        mViewW = viewW;
+        mViewH = viewH;
+        initPlatform();
+
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            setBoxSize(i, viewW, viewH, true);
+        }
+
+        updateScaleAndGapLimit();
+
+        // If the focused box was at minimal scale, we try to make it the
+        // minimal scale under the new view size.
+        if (wasMinimal) {
+            Box b = mBoxes.get(0);
+            b.mCurrentScale = b.mScaleMin;
+        }
+
+        // If we have the opening animation, do it. Otherwise go directly to the
+        // right position.
+        if (!startOpeningAnimationIfNeeded()) {
+            skipToFinalPosition();
+        }
+    }
+
+    public void setConstrainedFrame(Rect cFrame) {
+        if (mConstrainedFrame.equals(cFrame)) return;
+        mConstrainedFrame.set(cFrame);
+        mPlatform.updateDefaultXY();
+        updateScaleAndGapLimit();
+        snapAndRedraw();
+    }
+
+    public void forceImageSize(int index, int width, int height) {
+        if (width == 0 || height == 0) return;
+        Box b = mBoxes.get(index);
+        b.mImageW = width;
+        b.mImageH = height;
+        return;
+    }
+
+    public void setImageSize(int index, int width, int height, Rect cFrame) {
+        if (width == 0 || height == 0) return;
+
+        boolean needUpdate = false;
+        if (cFrame != null && !mConstrainedFrame.equals(cFrame)) {
+            mConstrainedFrame.set(cFrame);
+            mPlatform.updateDefaultXY();
+            needUpdate = true;
+        }
+        needUpdate |= setBoxSize(index, width, height, false);
+
+        if (!needUpdate) return;
+        updateScaleAndGapLimit();
+        snapAndRedraw();
+    }
+
+    // Returns false if the box size doesn't change.
+    private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
+        Box b = mBoxes.get(i);
+        boolean wasViewSize = b.mUseViewSize;
+
+        // If we already have an image size, we don't want to use the view size.
+        if (!wasViewSize && isViewSize) return false;
+
+        b.mUseViewSize = isViewSize;
+
+        if (width == b.mImageW && height == b.mImageH) {
+            return false;
+        }
+
+        // The ratio of the old size and the new size.
+        //
+        // If the aspect ratio changes, we don't know if it is because one side
+        // grows or the other side shrinks. Currently we just assume the view
+        // angle of the longer side doesn't change (so the aspect ratio change
+        // is because the view angle of the shorter side changes). This matches
+        // what camera preview does.
+        float ratio = (width > height)
+                ? (float) b.mImageW / width
+                : (float) b.mImageH / height;
+
+        b.mImageW = width;
+        b.mImageH = height;
+
+        // If this is the first time we receive an image size, we change the
+        // scale directly. Otherwise adjust the scales by a ratio, and snapback
+        // will animate the scale into the min/max bounds if necessary.
+        if (wasViewSize && !isViewSize) {
+            b.mCurrentScale = getMinimalScale(b);
+            b.mAnimationStartTime = NO_ANIMATION;
+        } else {
+            b.mCurrentScale *= ratio;
+            b.mFromScale *= ratio;
+            b.mToScale *= ratio;
+        }
+
+        if (i == 0) {
+            mFocusX /= ratio;
+            mFocusY /= ratio;
+        }
+
+        return true;
+    }
+
+    private boolean startOpeningAnimationIfNeeded() {
+        if (mOpenAnimationRect == null) return false;
+        Box b = mBoxes.get(0);
+        if (b.mUseViewSize) return false;
+
+        // Start animation from the saved rectangle if we have one.
+        Rect r = mOpenAnimationRect;
+        mOpenAnimationRect = null;
+
+        mPlatform.mCurrentX = r.centerX() - mViewW / 2;
+        b.mCurrentY = r.centerY() - mViewH / 2;
+        b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
+                r.height() / (float) b.mImageH);
+        startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin,
+                ANIM_KIND_OPENING);
+
+        // Animate from large gaps for neighbor boxes to avoid them
+        // shown on the screen during opening animation.
+        for (int i = -1; i < 1; i++) {
+            Gap g = mGaps.get(i);
+            g.mCurrentGap = mViewW;
+            g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING);
+        }
+
+        return true;
+    }
+
+    public void setFilmMode(boolean enabled) {
+        if (enabled == mFilmMode) return;
+        mFilmMode = enabled;
+
+        mPlatform.updateDefaultXY();
+        updateScaleAndGapLimit();
+        stopAnimation();
+        snapAndRedraw();
+    }
+
+    public void setExtraScalingRange(boolean enabled) {
+        if (mExtraScalingRange == enabled) return;
+        mExtraScalingRange = enabled;
+        if (!enabled) {
+            snapAndRedraw();
+        }
+    }
+
+    // This should be called whenever the scale range of boxes or the default
+    // gap size may change. Currently this can happen due to change of view
+    // size, image size, mFilmMode, mConstrained, and mConstrainedFrame.
+    private void updateScaleAndGapLimit() {
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            Box b = mBoxes.get(i);
+            b.mScaleMin = getMinimalScale(b);
+            b.mScaleMax = getMaximalScale(b);
+        }
+
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            Gap g = mGaps.get(i);
+            g.mDefaultSize = getDefaultGapSize(i);
+        }
+    }
+
+    // Returns the default gap size according the the size of the boxes around
+    // the gap and the current mode.
+    private int getDefaultGapSize(int i) {
+        if (mFilmMode) return IMAGE_GAP;
+        Box a = mBoxes.get(i);
+        Box b = mBoxes.get(i + 1);
+        return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
+    }
+
+    // Here is how we layout the boxes in the page mode.
+    //
+    //   previous             current             next
+    //  ___________       ________________     __________
+    // |  _______  |     |   __________   |   |  ______  |
+    // | |       | |     |  |   right->|  |   | |      | |
+    // | |       |<-------->|<--left   |  |   | |      | |
+    // | |_______| |  |  |  |__________|  |   | |______| |
+    // |___________|  |  |________________|   |__________|
+    //                |  <--> gapToSide()
+    //                |
+    // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
+    private int gapToSide(Box b) {
+        return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
+    }
+
+    // Stop all animations at where they are now.
+    public void stopAnimation() {
+        mPlatform.mAnimationStartTime = NO_ANIMATION;
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
+        }
+    }
+
+    public void skipAnimation() {
+        if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
+            mPlatform.mCurrentX = mPlatform.mToX;
+            mPlatform.mCurrentY = mPlatform.mToY;
+            mPlatform.mAnimationStartTime = NO_ANIMATION;
+        }
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            Box b = mBoxes.get(i);
+            if (b.mAnimationStartTime == NO_ANIMATION) continue;
+            b.mCurrentY = b.mToY;
+            b.mCurrentScale = b.mToScale;
+            b.mAnimationStartTime = NO_ANIMATION;
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            Gap g = mGaps.get(i);
+            if (g.mAnimationStartTime == NO_ANIMATION) continue;
+            g.mCurrentGap = g.mToGap;
+            g.mAnimationStartTime = NO_ANIMATION;
+        }
+        redraw();
+    }
+
+    public void snapback() {
+        snapAndRedraw();
+    }
+
+    public void skipToFinalPosition() {
+        stopAnimation();
+        snapAndRedraw();
+        skipAnimation();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Start an animations for the focused box
+    ////////////////////////////////////////////////////////////////////////////
+
     public void zoomIn(float tapX, float tapY, float targetScale) {
-        if (targetScale > mScaleMax) targetScale = mScaleMax;
+        tapX -= mViewW / 2;
+        tapY -= mViewH / 2;
+        Box b = mBoxes.get(0);
 
-        // Convert the tap position to image coordinate
-        int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
-        int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
+        // Convert the tap position to distance to center in bitmap coordinates
+        float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
+        float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
+
+        int x = (int) (-tempX * targetScale + 0.5f);
+        int y = (int) (-tempY * targetScale + 0.5f);
 
         calculateStableBound(targetScale);
-        int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
-        int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
+        int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
+        int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
+        targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
 
         startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
     }
 
     public void resetToFullView() {
-        startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
-    }
-
-    public float getMinimalScale(int w, int h) {
-        return Math.min(SCALE_LIMIT,
-                Math.min((float) mViewW / w, (float) mViewH / h));
-    }
-
-    // Translate a coordinate on bitmap if the bitmap size changes.
-    // If the aspect ratio doesn't change, it's easy:
-    //
-    //         r  = w / w' (= h / h')
-    //         x' = x / r
-    //         y' = y / r
-    //
-    // However the aspect ratio may change. That happens when the user slides
-    // a image before it's loaded, we don't know the actual aspect ratio, so
-    // we will assume one. When we receive the actual bitmap size, we need to
-    // translate the coordinate from the old bitmap into the new bitmap.
-    //
-    // What we want to do is center the bitmap at the original position.
-    //
-    //         ...+--+...
-    //         .  |  |  .
-    //         .  |  |  .
-    //         ...+--+...
-    //
-    // First we scale down the new bitmap by a factor r = min(w/w', h/h').
-    // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
-    // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
-    // the old bitmap maps to (x', y') in the new bitmap, where
-    //         x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
-    //         y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
-    private static int translate(int value, int size, int newSize, float ratio) {
-        return Math.round(newSize / 2f + (value - size / 2f) / ratio);
-    }
-
-    public void setViewSize(int viewW, int viewH) {
-        boolean needLayout = mViewW == 0 || mViewH == 0;
-
-        mViewW = viewW;
-        mViewH = viewH;
-
-        if (mUseViewSize) {
-            mImageW = viewW;
-            mImageH = viewH;
-            mCurrentX = mImageW / 2;
-            mCurrentY = mImageH / 2;
-            mCurrentScale = 1;
-            mScaleMin = 1;
-            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
-            return;
-        }
-
-        // In most cases we want to keep the scaling factor intact when the
-        // view size changes. The cases we want to reset the scaling factor
-        // (to fit the view if possible) are (1) the scaling factor is too
-        // small for the new view size (2) the scaling factor has not been
-        // changed by the user.
-        boolean wasMinScale = (mCurrentScale == mScaleMin);
-        mScaleMin = getMinimalScale(mImageW, mImageH);
-
-        if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
-            mCurrentX = mImageW / 2;
-            mCurrentY = mImageH / 2;
-            mCurrentScale = mScaleMin;
-            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
-        }
-    }
-
-    public void stopAnimation() {
-        mAnimationStartTime = NO_ANIMATION;
-    }
-
-    public void skipAnimation() {
-        if (mAnimationStartTime == NO_ANIMATION) return;
-        mAnimationStartTime = NO_ANIMATION;
-        mCurrentX = mToX;
-        mCurrentY = mToY;
-        mCurrentScale = mToScale;
+        Box b = mBoxes.get(0);
+        startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM);
     }
 
     public void beginScale(float focusX, float focusY) {
+        focusX -= mViewW / 2;
+        focusY -= mViewH / 2;
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
         mInScale = true;
-        mFocusBitmapX = Math.round(mCurrentX +
-                (focusX - mViewW / 2f) / mCurrentScale);
-        mFocusBitmapY = Math.round(mCurrentY +
-                (focusY - mViewH / 2f) / mCurrentScale);
+        mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
+        mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
     }
 
-    // Returns true if the result scale is outside the stable range.
-    public boolean scaleBy(float s, float focusX, float focusY) {
+    // Scales the image by the given factor.
+    // Returns an out-of-range indicator:
+    //   1 if the intended scale is too large for the stable range.
+    //   0 if the intended scale is in the stable range.
+    //  -1 if the intended scale is too small for the stable range.
+    public int scaleBy(float s, float focusX, float focusY) {
+        focusX -= mViewW / 2;
+        focusY -= mViewH / 2;
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
 
-        // We want to keep the focus point (on the bitmap) the same as when
-        // we begin the scale guesture, that is,
+        // We want to keep the focus point (on the bitmap) the same as when we
+        // begin the scale guesture, that is,
         //
-        // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
+        // (focusX' - currentX') / scale' = (focusX - currentX) / scale
         //
-        s *= getTargetScale();
-        int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
-        int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
-
+        s = b.clampScale(s * getTargetScale(b));
+        int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
+        int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
         startAnimation(x, y, s, ANIM_KIND_SCALE);
-        return (s < mScaleMin || s > mScaleMax);
+        if (s < b.mScaleMin) return -1;
+        if (s > b.mScaleMax) return 1;
+        return 0;
     }
 
     public void endScale() {
         mInScale = false;
-        startSnapbackIfNeeded();
+        snapAndRedraw();
     }
 
-    public void setExtraScalingRange(boolean enabled) {
-        mExtraScalingRange = enabled;
-        if (!enabled) {
-            startSnapbackIfNeeded();
+    // Slide the focused box to the center of the view.
+    public void startHorizontalSlide() {
+        Box b = mBoxes.get(0);
+        startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE);
+    }
+
+    // Slide the focused box to the center of the view with the capture
+    // animation. In addition to the sliding, the animation will also scale the
+    // the focused box, the specified neighbor box, and the gap between the
+    // two. The specified offset should be 1 or -1.
+    public void startCaptureAnimationSlide(int offset) {
+        Box b = mBoxes.get(0);
+        Box n = mBoxes.get(offset);  // the neighbor box
+        Gap g = mGaps.get(offset);  // the gap between the two boxes
+
+        mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY,
+                ANIM_KIND_CAPTURE);
+        b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE);
+        n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE);
+        g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
+        redraw();
+    }
+
+    public void startScroll(float dx, float dy) {
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
+        // Only allow scrolling when we are not currently in an animation or we
+        // are in some animation with can be interrupted.
+        if (b.mAnimationStartTime != NO_ANIMATION) {
+            switch (b.mAnimationKind) {
+                case ANIM_KIND_SCROLL:
+                case ANIM_KIND_FLING:
+                    break;
+                default:
+                    return;
+            }
+        }
+
+        int x = p.mCurrentX + (int) (dx + 0.5f);
+        int y = b.mCurrentY + (int) (dy + 0.5f);
+
+        if (mFilmMode) {
+            scrollToFilm(x, y);
+        } else {
+            scrollToPage(x, y);
         }
     }
 
-    public float getCurrentScale() {
-        return mCurrentScale;
-    }
+    private void scrollToPage(int x, int y) {
+        Box b = mBoxes.get(0);
 
-    public boolean isAtMinimalScale() {
-        return isAlmostEquals(mCurrentScale, mScaleMin);
-    }
-
-    private static boolean isAlmostEquals(float a, float b) {
-        float diff = a - b;
-        return (diff < 0 ? -diff : diff) < 0.02f;
-    }
-
-    public void up() {
-        startSnapback();
-    }
-
-    //             |<--| (1/2) * mImageW
-    // +-------+-------+-------+
-    // |       |       |       |
-    // |       |   o   |       |
-    // |       |       |       |
-    // +-------+-------+-------+
-    // |<----------| (3/2) * mImageW
-    // Slide in the image from left or right.
-    // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
-    // Sliding from left:  mCurrentX = (1/2) * mImageW
-    //              right: mCurrentX = (3/2) * mImageW
-    public void startSlideInAnimation(int direction) {
-        int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
-                mImageW / 2 : 3 * mImageW / 2;
-        mFromX = Math.round(fromX);
-        mFromY = Math.round(mImageH / 2f);
-        mCurrentX = mFromX;
-        mCurrentY = mFromY;
-        startAnimation(
-                mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
-    }
-
-    public void startHorizontalSlide(int distance) {
-        scrollBy(distance, 0, ANIM_KIND_SLIDE);
-    }
-
-    private void scrollBy(float dx, float dy, int type) {
-        startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
-                getTargetY() + Math.round(dy / mCurrentScale),
-                mCurrentScale, type);
-    }
-
-    public void startScroll(float dx, float dy, boolean hasNext,
-            boolean hasPrev) {
-        int x = getTargetX() + Math.round(dx / mCurrentScale);
-        int y = getTargetY() + Math.round(dy / mCurrentScale);
-
-        calculateStableBound(mCurrentScale);
+        calculateStableBound(b.mCurrentScale);
 
         // Vertical direction: If we have space to move in the vertical
         // direction, we show the edge effect when scrolling reaches the edge.
         if (mBoundTop != mBoundBottom) {
             if (y < mBoundTop) {
-                mEdgeView.onPull(mBoundTop - y, EdgeView.TOP);
+                mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
             } else if (y > mBoundBottom) {
-                mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
+                mListener.onPull(y - mBoundBottom, EdgeView.TOP);
             }
         }
 
@@ -370,216 +572,587 @@
 
         // Horizontal direction: we show the edge effect when the scrolling
         // tries to go left of the first image or go right of the last image.
-        if (!hasPrev && x < mBoundLeft) {
-            int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
-            mEdgeView.onPull(pixels, EdgeView.LEFT);
-            x = mBoundLeft;
-        } else if (!hasNext && x > mBoundRight) {
-            int pixels = Math.round((x - mBoundRight) * mCurrentScale);
-            mEdgeView.onPull(pixels, EdgeView.RIGHT);
+        if (!mHasPrev && x > mBoundRight) {
+            int pixels = x - mBoundRight;
+            mListener.onPull(pixels, EdgeView.LEFT);
             x = mBoundRight;
+        } else if (!mHasNext && x < mBoundLeft) {
+            int pixels = mBoundLeft - x;
+            mListener.onPull(pixels, EdgeView.RIGHT);
+            x = mBoundLeft;
         }
 
-        startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
+        startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
+    }
+
+    private void scrollToFilm(int x, int y) {
+        Box b = mBoxes.get(0);
+
+        // Horizontal direction: we show the edge effect when the scrolling
+        // tries to go left of the first image or go right of the last image.
+        x -= mPlatform.mDefaultX;
+        if (!mHasPrev && x > 0) {
+            mListener.onPull(x, EdgeView.LEFT);
+            x = 0;
+        } else if (!mHasNext && x < 0) {
+            mListener.onPull(-x, EdgeView.RIGHT);
+            x = 0;
+        }
+        x += mPlatform.mDefaultX;
+        startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
     }
 
     public boolean fling(float velocityX, float velocityY) {
+        int vx = (int) (velocityX + 0.5f);
+        int vy = (int) (velocityY + 0.5f);
+        return mFilmMode ? flingFilm(vx, vy) : flingPage(vx, vy);
+    }
+
+    private boolean flingPage(int velocityX, int velocityY) {
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+
         // We only want to do fling when the picture is zoomed-in.
-        if (mImageW * mCurrentScale <= mViewW &&
-            mImageH * mCurrentScale <= mViewH) {
+        if (viewWiderThanScaledImage(b.mCurrentScale) &&
+            viewTallerThanScaledImage(b.mCurrentScale)) {
             return false;
         }
 
-        calculateStableBound(mCurrentScale);
-        mScroller.fling(mCurrentX, mCurrentY,
-                Math.round(-velocityX / mCurrentScale),
-                Math.round(-velocityY / mCurrentScale),
+        // We only allow flinging in the directions where it won't go over the
+        // picture.
+        int edges = getImageAtEdges();
+        if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
+            (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
+            velocityX = 0;
+        }
+        if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
+            (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
+            velocityY = 0;
+        }
+
+        if (velocityX == 0 && velocityY == 0) return false;
+
+        mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
                 mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
-        int targetX = mScroller.getFinalX();
-        int targetY = mScroller.getFinalY();
-        mAnimationDuration = mScroller.getDuration();
-        startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
+        int targetX = mPageScroller.getFinalX();
+        int targetY = mPageScroller.getFinalY();
+        ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
+        startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
         return true;
     }
 
-    private void startAnimation(
-            int targetX, int targetY, float scale, int kind) {
-        if (targetX == mCurrentX && targetY == mCurrentY
-                && scale == mCurrentScale) return;
+    private boolean flingFilm(int velocityX, int velocityY) {
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
 
-        mFromX = mCurrentX;
-        mFromY = mCurrentY;
-        mFromScale = mCurrentScale;
-
-        mToX = targetX;
-        mToY = targetY;
-        mToScale = Utils.clamp(scale, SCALE_MIN_EXTRA * mScaleMin,
-                SCALE_MAX_EXTRA * mScaleMax);
-
-        // If the scaled height is smaller than the view height,
-        // force it to be in the center.
-        // (We do for height only, not width, because the user may
-        // want to scroll to the previous/next image.)
-        if (Math.floor(mImageH * mToScale) <= mViewH) {
-            mToY = mImageH / 2;
-        }
-
-        mAnimationStartTime = SystemClock.uptimeMillis();
-        mAnimationKind = kind;
-        if (mAnimationKind != ANIM_KIND_FLING) {
-            mAnimationDuration = ANIM_TIME[mAnimationKind];
-        }
-        if (advanceAnimation()) mViewer.invalidate();
-    }
-
-    // Returns true if redraw is needed.
-    public boolean advanceAnimation() {
-        if (mAnimationStartTime == NO_ANIMATION) {
+        // If we are already at the edge, don't start the fling.
+        int defaultX = p.mDefaultX;
+        if ((!mHasPrev && p.mCurrentX >= defaultX)
+                || (!mHasNext && p.mCurrentX <= defaultX)) {
             return false;
-        } else if (mAnimationStartTime == LAST_ANIMATION) {
-            mAnimationStartTime = NO_ANIMATION;
-            if (mViewer.isInTransition()) {
-                mViewer.notifyTransitionComplete();
-                return false;
-            } else {
-                return startSnapbackIfNeeded();
-            }
         }
 
-        long now = SystemClock.uptimeMillis();
-        float progress;
-        if (mAnimationDuration == 0) {
-            progress = 1;
-        } else {
-            progress = (now - mAnimationStartTime) / mAnimationDuration;
-        }
+        if (velocityX == 0) return false;
 
-        if (progress >= 1) {
-            progress = 1;
-            mCurrentX = mToX;
-            mCurrentY = mToY;
-            mCurrentScale = mToScale;
-            mAnimationStartTime = LAST_ANIMATION;
-        } else {
-            float f = 1 - progress;
-            switch (mAnimationKind) {
-                case ANIM_KIND_SCROLL:
-                case ANIM_KIND_FLING:
-                    progress = 1 - f;  // linear
-                    break;
-                case ANIM_KIND_SCALE:
-                    progress = 1 - f * f;  // quadratic
-                    break;
-                case ANIM_KIND_SNAPBACK:
-                case ANIM_KIND_ZOOM:
-                case ANIM_KIND_SLIDE:
-                    progress = 1 - f * f * f * f * f; // x^5
-                    break;
-            }
-            if (mAnimationKind == ANIM_KIND_FLING) {
-                flingInterpolate(progress);
-            } else {
-                linearInterpolate(progress);
-            }
-        }
-        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+        mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
+                Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+        int targetX = mFilmScroller.getFinalX();
+        // This value doesn't matter because we use mFilmScroller.isFinished()
+        // to decide when to stop. We set this to 0 so it's faster for
+        // Animatable.advanceAnimation() to calculate the progress (always 1).
+        ANIM_TIME[ANIM_KIND_FLING] = 0;
+        startAnimation(targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING);
         return true;
     }
 
-    private void flingInterpolate(float progress) {
-        mScroller.computeScrollOffset(progress);
-        int oldX = mCurrentX;
-        int oldY = mCurrentY;
-        mCurrentX = mScroller.getCurrX();
-        mCurrentY = mScroller.getCurrY();
+    ////////////////////////////////////////////////////////////////////////////
+    //  Redraw
+    //
+    //  If a method changes box positions directly, redraw()
+    //  should be called.
+    //
+    //  If a method may also cause a snapback to happen, snapAndRedraw() should
+    //  be called.
+    //
+    //  If a method starts an animation to change the position of focused box,
+    //  startAnimation() should be called.
+    //
+    //  If time advances to change the box position, advanceAnimation() should
+    //  be called.
+    ////////////////////////////////////////////////////////////////////////////
+    private void redraw() {
+        layoutAndSetPosition();
+        mListener.invalidate();
+    }
 
-        // Check if we hit the edges; show edge effects if we do.
-        if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
-            int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale);
-            mEdgeView.onAbsorb(v, EdgeView.LEFT);
-        } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
-            int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale);
-            mEdgeView.onAbsorb(v, EdgeView.RIGHT);
+    private void snapAndRedraw() {
+        mPlatform.startSnapback();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mBoxes.get(i).startSnapback();
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mGaps.get(i).startSnapback();
+        }
+        mFilmRatio.startSnapback();
+        redraw();
+    }
+
+    private void startAnimation(int targetX, int targetY, float targetScale,
+            int kind) {
+        boolean changed = false;
+        changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
+        changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
+        if (changed) redraw();
+    }
+
+    public void advanceAnimation() {
+        boolean changed = false;
+        changed |= mPlatform.advanceAnimation();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            changed |= mBoxes.get(i).advanceAnimation();
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            changed |= mGaps.get(i).advanceAnimation();
+        }
+        changed |= mFilmRatio.advanceAnimation();
+        if (changed) redraw();
+    }
+
+    public boolean inOpeningAnimation() {
+        return (mPlatform.mAnimationKind == ANIM_KIND_OPENING &&
+                mPlatform.mAnimationStartTime != NO_ANIMATION) ||
+               (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING &&
+                mBoxes.get(0).mAnimationStartTime != NO_ANIMATION);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Layout
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Returns the display width of this box.
+    private int widthOf(Box b) {
+        return (int) (b.mImageW * b.mCurrentScale + 0.5f);
+    }
+
+    // Returns the display height of this box.
+    private int heightOf(Box b) {
+        return (int) (b.mImageH * b.mCurrentScale + 0.5f);
+    }
+
+    // Returns the display width of this box, using the given scale.
+    private int widthOf(Box b, float scale) {
+        return (int) (b.mImageW * scale + 0.5f);
+    }
+
+    // Returns the display height of this box, using the given scale.
+    private int heightOf(Box b, float scale) {
+        return (int) (b.mImageH * scale + 0.5f);
+    }
+
+    // Convert the information in mPlatform and mBoxes to mRects, so the user
+    // can get the position of each box by getPosition().
+    //
+    // Note the loop index goes from inside-out because each box's X coordinate
+    // is relative to its anchor box (except the focused box).
+    private void layoutAndSetPosition() {
+        // layout box 0 (focused box)
+        convertBoxToRect(0);
+        for (int i = 1; i <= BOX_MAX; i++) {
+            // layout box i and -i
+            convertBoxToRect(i);
+            convertBoxToRect(-i);
+        }
+        //dumpState();
+    }
+
+    private void dumpState() {
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
         }
 
-        if (oldY > mBoundTop && mCurrentY == mBoundTop) {
-            int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale);
-            mEdgeView.onAbsorb(v, EdgeView.TOP);
-        } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
-            int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale);
-            mEdgeView.onAbsorb(v, EdgeView.BOTTOM);
+        dumpRect(0);
+        for (int i = 1; i <= BOX_MAX; i++) {
+            dumpRect(i);
+            dumpRect(-i);
+        }
+
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            for (int j = i + 1; j <= BOX_MAX; j++) {
+                if (Rect.intersects(mRects.get(i), mRects.get(j))) {
+                    Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
+                }
+            }
         }
     }
 
-    // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
-    private void linearInterpolate(float progress) {
-        // To linearly interpolate the position on view coordinates, we do the
-        // following steps:
-        // (1) convert a bitmap position (x, y) to view coordinates:
-        //     from: (x - mFromX) * mFromScale + mViewW / 2
-        //     to: (x - mToX) * mToScale + mViewW / 2
-        // (2) interpolate between the "from" and "to" coordinates:
-        //     (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
-        //     + mViewW / 2
-        //     should be equal to
-        //     (x - mCurrentX) * mCurrentScale + mViewW / 2
-        // (3) The x-related terms in the above equation can be removed because
-        //     mFromScale * (1 - p) + ToScale * p = mCurrentScale
-        // (4) Solve for mCurrentX, we have mCurrentX =
-        // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
-        float fromX = mFromX * mFromScale;
-        float toX = mToX * mToScale;
-        float currentX = fromX + progress * (toX - fromX);
-
-        float fromY = mFromY * mFromScale;
-        float toY = mToY * mToScale;
-        float currentY = fromY + progress * (toY - fromY);
-
-        mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
-        mCurrentX = Math.round(currentX / mCurrentScale);
-        mCurrentY = Math.round(currentY / mCurrentScale);
+    private void dumpRect(int i) {
+        StringBuilder sb = new StringBuilder();
+        Rect r = mRects.get(i);
+        sb.append("Rect " + i + ":");
+        sb.append("(");
+        sb.append(r.centerX());
+        sb.append(",");
+        sb.append(r.centerY());
+        sb.append(") [");
+        sb.append(r.width());
+        sb.append("x");
+        sb.append(r.height());
+        sb.append("]");
+        Log.d(TAG, sb.toString());
     }
 
-    // Returns true if redraw is needed.
-    private boolean startSnapbackIfNeeded() {
-        if (mAnimationStartTime != NO_ANIMATION) return false;
-        if (mInScale) return false;
-        if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
-            return false;
+    private void convertBoxToRect(int i) {
+        Box b = mBoxes.get(i);
+        Rect r = mRects.get(i);
+        int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
+        int w = widthOf(b);
+        int h = heightOf(b);
+        if (i == 0) {
+            int x = mPlatform.mCurrentX + mViewW / 2;
+            r.left = x - w / 2;
+            r.right = r.left + w;
+        } else if (i > 0) {
+            Rect a = mRects.get(i - 1);
+            Gap g = mGaps.get(i - 1);
+            r.left = a.right + g.mCurrentGap;
+            r.right = r.left + w;
+        } else {  // i < 0
+            Rect a = mRects.get(i + 1);
+            Gap g = mGaps.get(i);
+            r.right = a.left - g.mCurrentGap;
+            r.left = r.right - w;
         }
-        return startSnapback();
+        r.top = y - h / 2;
+        r.bottom = r.top + h;
     }
 
-    public boolean startSnapback() {
-        boolean needAnimation = false;
-        float scale = mCurrentScale;
-
-        float scaleMin = mExtraScalingRange ?
-                mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
-        float scaleMax = mExtraScalingRange ?
-                mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
-
-        if (mCurrentScale < scaleMin || mCurrentScale > scaleMax) {
-            needAnimation = true;
-            scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
-        }
-
-        calculateStableBound(scale, sHorizontalSlack);
-        int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
-        int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
-
-        if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
-            needAnimation = true;
-        }
-
-        if (needAnimation) {
-            startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
-        }
-
-        return needAnimation;
+    // Returns the position of a box.
+    public Rect getPosition(int index) {
+        return mRects.get(index);
     }
 
-    // Calculates the stable region of mCurrent{X/Y}, where "stable" means
+    ////////////////////////////////////////////////////////////////////////////
+    //  Box management
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Initialize the platform to be at the view center.
+    private void initPlatform() {
+        mPlatform.updateDefaultXY();
+        mPlatform.mCurrentX = mPlatform.mDefaultX;
+        mPlatform.mCurrentY = mPlatform.mDefaultY;
+        mPlatform.mAnimationStartTime = NO_ANIMATION;
+    }
+
+    // Initialize a box to have the size of the view.
+    private void initBox(int index) {
+        Box b = mBoxes.get(index);
+        b.mImageW = mViewW;
+        b.mImageH = mViewH;
+        b.mUseViewSize = true;
+        b.mScaleMin = getMinimalScale(b);
+        b.mScaleMax = getMaximalScale(b);
+        b.mCurrentY = 0;
+        b.mCurrentScale = b.mScaleMin;
+        b.mAnimationStartTime = NO_ANIMATION;
+    }
+
+    // Initialize a gap. This can only be called after the boxes around the gap
+    // has been initialized.
+    private void initGap(int index) {
+        Gap g = mGaps.get(index);
+        g.mDefaultSize = getDefaultGapSize(index);
+        g.mCurrentGap = g.mDefaultSize;
+        g.mAnimationStartTime = NO_ANIMATION;
+    }
+
+    private void initGap(int index, int size) {
+        Gap g = mGaps.get(index);
+        g.mDefaultSize = getDefaultGapSize(index);
+        g.mCurrentGap = size;
+        g.mAnimationStartTime = NO_ANIMATION;
+    }
+
+    private void debugMoveBox(int fromIndex[]) {
+        StringBuilder s = new StringBuilder("moveBox:");
+        for (int i = 0; i < fromIndex.length; i++) {
+            int j = fromIndex[i];
+            if (j == Integer.MAX_VALUE) {
+                s.append(" N");
+            } else {
+                s.append(" ");
+                s.append(fromIndex[i]);
+            }
+        }
+        Log.d(TAG, s.toString());
+    }
+
+    // Move the boxes: it may indicate focus change, box deleted, box appearing,
+    // box reordered, etc.
+    //
+    // Each element in the fromIndex array indicates where each box was in the
+    // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
+    // means the box is new.
+    //
+    // For example:
+    // N N N N N N N -- all new boxes
+    // -3 -2 -1 0 1 2 3 -- nothing changed
+    // -2 -1 0 1 2 3 N -- focus goes to the next box
+    // N -3 -2 -1 0 1 2 -- focuse goes to the previous box
+    // -3 -2 -1 1 2 3 N -- the focused box was deleted.
+    //
+    // hasPrev/hasNext indicates if there are previous/next boxes for the
+    // focused box. constrained indicates whether the focused box should be put
+    // into the constrained frame.
+    public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
+            boolean constrained) {
+        //debugMoveBox(fromIndex);
+        mHasPrev = hasPrev;
+        mHasNext = hasNext;
+
+        RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
+
+        // 1. Get the absolute X coordiates for the boxes.
+        layoutAndSetPosition();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            Box b = mBoxes.get(i);
+            Rect r = mRects.get(i);
+            b.mAbsoluteX = r.centerX() - mViewW / 2;
+        }
+
+        // 2. copy boxes and gaps to temporary storage.
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mTempBoxes.put(i, mBoxes.get(i));
+            mBoxes.put(i, null);
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mTempGaps.put(i, mGaps.get(i));
+            mGaps.put(i, null);
+        }
+
+        // 3. move back boxes that are used in the new array.
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            int j = from.get(i);
+            if (j == Integer.MAX_VALUE) continue;
+            mBoxes.put(i, mTempBoxes.get(j));
+            mTempBoxes.put(j, null);
+        }
+
+        // 4. move back gaps if both boxes around it are kept together.
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            int j = from.get(i);
+            if (j == Integer.MAX_VALUE) continue;
+            int k = from.get(i + 1);
+            if (k == Integer.MAX_VALUE) continue;
+            if (j + 1 == k) {
+                mGaps.put(i, mTempGaps.get(j));
+                mTempGaps.put(j, null);
+            }
+        }
+
+        // 5. recycle the boxes that are not used in the new array.
+        int k = -BOX_MAX;
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            if (mBoxes.get(i) != null) continue;
+            while (mTempBoxes.get(k) == null) {
+                k++;
+            }
+            mBoxes.put(i, mTempBoxes.get(k++));
+            initBox(i);
+        }
+
+        // 6. Now give the recycled box a reasonable absolute X position.
+        //
+        // First try to find the first and the last box which the absolute X
+        // position is known.
+        int first, last;
+        for (first = -BOX_MAX; first <= BOX_MAX; first++) {
+            if (from.get(first) != Integer.MAX_VALUE) break;
+        }
+        for (last = BOX_MAX; last >= -BOX_MAX; last--) {
+            if (from.get(last) != Integer.MAX_VALUE) break;
+        }
+        // If there is no box has known X position at all, make the focused one
+        // as known.
+        if (first > BOX_MAX) {
+            mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
+            first = last = 0;
+        }
+        // Now for those boxes between first and last, just assign the same
+        // position as the next box. (We can do better, but this should be
+        // rare). For the boxes before first or after last, we will use a new
+        // default gap size below.
+        for (int i = last - 1; i > first; i--) {
+            if (from.get(i) != Integer.MAX_VALUE) continue;
+            mBoxes.get(i).mAbsoluteX = mBoxes.get(i + 1).mAbsoluteX;
+        }
+
+        // 7. recycle the gaps that are not used in the new array.
+        k = -BOX_MAX;
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            if (mGaps.get(i) != null) continue;
+            while (mTempGaps.get(k) == null) {
+                k++;
+            }
+            mGaps.put(i, mTempGaps.get(k++));
+            Box a = mBoxes.get(i);
+            Box b = mBoxes.get(i + 1);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            if (i >= first && i < last) {
+                int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
+                initGap(i, g);
+            } else {
+                initGap(i);
+            }
+        }
+
+        // 8. calculate the new absolute X coordinates for those box before
+        // first or after last.
+        for (int i = first - 1; i >= -BOX_MAX; i--) {
+            Box a = mBoxes.get(i + 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            Gap g = mGaps.get(i);
+            b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
+        }
+
+        for (int i = last + 1; i <= BOX_MAX; i++) {
+            Box a = mBoxes.get(i - 1);
+            Box b = mBoxes.get(i);
+            int wa = widthOf(a);
+            int wb = widthOf(b);
+            Gap g = mGaps.get(i - 1);
+            b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
+        }
+
+        // 9. offset the Platform position
+        int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
+        mPlatform.mCurrentX += dx;
+        mPlatform.mFromX += dx;
+        mPlatform.mToX += dx;
+        mPlatform.mFlingOffset += dx;
+
+        if (mConstrained != constrained) {
+            mConstrained = constrained;
+            mPlatform.updateDefaultXY();
+            updateScaleAndGapLimit();
+        }
+
+        snapAndRedraw();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Public utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    public boolean isAtMinimalScale() {
+        Box b = mBoxes.get(0);
+        return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
+    }
+
+    public boolean isCenter() {
+        Box b = mBoxes.get(0);
+        return mPlatform.mCurrentX == mPlatform.mDefaultX
+            && b.mCurrentY == 0;
+    }
+
+    public int getImageWidth() {
+        Box b = mBoxes.get(0);
+        return b.mImageW;
+    }
+
+    public int getImageHeight() {
+        Box b = mBoxes.get(0);
+        return b.mImageH;
+    }
+
+    public float getImageScale() {
+        Box b = mBoxes.get(0);
+        return b.mCurrentScale;
+    }
+
+    public int getImageAtEdges() {
+        Box b = mBoxes.get(0);
+        Platform p = mPlatform;
+        calculateStableBound(b.mCurrentScale);
+        int edges = 0;
+        if (p.mCurrentX <= mBoundLeft) {
+            edges |= IMAGE_AT_RIGHT_EDGE;
+        }
+        if (p.mCurrentX >= mBoundRight) {
+            edges |= IMAGE_AT_LEFT_EDGE;
+        }
+        if (b.mCurrentY <= mBoundTop) {
+            edges |= IMAGE_AT_BOTTOM_EDGE;
+        }
+        if (b.mCurrentY >= mBoundBottom) {
+            edges |= IMAGE_AT_TOP_EDGE;
+        }
+        return edges;
+    }
+
+    public boolean isScrolling() {
+        return mPlatform.mAnimationStartTime != NO_ANIMATION
+                && mPlatform.mCurrentX != mPlatform.mToX;
+    }
+
+    public void stopScrolling() {
+        if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
+        if (mFilmMode) mFilmScroller.forceFinished(true);
+        mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
+    }
+
+    public float getFilmRatio() {
+        return mFilmRatio.mCurrentRatio;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Private utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    private float getMinimalScale(Box b) {
+        float wFactor = 1.0f;
+        float hFactor = 1.0f;
+        int viewW, viewH;
+
+        if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
+                && b == mBoxes.get(0)) {
+            viewW = mConstrainedFrame.width();
+            viewH = mConstrainedFrame.height();
+        } else {
+            viewW = mViewW;
+            viewH = mViewH;
+        }
+
+        if (mFilmMode) {
+            if (mViewH > mViewW) {  // portrait
+                wFactor = FILM_MODE_PORTRAIT_WIDTH;
+                hFactor = FILM_MODE_PORTRAIT_HEIGHT;
+            } else {  // landscape
+                wFactor = FILM_MODE_LANDSCAPE_WIDTH;
+                hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
+            }
+        }
+
+        float s = Math.min(wFactor * viewW / b.mImageW,
+                hFactor * viewH / b.mImageH);
+        return Math.min(SCALE_LIMIT, s);
+    }
+
+    private float getMaximalScale(Box b) {
+        if (mFilmMode) return getMinimalScale(b);
+        if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
+        return SCALE_LIMIT;
+    }
+
+    private static boolean isAlmostEqual(float a, float b) {
+        float diff = a - b;
+        return (diff < 0 ? -diff : diff) < 0.02f;
+    }
+
+    // Calculates the stable region of mPlatform.mCurrentX and
+    // mBoxes.get(0).mCurrentY, where "stable" means
     //
     // (1) If the dimension of scaled image >= view dimension, we will not
     // see black region outside the image (at that dimension).
@@ -594,95 +1167,511 @@
     // An extra parameter "horizontalSlack" (which has the value of 0 usually)
     // is used to extend the stable region by some pixels on each side
     // horizontally.
-    private void calculateStableBound(float scale) {
-        calculateStableBound(scale, 0f);
-    }
+    private void calculateStableBound(float scale, int horizontalSlack) {
+        Box b = mBoxes.get(0);
 
-    private void calculateStableBound(float scale, float horizontalSlack) {
-        // The number of pixels between the center of the view
-        // and the edge when the edge is aligned.
-        mBoundLeft = (int) Math.ceil((mViewW - horizontalSlack) / (2 * scale));
-        mBoundRight = mImageW - mBoundLeft;
-        mBoundTop = (int) Math.ceil(mViewH / (2 * scale));
-        mBoundBottom = mImageH - mBoundTop;
+        // The width and height of the box in number of view pixels
+        int w = widthOf(b, scale);
+        int h = heightOf(b, scale);
+
+        // When the edge of the view is aligned with the edge of the box
+        mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
+        mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
+        mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
+        mBoundBottom = h / 2 - mViewH / 2;
 
         // If the scaled height is smaller than the view height,
         // force it to be in the center.
-        if (Math.floor(mImageH * scale) <= mViewH) {
-            mBoundTop = mBoundBottom = mImageH / 2;
+        if (viewTallerThanScaledImage(scale)) {
+            mBoundTop = mBoundBottom = 0;
         }
 
         // Same for width
-        if (Math.floor(mImageW * scale) <= mViewW) {
-            mBoundLeft = mBoundRight = mImageW / 2;
+        if (viewWiderThanScaledImage(scale)) {
+            mBoundLeft = mBoundRight = mPlatform.mDefaultX;
         }
     }
 
-    private boolean useCurrentValueAsTarget() {
-        return mAnimationStartTime == NO_ANIMATION ||
-                mAnimationKind == ANIM_KIND_SNAPBACK ||
-                mAnimationKind == ANIM_KIND_FLING;
+    private void calculateStableBound(float scale) {
+        calculateStableBound(scale, 0);
     }
 
-    private float getTargetScale() {
-        return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
+    private boolean viewTallerThanScaledImage(float scale) {
+        return mViewH >= heightOf(mBoxes.get(0), scale);
     }
 
-    private int getTargetX() {
-        return useCurrentValueAsTarget() ? mCurrentX : mToX;
+    private boolean viewWiderThanScaledImage(float scale) {
+        return mViewW >= widthOf(mBoxes.get(0), scale);
     }
 
-    private int getTargetY() {
-        return useCurrentValueAsTarget() ? mCurrentY : mToY;
+    private float getTargetScale(Box b) {
+        return b.mAnimationStartTime == NO_ANIMATION
+                ? b.mCurrentScale : b.mToScale;
     }
 
-    public RectF getImageBounds() {
-        float points[] = mTempPoints;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Animatable: an thing which can do animation.
+    ////////////////////////////////////////////////////////////////////////////
+    private abstract static class Animatable {
+        public long mAnimationStartTime;
+        public int mAnimationKind;
+        public int mAnimationDuration;
 
-        /*
-         * (p0,p1)----------(p2,p3)
-         *   |                  |
-         *   |                  |
-         * (p4,p5)----------(p6,p7)
-         */
-        points[0] = points[4] = -mCurrentX;
-        points[1] = points[3] = -mCurrentY;
-        points[2] = points[6] = mImageW - mCurrentX;
-        points[5] = points[7] = mImageH - mCurrentY;
+        // This should be overidden in subclass to change the animation values
+        // give the progress value in [0, 1].
+        protected abstract boolean interpolate(float progress);
+        public abstract boolean startSnapback();
 
-        RectF rect = mTempRect;
-        rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
-                Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+        // Returns true if the animation values changes, so things need to be
+        // redrawn.
+        public boolean advanceAnimation() {
+            if (mAnimationStartTime == NO_ANIMATION) {
+                return false;
+            }
+            if (mAnimationStartTime == LAST_ANIMATION) {
+                mAnimationStartTime = NO_ANIMATION;
+                return startSnapback();
+            }
 
-        float scale = mCurrentScale;
-        float offsetX = mViewW / 2;
-        float offsetY = mViewH / 2;
-        for (int i = 0; i < 4; ++i) {
-            float x = points[i + i] * scale + offsetX;
-            float y = points[i + i + 1] * scale + offsetY;
-            if (x < rect.left) rect.left = x;
-            if (x > rect.right) rect.right = x;
-            if (y < rect.top) rect.top = y;
-            if (y > rect.bottom) rect.bottom = y;
+            float progress;
+            if (mAnimationDuration == 0) {
+                progress = 1;
+            } else {
+                long now = AnimationTime.get();
+                progress =
+                    (float) (now - mAnimationStartTime) / mAnimationDuration;
+            }
+
+            if (progress >= 1) {
+                progress = 1;
+            } else {
+                progress = applyInterpolationCurve(mAnimationKind, progress);
+            }
+
+            boolean done = interpolate(progress);
+
+            if (done) {
+                mAnimationStartTime = LAST_ANIMATION;
+            }
+
+            return true;
         }
-        return rect;
+
+        private static float applyInterpolationCurve(int kind, float progress) {
+            float f = 1 - progress;
+            switch (kind) {
+                case ANIM_KIND_SCROLL:
+                case ANIM_KIND_FLING:
+                case ANIM_KIND_CAPTURE:
+                    progress = 1 - f;  // linear
+                    break;
+                case ANIM_KIND_SCALE:
+                    progress = 1 - f * f;  // quadratic
+                    break;
+                case ANIM_KIND_OPENING:
+                    progress = 1 - f * f * f;  // x^3
+                    break;
+                case ANIM_KIND_SNAPBACK:
+                case ANIM_KIND_ZOOM:
+                case ANIM_KIND_SLIDE:
+                    progress = 1 - f * f * f * f * f; // x^5
+                    break;
+            }
+            return progress;
+        }
     }
 
-    public int getImageWidth() {
-        return mImageW;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Platform: captures the global X/Y movement.
+    ////////////////////////////////////////////////////////////////////////////
+    private class Platform extends Animatable {
+        public int mCurrentX, mFromX, mToX, mDefaultX;
+        public int mCurrentY, mFromY, mToY, mDefaultY;
+        public int mFlingOffset;
+
+        @Override
+        public boolean startSnapback() {
+            if (mAnimationStartTime != NO_ANIMATION) return false;
+            if (mAnimationKind == ANIM_KIND_SCROLL
+                    && mListener.isHolding()) return false;
+            if (mInScale) return false;
+
+            Box b = mBoxes.get(0);
+            float scaleMin = mExtraScalingRange ?
+                b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
+            float scaleMax = mExtraScalingRange ?
+                b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
+            float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
+            int x = mCurrentX;
+            int y = mDefaultY;
+            if (mFilmMode) {
+                int defaultX = mDefaultX;
+                if (!mHasNext) x = Math.max(x, defaultX);
+                if (!mHasPrev) x = Math.min(x, defaultX);
+            } else {
+                calculateStableBound(scale, HORIZONTAL_SLACK);
+                // If the picture is zoomed-in, we want to keep the focus point
+                // stay in the same position on screen, so we need to adjust
+                // target mCurrentX (which is the center of the focused
+                // box). The position of the focus point on screen (relative the
+                // the center of the view) is:
+                //
+                // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
+                // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
+                //
+                if (!viewWiderThanScaledImage(scale)) {
+                    float scaleDiff = b.mCurrentScale - scale;
+                    x += (int) (mFocusX * scaleDiff + 0.5f);
+                }
+                x = Utils.clamp(x, mBoundLeft, mBoundRight);
+            }
+            if (mCurrentX != x || mCurrentY != y) {
+                return doAnimation(x, y, ANIM_KIND_SNAPBACK);
+            }
+            return false;
+        }
+
+        // The updateDefaultXY() should be called whenever these variables
+        // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
+        // mFilmMode
+        public void updateDefaultXY() {
+            // We don't check mFilmMode and return 0 for mDefaultX. Because
+            // otherwise if we decide to leave film mode because we are
+            // centered, we will immediately back into film mode because we find
+            // we are not centered.
+            if (mConstrained && !mConstrainedFrame.isEmpty()) {
+                mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
+                mDefaultY = mFilmMode ? 0 :
+                        mConstrainedFrame.centerY() - mViewH / 2;
+            } else {
+                mDefaultX = 0;
+                mDefaultY = 0;
+            }
+        }
+
+        // Starts an animation for the platform.
+        private boolean doAnimation(int targetX, int targetY, int kind) {
+            if (mCurrentX == targetX && mCurrentY == targetY) return false;
+            mAnimationKind = kind;
+            mFromX = mCurrentX;
+            mFromY = mCurrentY;
+            mToX = targetX;
+            mToY = targetY;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[kind];
+            mFlingOffset = 0;
+            advanceAnimation();
+            return true;
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (mAnimationKind == ANIM_KIND_FLING) {
+                return mFilmMode
+                        ? interpolateFlingFilm(progress)
+                        : interpolateFlingPage(progress);
+            } else {
+                return interpolateLinear(progress);
+            }
+        }
+
+        private boolean interpolateFlingFilm(float progress) {
+            mFilmScroller.computeScrollOffset();
+            mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
+
+            int dir = EdgeView.INVALID_DIRECTION;
+            if (mCurrentX < mDefaultX) {
+                if (!mHasNext) {
+                    dir = EdgeView.RIGHT;
+                }
+            } else if (mCurrentX > mDefaultX) {
+                if (!mHasPrev) {
+                    dir = EdgeView.LEFT;
+                }
+            }
+            if (dir != EdgeView.INVALID_DIRECTION) {
+                int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
+                mListener.onAbsorb(v, dir);
+                mFilmScroller.forceFinished(true);
+                mCurrentX = mDefaultX;
+            }
+            return mFilmScroller.isFinished();
+        }
+
+        private boolean interpolateFlingPage(float progress) {
+            mPageScroller.computeScrollOffset(progress);
+            Box b = mBoxes.get(0);
+            calculateStableBound(b.mCurrentScale);
+
+            int oldX = mCurrentX;
+            mCurrentX = mPageScroller.getCurrX();
+
+            // Check if we hit the edges; show edge effects if we do.
+            if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
+                int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.RIGHT);
+            } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
+                int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.LEFT);
+            }
+
+            return progress >= 1;
+        }
+
+        private boolean interpolateLinear(float progress) {
+            // Other animations
+            if (progress >= 1) {
+                mCurrentX = mToX;
+                mCurrentY = mToY;
+                return true;
+            } else {
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    progress = CaptureAnimation.calculateSlide(progress);
+                }
+                mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+                mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    return false;
+                } else {
+                    return (mCurrentX == mToX && mCurrentY == mToY);
+                }
+            }
+        }
     }
 
-    public int getImageHeight() {
-        return mImageH;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Box: represents a rectangular area which shows a picture.
+    ////////////////////////////////////////////////////////////////////////////
+    private class Box extends Animatable {
+        // Size of the bitmap
+        public int mImageW, mImageH;
+
+        // This is true if we assume the image size is the same as view size
+        // until we know the actual size of image. This is also used to
+        // determine if there is an image ready to show.
+        public boolean mUseViewSize;
+
+        // The minimum and maximum scale we allow for this box.
+        public float mScaleMin, mScaleMax;
+
+        // The X/Y value indicates where the center of the box is on the view
+        // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
+        // actual values used currently. Note that the X values are implicitly
+        // defined by Platform and Gaps.
+        public int mCurrentY, mFromY, mToY;
+        public float mCurrentScale, mFromScale, mToScale;
+
+        // The absolute X coordinate of the center of the box. This is only used
+        // during moveBox().
+        public int mAbsoluteX;
+
+        @Override
+        public boolean startSnapback() {
+            if (mAnimationStartTime != NO_ANIMATION) return false;
+            if (mAnimationKind == ANIM_KIND_SCROLL
+                    && mListener.isHolding()) return false;
+            if (mInScale && this == mBoxes.get(0)) return false;
+
+            int y = mCurrentY;
+            float scale;
+
+            if (this == mBoxes.get(0)) {
+                float scaleMin = mExtraScalingRange ?
+                    mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
+                float scaleMax = mExtraScalingRange ?
+                    mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
+                scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
+                if (mFilmMode) {
+                    y = 0;
+                } else {
+                    calculateStableBound(scale, HORIZONTAL_SLACK);
+                    // If the picture is zoomed-in, we want to keep the focus
+                    // point stay in the same position on screen. See the
+                    // comment in Platform.startSnapback for details.
+                    if (!viewTallerThanScaledImage(scale)) {
+                        float scaleDiff = mCurrentScale - scale;
+                        y += (int) (mFocusY * scaleDiff + 0.5f);
+                    }
+                    y = Utils.clamp(y, mBoundTop, mBoundBottom);
+                }
+            } else {
+                y = 0;
+                scale = mScaleMin;
+            }
+
+            if (mCurrentY != y || mCurrentScale != scale) {
+                return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
+            }
+            return false;
+        }
+
+        private boolean doAnimation(int targetY, float targetScale, int kind) {
+            targetScale = clampScale(targetScale);
+
+            // If the scaled height is smaller than the view height, force it to be
+            // in the center.  (We do this for height only, not width, because the
+            // user may want to scroll to the previous/next image.)
+            if (!mInScale && viewTallerThanScaledImage(targetScale)) {
+                targetY = 0;
+            }
+
+            if (mCurrentY == targetY && mCurrentScale == targetScale
+                    && kind != ANIM_KIND_CAPTURE) {
+                return false;
+            }
+
+            // Now starts an animation for the box.
+            mAnimationKind = kind;
+            mFromY = mCurrentY;
+            mFromScale = mCurrentScale;
+            mToY = targetY;
+            mToScale = targetScale;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[kind];
+            advanceAnimation();
+            return true;
+        }
+
+        // Clamps the input scale to the range that doAnimation() can reach.
+        public float clampScale(float s) {
+            return Utils.clamp(s,
+                    SCALE_MIN_EXTRA * mScaleMin,
+                    SCALE_MAX_EXTRA * mScaleMax);
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (mAnimationKind == ANIM_KIND_FLING) {
+                // Currently a Box can only be flung in page mode.
+                return interpolateFlingPage(progress);
+            } else {
+                return interpolateLinear(progress);
+            }
+        }
+
+        private boolean interpolateFlingPage(float progress) {
+            mPageScroller.computeScrollOffset(progress);
+            calculateStableBound(mCurrentScale);
+
+            int oldY = mCurrentY;
+            mCurrentY = mPageScroller.getCurrY();
+
+            // Check if we hit the edges; show edge effects if we do.
+            if (oldY > mBoundTop && mCurrentY == mBoundTop) {
+                int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.BOTTOM);
+            } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
+                int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
+                mListener.onAbsorb(v, EdgeView.TOP);
+            }
+
+            return progress >= 1;
+        }
+
+        private boolean interpolateLinear(float progress) {
+            if (progress >= 1) {
+                mCurrentY = mToY;
+                mCurrentScale = mToScale;
+                return true;
+            } else {
+                mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+                mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    float f = CaptureAnimation.calculateScale(progress);
+                    mCurrentScale *= f;
+                    return false;
+                } else {
+                    return (mCurrentY == mToY && mCurrentScale == mToScale);
+                }
+            }
+        }
     }
 
-    public boolean isAtLeftEdge() {
-        calculateStableBound(mCurrentScale);
-        return mCurrentX <= mBoundLeft;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Gap: represents a rectangular area which is between two boxes.
+    ////////////////////////////////////////////////////////////////////////////
+    private class Gap extends Animatable {
+        // The default gap size between two boxes. The value may vary for
+        // different image size of the boxes and for different modes (page or
+        // film).
+        public int mDefaultSize;
+
+        // The gap size between the two boxes.
+        public int mCurrentGap, mFromGap, mToGap;
+
+        @Override
+        public boolean startSnapback() {
+            if (mAnimationStartTime != NO_ANIMATION) return false;
+            return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
+        }
+
+        // Starts an animation for a gap.
+        public boolean doAnimation(int targetSize, int kind) {
+            if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
+                return false;
+            }
+            mAnimationKind = kind;
+            mFromGap = mCurrentGap;
+            mToGap = targetSize;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[mAnimationKind];
+            advanceAnimation();
+            return true;
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (progress >= 1) {
+                mCurrentGap = mToGap;
+                return true;
+            } else {
+                mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    float f = CaptureAnimation.calculateScale(progress);
+                    mCurrentGap = (int) (mCurrentGap * f);
+                    return false;
+                } else {
+                    return (mCurrentGap == mToGap);
+                }
+            }
+        }
     }
 
-    public boolean isAtRightEdge() {
-        calculateStableBound(mCurrentScale);
-        return mCurrentX >= mBoundRight;
+    ////////////////////////////////////////////////////////////////////////////
+    //  FilmRatio: represents the progress of film mode change.
+    ////////////////////////////////////////////////////////////////////////////
+    private class FilmRatio extends Animatable {
+        // The film ratio: 1 means switching to film mode is complete, 0 means
+        // switching to page mode is complete.
+        public float mCurrentRatio, mFromRatio, mToRatio;
+
+        @Override
+        public boolean startSnapback() {
+            float target = mFilmMode ? 1f : 0f;
+            if (target == mToRatio) return false;
+            return doAnimation(target, ANIM_KIND_SNAPBACK);
+        }
+
+        // Starts an animation for the film ratio.
+        private boolean doAnimation(float targetRatio, int kind) {
+            mAnimationKind = kind;
+            mFromRatio = mCurrentRatio;
+            mToRatio = targetRatio;
+            mAnimationStartTime = AnimationTime.startTime();
+            mAnimationDuration = ANIM_TIME[mAnimationKind];
+            advanceAnimation();
+            return true;
+        }
+
+        @Override
+        protected boolean interpolate(float progress) {
+            if (progress >= 1) {
+                mCurrentRatio = mToRatio;
+                return true;
+            } else {
+                mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
+                return (mCurrentRatio == mToRatio);
+            }
+        }
     }
 }
diff --git a/src/com/android/gallery3d/ui/PositionRepository.java b/src/com/android/gallery3d/ui/PositionRepository.java
deleted file mode 100644
index 0b829fa..0000000
--- a/src/com/android/gallery3d/ui/PositionRepository.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.common.Utils;
-
-import java.util.HashMap;
-import java.util.WeakHashMap;
-
-public class PositionRepository {
-    private static final WeakHashMap<GalleryActivity, PositionRepository>
-            sMap = new WeakHashMap<GalleryActivity, PositionRepository>();
-
-    public static class Position implements Cloneable {
-        public float x;
-        public float y;
-        public float z;
-        public float theta;
-        public float alpha;
-
-        public Position() {
-        }
-
-        public Position(float x, float y, float z) {
-            this(x, y, z, 0f, 1f);
-        }
-
-        public Position(float x, float y, float z, float ftheta, float alpha) {
-            this.x = x;
-            this.y = y;
-            this.z = z;
-            this.theta = ftheta;
-            this.alpha = alpha;
-        }
-
-        @Override
-        public Position clone() {
-            try {
-                return (Position) super.clone();
-            } catch (CloneNotSupportedException e) {
-                throw new AssertionError(); // we do support clone.
-            }
-        }
-
-        public void set(Position another) {
-            x = another.x;
-            y = another.y;
-            z = another.z;
-            theta = another.theta;
-            alpha = another.alpha;
-        }
-
-        public void set(float x, float y, float z, float ftheta, float alpha) {
-            this.x = x;
-            this.y = y;
-            this.z = z;
-            this.theta = ftheta;
-            this.alpha = alpha;
-        }
-
-        @Override
-        public boolean equals(Object object) {
-            if (!(object instanceof Position)) return false;
-            Position position = (Position) object;
-            return x == position.x && y == position.y && z == position.z
-                    && theta == position.theta
-                    && alpha == position.alpha;
-        }
-
-        public static void interpolate(
-                Position source, Position target, Position output, float progress) {
-            if (progress < 1f) {
-                output.set(
-                        Utils.interpolateScale(source.x, target.x, progress),
-                        Utils.interpolateScale(source.y, target.y, progress),
-                        Utils.interpolateScale(source.z, target.z, progress),
-                        Utils.interpolateAngle(source.theta, target.theta, progress),
-                        Utils.interpolateScale(source.alpha, target.alpha, progress));
-            } else {
-                output.set(target);
-            }
-        }
-    }
-
-    public static PositionRepository getInstance(GalleryActivity activity) {
-        PositionRepository repository = sMap.get(activity);
-        if (repository == null) {
-            repository = new PositionRepository();
-            sMap.put(activity, repository);
-        }
-        return repository;
-    }
-
-    private HashMap<Long, Position> mData = new HashMap<Long, Position>();
-    private int mOffsetX;
-    private int mOffsetY;
-    private Position mTempPosition = new Position();
-
-    public Position get(Long identity) {
-        Position position = mData.get(identity);
-        if (position == null) return null;
-        mTempPosition.set(position);
-        position = mTempPosition;
-        position.x -= mOffsetX;
-        position.y -= mOffsetY;
-        return position;
-    }
-
-    public void setOffset(int offsetX, int offsetY) {
-        mOffsetX = offsetX;
-        mOffsetY = offsetY;
-    }
-
-    public void putPosition(Long identity, Position position) {
-        Position clone = position.clone();
-        clone.x += mOffsetX;
-        clone.y += mOffsetY;
-        mData.put(identity, clone);
-    }
-
-    public void clear() {
-        mData.clear();
-    }
-}
diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java
index e4d6024..4f9381c 100644
--- a/src/com/android/gallery3d/ui/ProgressSpinner.java
+++ b/src/com/android/gallery3d/ui/ProgressSpinner.java
@@ -16,10 +16,10 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.R;
-
 import android.content.Context;
 
+import com.android.gallery3d.R;
+
 public class ProgressSpinner {
     private static float ROTATE_SPEED_OUTER = 1080f / 3500f;
     private static float ROTATE_SPEED_INNER = -720f / 3500f;
@@ -55,7 +55,7 @@
     }
 
     public void draw(GLCanvas canvas, int x, int y) {
-        long now = canvas.currentAnimationTimeMillis();
+        long now = AnimationTime.get();
         if (mAnimationTimestamp == -1) mAnimationTimestamp = now;
         mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER;
         mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER;
@@ -68,7 +68,7 @@
 
         canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
 
-        canvas.translate(x + mWidth / 2, y + mHeight / 2, 0);
+        canvas.translate(x + mWidth / 2, y + mHeight / 2);
         canvas.rotate(mInnerDegree, 0, 0, 1);
         mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2);
         canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1);
diff --git a/src/com/android/gallery3d/ui/RawTexture.java b/src/com/android/gallery3d/ui/RawTexture.java
index c1be435..4c0d9d3 100644
--- a/src/com/android/gallery3d/ui/RawTexture.java
+++ b/src/com/android/gallery3d/ui/RawTexture.java
@@ -17,38 +17,75 @@
 package com.android.gallery3d.ui;
 
 import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
 
-// RawTexture is used for texture created by glCopyTexImage2D.
-//
-// It will throw RuntimeException in onBind() if used with a different GL
-// context. It is only used internally by copyTexture() in GLCanvas.
-class RawTexture extends BasicTexture {
+public class RawTexture extends BasicTexture {
+    private static final String TAG = "RawTexture";
 
-    private RawTexture(GLCanvas canvas, int id) {
-        super(canvas, id, STATE_LOADED);
+    private final static int[] sTextureId = new int[1];
+    private final static float[] sCropRect = new float[4];
+
+    private final boolean mOpaque;
+
+    public RawTexture(int width, int height, boolean opaque) {
+        mOpaque = opaque;
+        setSize(width, height);
     }
 
-    public static RawTexture newInstance(GLCanvas canvas) {
-        int[] textureId = new int[1];
+    @Override
+    public boolean isOpaque() {
+        return mOpaque;
+    }
+
+    protected void prepare(GLCanvas canvas) {
         GL11 gl = canvas.getGLInstance();
-        gl.glGenTextures(1, textureId, 0);
-        return new RawTexture(canvas, textureId[0]);
+
+        // Define a vertically flipped crop rectangle for
+        // OES_draw_texture.
+        // The four values in sCropRect are: left, bottom, width, and
+        // height. Negative value of width or height means flip.
+        sCropRect[0] = 0;
+        sCropRect[1] = mHeight;
+        sCropRect[2] = mWidth;
+        sCropRect[3] = -mHeight;
+
+        // Upload the bitmap to a new texture.
+        GLId.glGenTextures(1, sTextureId, 0);
+        gl.glBindTexture(GL11.GL_TEXTURE_2D, sTextureId[0]);
+        gl.glTexParameterfv(GL11.GL_TEXTURE_2D,
+                GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
+        gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+                GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+        gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+                GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+        gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+                GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+        gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+                GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+
+        gl.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA,
+                getTextureWidth(), getTextureHeight(),
+                0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, null);
+
+        mId = sTextureId[0];
+        mState = STATE_LOADED;
+        setAssociatedCanvas(canvas);
     }
 
     @Override
     protected boolean onBind(GLCanvas canvas) {
-        if (mCanvasRef.get() != canvas) {
-            throw new RuntimeException("cannot bind to different canvas");
-        }
-        return true;
-    }
-
-    public boolean isOpaque() {
-        return true;
+        if (isLoaded()) return true;
+        Log.w(TAG, "lost the content due to context change");
+        return false;
     }
 
     @Override
-    public void yield() {
-        // we cannot free the texture because we have no backup.
+     public void yield() {
+         // we cannot free the texture because we have no backup.
+     }
+
+    @Override
+    protected int getTarget() {
+        return GL11.GL_TEXTURE_2D;
     }
 }
diff --git a/src/com/android/gallery3d/ui/RelativePosition.java b/src/com/android/gallery3d/ui/RelativePosition.java
new file mode 100644
index 0000000..0f2bfd8
--- /dev/null
+++ b/src/com/android/gallery3d/ui/RelativePosition.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+public class RelativePosition {
+    private float mAbsoluteX;
+    private float mAbsoluteY;
+    private float mReferenceX;
+    private float mReferenceY;
+
+    public void setAbsolutePosition(int absoluteX, int absoluteY) {
+        mAbsoluteX = absoluteX;
+        mAbsoluteY = absoluteY;
+    }
+
+    public void setReferencePosition(int x, int y) {
+        mReferenceX = x;
+        mReferenceY = y;
+    }
+
+    public float getX() {
+        return mAbsoluteX - mReferenceX;
+    }
+
+    public float getY() {
+        return mAbsoluteY - mReferenceY;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/ResourceTexture.java b/src/com/android/gallery3d/ui/ResourceTexture.java
index 08fb891..1fa9d70 100644
--- a/src/com/android/gallery3d/ui/ResourceTexture.java
+++ b/src/com/android/gallery3d/ui/ResourceTexture.java
@@ -16,12 +16,12 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 
+import com.android.gallery3d.common.Utils;
+
 // ResourceTexture is a texture whose Bitmap is decoded from a resource.
 // By default ResourceTexture is not opaque.
 public class ResourceTexture extends UploadedTexture {
diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java
new file mode 100644
index 0000000..0a16ab8
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScreenNail.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.ui;
+
+import android.graphics.RectF;
+
+public interface ScreenNail {
+    public int getWidth();
+    public int getHeight();
+    public void draw(GLCanvas canvas, int x, int y, int width, int height);
+
+    // We do not need to draw this ScreenNail in this frame.
+    public void noDraw();
+
+    // This ScreenNail will not be used anymore. Release related resources.
+    public void recycle();
+
+    // This is only used by TileImageView to back up the tiles not yet loaded.
+    public void draw(GLCanvas canvas, RectF source, RectF dest);
+}
diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java
index b33f03b..82d4800 100644
--- a/src/com/android/gallery3d/ui/ScrollBarView.java
+++ b/src/com/android/gallery3d/ui/ScrollBarView.java
@@ -16,8 +16,6 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.R;
-
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.TypedValue;
@@ -26,10 +24,6 @@
     @SuppressWarnings("unused")
     private static final String TAG = "ScrollBarView";
 
-    public interface Listener {
-        void onScrollBarPositionChanged(int position);
-    }
-
     private int mBarHeight;
 
     private int mGripHeight;
@@ -40,7 +34,6 @@
     private int mContentPosition;
     private int mContentTotal;
 
-    private Listener mListener;
     private NinePatchTexture mScrollBarTexture;
 
     public ScrollBarView(Context context, int gripHeight, int gripWidth) {
@@ -55,10 +48,6 @@
         mGripHeight = gripHeight;
     }
 
-    public void setListener(Listener listener) {
-        mListener = listener;
-    }
-
     @Override
     protected void onLayout(
             boolean changed, int left, int top, int right, int bottom) {
@@ -94,13 +83,6 @@
         mGripPosition = Math.round(r * mContentPosition);
     }
 
-    private void notifyContentPositionFromGrip() {
-        if (mContentTotal <= 0) return;
-        float r = (getWidth() - mGripWidth) / (float) mContentTotal;
-        int newContentPosition = Math.round(mGripPosition / r);
-        mListener.onScrollBarPositionChanged(newContentPosition);
-    }
-
     @Override
     protected void render(GLCanvas canvas) {
         super.render(canvas);
@@ -109,31 +91,4 @@
         int y = (mBarHeight - mGripHeight) / 2;
         mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight);
     }
-
-    // The onTouch() handler is disabled because now we don't want the user
-    // to drag the bar (it's an indicator only).
-    /*
-    @Override
-    protected boolean onTouch(MotionEvent event) {
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN: {
-                int x = (int) event.getX();
-                return (x >= mGripPosition && x < mGripPosition + mGripWidth);
-            }
-            case MotionEvent.ACTION_MOVE: {
-                // Adjust x by mGripWidth / 2 so the center of the grip
-                // matches the touch position.
-                int x = (int) event.getX() - mGripWidth / 2;
-                x = Utils.clamp(x, 0, getWidth() - mGripWidth);
-                if (mGripPosition != x) {
-                    mGripPosition = x;
-                    notifyContentPositionFromGrip();
-                    invalidate();
-                }
-                break;
-            }
-        }
-        return true;
-    }
-    */
 }
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
index 8423518..1ff633a 100644
--- a/src/com/android/gallery3d/ui/ScrollerHelper.java
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -16,12 +16,12 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-
 import android.content.Context;
 import android.view.ViewConfiguration;
 import android.widget.OverScroller;
 
+import com.android.gallery3d.common.Utils;
+
 public class ScrollerHelper {
     private OverScroller mScroller;
     private int mOverflingDistance;
@@ -84,7 +84,8 @@
     // Returns the distance that over the scroll limit.
     public int startScroll(int distance, int min, int max) {
         int currPosition = mScroller.getCurrX();
-        int finalPosition = mScroller.getFinalX();
+        int finalPosition = mScroller.isFinished() ? currPosition :
+                mScroller.getFinalX();
         int newPosition = Utils.clamp(finalPosition + distance, min, max);
         if (newPosition != currPosition) {
             mScroller.startScroll(
diff --git a/src/com/android/gallery3d/ui/SelectionDrawer.java b/src/com/android/gallery3d/ui/SelectionDrawer.java
deleted file mode 100644
index 43b368f..0000000
--- a/src/com/android/gallery3d/ui/SelectionDrawer.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.Path;
-
-import android.graphics.Rect;
-
-/**
- * Drawer class responsible for drawing selectable frame.
- */
-public abstract class SelectionDrawer {
-    public static final int DATASOURCE_TYPE_NOT_CATEGORIZED = 0;
-    public static final int DATASOURCE_TYPE_LOCAL = 1;
-    public static final int DATASOURCE_TYPE_PICASA = 2;
-    public static final int DATASOURCE_TYPE_MTP = 3;
-    public static final int DATASOURCE_TYPE_CAMERA = 4;
-
-    public abstract void prepareDrawing();
-    public abstract void draw(GLCanvas canvas, Texture content,
-            int width, int height, int rotation, Path path,
-            int dataSourceType, int mediaType, boolean isPanorama,
-            int labelBackgroundHeight, boolean wantCache, boolean isCaching);
-    public abstract void drawFocus(GLCanvas canvas, int width, int height);
-
-    public void draw(GLCanvas canvas, Texture content, int width, int height,
-            int rotation, Path path, int mediaType, boolean isPanorama) {
-        draw(canvas, content, width, height, rotation, path,
-                DATASOURCE_TYPE_NOT_CATEGORIZED, mediaType, isPanorama,
-                0, false, false);
-    }
-
-    public static void drawWithRotation(GLCanvas canvas, Texture content,
-            int x, int y, int width, int height, int rotation) {
-        if (rotation != 0) {
-            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
-            canvas.rotate(rotation, 0, 0, 1);
-        }
-
-        content.draw(canvas, x, y, width, height);
-
-        if (rotation != 0) {
-            canvas.restore();
-        }
-    }
-
-    public static void drawFrame(GLCanvas canvas, NinePatchTexture frame,
-            int x, int y, int width, int height) {
-        Rect p = frame.getPaddings();
-        frame.draw(canvas, x - p.left, y - p.top, width + p.left + p.right,
-                 height + p.top + p.bottom);
-    }
-}
diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java
index 2726e8a..86e92da 100644
--- a/src/com/android/gallery3d/ui/SelectionManager.java
+++ b/src/com/android/gallery3d/ui/SelectionManager.java
@@ -43,7 +43,6 @@
     private boolean mInSelectionMode;
     private boolean mAutoLeave = true;
     private int mTotal;
-    private Path mPressedPath;
 
     public interface SelectionListener {
         public void onSelectionModeChange(int mode);
@@ -108,15 +107,21 @@
         return mInverseSelection ^ mClickedSet.contains(itemId);
     }
 
+    private int getTotalCount() {
+        if (mSourceMediaSet == null) return -1;
+
+        if (mTotal < 0) {
+            mTotal = mIsAlbumSet
+                    ? mSourceMediaSet.getSubMediaSetCount()
+                    : mSourceMediaSet.getMediaItemCount();
+        }
+        return mTotal;
+    }
+
     public int getSelectedCount() {
         int count = mClickedSet.size();
         if (mInverseSelection) {
-            if (mTotal < 0) {
-                mTotal = mIsAlbumSet
-                        ? mSourceMediaSet.getSubMediaSetCount()
-                        : mSourceMediaSet.getMediaItemCount();
-            }
-            count = mTotal - count;
+            count = getTotalCount() - count;
         }
         return count;
     }
@@ -129,20 +134,18 @@
             mClickedSet.add(path);
         }
 
+        // Convert to inverse selection mode if everything is selected.
+        int count = getSelectedCount();
+        if (count == getTotalCount()) {
+            selectAll();
+        }
+
         if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path));
-        if (getSelectedCount() == 0 && mAutoLeave) {
+        if (count == 0 && mAutoLeave) {
             leaveSelectionMode();
         }
     }
 
-    public void setPressedPath(Path path) {
-        mPressedPath = path;
-    }
-
-    public boolean isPressedPath(Path path) {
-        return path != null && path == mPressedPath;
-    }
-
     private static void expandMediaSet(ArrayList<Path> items, MediaSet set) {
         int subCount = set.getSubMediaSetCount();
         for (int i = 0; i < subCount; i++) {
@@ -168,8 +171,8 @@
         ArrayList<Path> selected = new ArrayList<Path>();
         if (mIsAlbumSet) {
             if (mInverseSelection) {
-                int max = mSourceMediaSet.getSubMediaSetCount();
-                for (int i = 0; i < max; i++) {
+                int total = getTotalCount();
+                for (int i = 0; i < total; i++) {
                     MediaSet set = mSourceMediaSet.getSubMediaSet(i);
                     Path id = set.getPath();
                     if (!mClickedSet.contains(id)) {
@@ -191,8 +194,7 @@
             }
         } else {
             if (mInverseSelection) {
-
-                int total = mSourceMediaSet.getMediaItemCount();
+                int total = getTotalCount();
                 int index = 0;
                 while (index < total) {
                     int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT);
@@ -216,8 +218,4 @@
         mSourceMediaSet = set;
         mTotal = -1;
     }
-
-    public MediaSet getSourceMediaSet() {
-        return mSourceMediaSet;
-    }
 }
diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java
index 79a6bf0..a057bb7 100644
--- a/src/com/android/gallery3d/ui/SlideshowView.java
+++ b/src/com/android/gallery3d/ui/SlideshowView.java
@@ -16,13 +16,14 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.anim.CanvasAnimation;
-import com.android.gallery3d.anim.FloatAnimation;
-
 import android.graphics.Bitmap;
 import android.graphics.PointF;
 
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.FloatAnimation;
+
 import java.util.Random;
+
 import javax.microedition.khronos.opengles.GL11;
 
 public class SlideshowView extends GLView {
@@ -90,14 +91,14 @@
 
     @Override
     protected void render(GLCanvas canvas) {
-        long currentTimeMillis = canvas.currentAnimationTimeMillis();
-        boolean requestRender = mTransitionAnimation.calculate(currentTimeMillis);
+        long animTime = AnimationTime.get();
+        boolean requestRender = mTransitionAnimation.calculate(animTime);
         GL11 gl = canvas.getGLInstance();
         gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE);
         float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get();
 
         if (mPrevTexture != null && alpha != 1f) {
-            requestRender |= mPrevAnimation.calculate(currentTimeMillis);
+            requestRender |= mPrevAnimation.calculate(animTime);
             canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
             canvas.setAlpha(1f - alpha);
             mPrevAnimation.apply(canvas);
@@ -107,7 +108,7 @@
             canvas.restore();
         }
         if (mCurrentTexture != null) {
-            requestRender |= mCurrentAnimation.calculate(currentTimeMillis);
+            requestRender |= mCurrentAnimation.calculate(animTime);
             canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
             canvas.setAlpha(alpha);
             mCurrentAnimation.apply(canvas);
@@ -148,7 +149,7 @@
             float centerX = viewWidth / 2 + mMovingVector.x * mProgress;
             float centerY = viewHeight / 2 + mMovingVector.y * mProgress;
 
-            canvas.translate(centerX, centerY, 0);
+            canvas.translate(centerX, centerY);
             canvas.scale(scale, scale, 0);
         }
 
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
index 3e0e2f2..88a2e0d 100644
--- a/src/com/android/gallery3d/ui/SlotView.java
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -24,35 +24,40 @@
 import android.view.animation.DecelerateInterpolator;
 
 import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.ui.PositionRepository.Position;
-import com.android.gallery3d.util.LinkedNode;
-
-import java.util.ArrayList;
-import java.util.HashMap;
 
 public class SlotView extends GLView {
     @SuppressWarnings("unused")
     private static final String TAG = "SlotView";
 
     private static final boolean WIDE = true;
-
     private static final int INDEX_NONE = -1;
 
+    public static final int RENDER_MORE_PASS = 1;
+    public static final int RENDER_MORE_FRAME = 2;
+
     public interface Listener {
         public void onDown(int index);
-        public void onUp();
+        public void onUp(boolean followedByLongPress);
         public void onSingleTapUp(int index);
         public void onLongTap(int index);
         public void onScrollPositionChanged(int position, int total);
     }
 
     public static class SimpleListener implements Listener {
-        public void onDown(int index) {}
-        public void onUp() {}
-        public void onSingleTapUp(int index) {}
-        public void onLongTap(int index) {}
-        public void onScrollPositionChanged(int position, int total) {}
+        @Override public void onDown(int index) {}
+        @Override public void onUp(boolean followedByLongPress) {}
+        @Override public void onSingleTapUp(int index) {}
+        @Override public void onLongTap(int index) {}
+        @Override public void onScrollPositionChanged(int position, int total) {}
+    }
+
+    public static interface SlotRenderer {
+        public void prepareDrawing();
+        public void onVisibleRangeChanged(int visibleStart, int visibleEnd);
+        public void onSlotSizeChanged(int width, int height);
+        public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height);
     }
 
     private final GestureDetector mGestureDetector;
@@ -62,21 +67,9 @@
     private Listener mListener;
     private UserInteractionListener mUIListener;
 
-    // Use linked hash map to keep the rendering order
-    private final HashMap<DisplayItem, ItemEntry> mItems =
-            new HashMap<DisplayItem, ItemEntry>();
-
-    public LinkedNode.List<ItemEntry> mItemList = LinkedNode.newList();
-
-    // This is used for multipass rendering
-    private ArrayList<ItemEntry> mCurrentItems = new ArrayList<ItemEntry>();
-    private ArrayList<ItemEntry> mNextItems = new ArrayList<ItemEntry>();
-
     private boolean mMoreAnimation = false;
-    private MyAnimation mAnimation = null;
-    private final Position mTempPosition = new Position();
+    private SlotAnimation mAnimation = null;
     private final Layout mLayout = new Layout();
-    private PositionProvider mPositions;
     private int mStartIndex = INDEX_NONE;
 
     // whether the down action happened while the view is scrolling.
@@ -84,15 +77,31 @@
     private int mOverscrollEffect = OVERSCROLL_3D;
     private final Handler mHandler;
 
+    private SlotRenderer mRenderer;
+
+    private int[] mRequestRenderSlots = new int[16];
+
     public static final int OVERSCROLL_3D = 0;
     public static final int OVERSCROLL_SYSTEM = 1;
     public static final int OVERSCROLL_NONE = 2;
 
-    public SlotView(Context context) {
-        mGestureDetector =
-                new GestureDetector(context, new MyGestureListener());
-        mScroller = new ScrollerHelper(context);
-        mHandler = new Handler(context.getMainLooper());
+    // to prevent allocating memory
+    private final Rect mTempRect = new Rect();
+
+    public SlotView(GalleryActivity activity, Spec spec) {
+        mGestureDetector = new GestureDetector(
+                (Context) activity, new MyGestureListener());
+        mScroller = new ScrollerHelper((Context) activity);
+        mHandler = new SynchronizedHandler(activity.getGLRoot());
+        setSlotSpec(spec);
+    }
+
+    public void setSlotRenderer(SlotRenderer slotDrawer) {
+        mRenderer = slotDrawer;
+        if (mRenderer != null) {
+            mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight);
+            mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd());
+        }
     }
 
     public void setCenterIndex(int index) {
@@ -100,7 +109,7 @@
         if (index < 0 || index >= slotCount) {
             return;
         }
-        Rect rect = mLayout.getSlotRect(index);
+        Rect rect = mLayout.getSlotRect(index, mTempRect);
         int position = WIDE
                 ? (rect.left + rect.right - getWidth()) / 2
                 : (rect.top + rect.bottom - getHeight()) / 2;
@@ -108,7 +117,7 @@
     }
 
     public void makeSlotVisible(int index) {
-        Rect rect = mLayout.getSlotRect(index);
+        Rect rect = mLayout.getSlotRect(index, mTempRect);
         int visibleBegin = WIDE ? mScrollX : mScrollY;
         int visibleLength = WIDE ? getWidth() : getHeight();
         int visibleEnd = visibleBegin + visibleLength;
@@ -143,11 +152,6 @@
     }
 
     @Override
-    public boolean removeComponent(GLView view) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
     protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
         if (!changeSize) return;
 
@@ -158,35 +162,21 @@
                 (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
         mLayout.setSize(r - l, b - t);
         makeSlotVisible(visibleIndex);
-
-        onLayoutChanged(r - l, b - t);
         if (mOverscrollEffect == OVERSCROLL_3D) {
             mPaper.setSize(r - l, b - t);
         }
     }
 
-    protected void onLayoutChanged(int width, int height) {
-    }
-
-    public void startTransition(PositionProvider position) {
-        mPositions = position;
-        mAnimation = new MyAnimation();
+    public void startScatteringAnimation(RelativePosition position) {
+        mAnimation = new ScatteringAnimation(position);
         mAnimation.start();
-        if (mItems.size() != 0) invalidate();
+        if (mLayout.mSlotCount != 0) invalidate();
     }
 
-    public void savePositions(PositionRepository repository) {
-        repository.clear();
-        LinkedNode.List<ItemEntry> list = mItemList;
-        ItemEntry entry = list.getFirst();
-        Position position = new Position();
-        while (entry != null) {
-            position.set(entry.target);
-            position.x -= mScrollX;
-            position.y -= mScrollY;
-            repository.putPosition(entry.item.getIdentity(), position);
-            entry = list.nextOf(entry);
-        }
+    public void startRisingAnimation() {
+        mAnimation = new RisingAnimation();
+        mAnimation.start();
+        if (mLayout.mSlotCount != 0) invalidate();
     }
 
     private void updateScrollPosition(int position, boolean force) {
@@ -205,20 +195,8 @@
         mListener.onScrollPositionChanged(newPosition, limit);
     }
 
-    public void putDisplayItem(Position target, Position base, DisplayItem item) {
-        item.setBox(mLayout.getSlotWidth(), mLayout.getSlotHeight());
-        ItemEntry entry = new ItemEntry(item, target, base);
-        mItemList.insertLast(entry);
-        mItems.put(item, entry);
-    }
-
-    public void removeDisplayItem(DisplayItem item) {
-        ItemEntry entry = mItems.remove(item);
-        if (entry != null) entry.remove();
-    }
-
     public Rect getSlotRect(int slotIndex) {
-        return mLayout.getSlotRect(slotIndex);
+        return mLayout.getSlotRect(slotIndex, new Rect());
     }
 
     @Override
@@ -251,12 +229,23 @@
         mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
     }
 
+    private static int[] expandIntArray(int array[], int capacity) {
+        while (array.length < capacity) {
+            array = new int[array.length * 2];
+        }
+        return array;
+    }
+
     @Override
     protected void render(GLCanvas canvas) {
         super.render(canvas);
 
-        long currentTimeMillis = canvas.currentAnimationTimeMillis();
-        boolean more = mScroller.advanceAnimation(currentTimeMillis);
+        if (mRenderer == null) return;
+        mRenderer.prepareDrawing();
+
+        long animTime = AnimationTime.get();
+        boolean more = mScroller.advanceAnimation(animTime);
+        more |= mLayout.advanceAnimation(animTime);
         int oldX = mScrollX;
         updateScrollPosition(mScroller.getPosition(), false);
 
@@ -279,51 +268,34 @@
 
         more |= paperActive;
 
-        float interpolate = 1f;
         if (mAnimation != null) {
-            more |= mAnimation.calculate(currentTimeMillis);
-            interpolate = mAnimation.value;
+            more |= mAnimation.calculate(animTime);
         }
 
-        if (WIDE) {
-            canvas.translate(-mScrollX, 0, 0);
-        } else {
-            canvas.translate(0, -mScrollY, 0);
+        canvas.translate(-mScrollX, -mScrollY);
+
+        int requestCount = 0;
+        int requestedSlot[] = expandIntArray(mRequestRenderSlots,
+                mLayout.mVisibleEnd - mLayout.mVisibleStart);
+
+        for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) {
+            int r = renderItem(canvas, i, 0, paperActive);
+            if ((r & RENDER_MORE_FRAME) != 0) more = true;
+            if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i;
         }
 
-        LinkedNode.List<ItemEntry> list = mItemList;
-        for (ItemEntry entry = list.getLast(); entry != null;) {
-            int r = renderItem(canvas, entry, interpolate, 0, paperActive);
-            if ((r & DisplayItem.RENDER_MORE_PASS) != 0) {
-                mCurrentItems.add(entry);
+        for (int pass = 1; requestCount != 0; ++pass) {
+            int newCount = 0;
+            for (int i = 0; i < requestCount; ++i) {
+                int r = renderItem(canvas,
+                        requestedSlot[i], pass, paperActive);
+                if ((r & RENDER_MORE_FRAME) != 0) more = true;
+                if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i;
             }
-            more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0);
-            entry = list.previousOf(entry);
+            requestCount = newCount;
         }
 
-        int pass = 1;
-        while (!mCurrentItems.isEmpty()) {
-            for (int i = 0, n = mCurrentItems.size(); i < n; i++) {
-                ItemEntry entry = mCurrentItems.get(i);
-                int r = renderItem(canvas, entry, interpolate, pass, paperActive);
-                if ((r & DisplayItem.RENDER_MORE_PASS) != 0) {
-                    mNextItems.add(entry);
-                }
-                more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0);
-            }
-            mCurrentItems.clear();
-            // swap mNextItems with mCurrentItems
-            ArrayList<ItemEntry> tmp = mNextItems;
-            mNextItems = mCurrentItems;
-            mCurrentItems = tmp;
-            pass += 1;
-        }
-
-        if (WIDE) {
-            canvas.translate(mScrollX, 0, 0);
-        } else {
-            canvas.translate(0, mScrollY, 0);
-        }
+        canvas.translate(mScrollX, mScrollY);
 
         if (more) invalidate();
 
@@ -339,59 +311,64 @@
         mMoreAnimation = more;
     }
 
-    private int renderItem(GLCanvas canvas, ItemEntry entry,
-            float interpolate, int pass, boolean paperActive) {
+    private int renderItem(
+            GLCanvas canvas, int index, int pass, boolean paperActive) {
         canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
-        Position position = entry.target;
-        if (mPositions != null) {
-            position = mTempPosition;
-            position.set(entry.target);
-            position.x -= mScrollX;
-            position.y -= mScrollY;
-            Position source = mPositions
-                    .getPosition(entry.item.getIdentity(), position);
-            source.x += mScrollX;
-            source.y += mScrollY;
-            position = mTempPosition;
-            Position.interpolate(
-                    source, entry.target, position, interpolate);
-        }
-        canvas.multiplyAlpha(position.alpha);
+        Rect rect = mLayout.getSlotRect(index, mTempRect);
         if (paperActive) {
-            canvas.multiplyMatrix(mPaper.getTransform(
-                    position, entry.base, mScrollX, mScrollY), 0);
+            canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0);
         } else {
-            canvas.translate(position.x, position.y, position.z);
+            canvas.translate(rect.left, rect.top, 0);
         }
-        canvas.rotate(position.theta, 0, 0, 1);
-        int more = entry.item.render(canvas, pass);
+        if (mAnimation != null && mAnimation.isActive()) {
+            mAnimation.apply(canvas, index, rect);
+        }
+        int result = mRenderer.renderSlot(
+                canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top);
         canvas.restore();
-        return more;
+        return result;
     }
 
-    public static class MyAnimation extends Animation {
-        public float value;
+    public static abstract class SlotAnimation extends Animation {
+        protected float mProgress = 0;
 
-        public MyAnimation() {
+        public SlotAnimation() {
             setInterpolator(new DecelerateInterpolator(4));
             setDuration(1500);
         }
 
         @Override
         protected void onCalculate(float progress) {
-            value = progress;
+            mProgress = progress;
+        }
+
+        abstract public void apply(GLCanvas canvas, int slotIndex, Rect target);
+    }
+
+    public static class RisingAnimation extends SlotAnimation {
+        private static final int RISING_DISTANCE = 128;
+
+        @Override
+        public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+            canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress));
         }
     }
 
-    private static class ItemEntry extends LinkedNode {
-        public DisplayItem item;
-        public Position target;
-        public Position base;
+    public static class ScatteringAnimation extends SlotAnimation {
+        private int PHOTO_DISTANCE = 1000;
+        private RelativePosition mCenter;
 
-        public ItemEntry(DisplayItem item, Position target, Position base) {
-            this.item = item;
-            this.target = target;
-            this.base = base;
+        public ScatteringAnimation(RelativePosition center) {
+            mCenter = center;
+        }
+
+        @Override
+        public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+            canvas.translate(
+                    (mCenter.getX() - target.centerX()) * (1 - mProgress),
+                    (mCenter.getY() - target.centerY()) * (1 - mProgress),
+                    slotIndex * PHOTO_DISTANCE * (1 - mProgress));
+            canvas.setAlpha(mProgress);
         }
     }
 
@@ -413,24 +390,9 @@
         public int rowsLand = -1;
         public int rowsPort = -1;
         public int slotGap = -1;
-
-        static Spec newWithSize(int width, int height) {
-            Spec s = new Spec();
-            s.slotWidth = width;
-            s.slotHeight = height;
-            return s;
-        }
-
-        static Spec newWithRows(int rowsLand, int rowsPort, int slotGap) {
-            Spec s = new Spec();
-            s.rowsLand = rowsLand;
-            s.rowsPort = rowsPort;
-            s.slotGap = slotGap;
-            return s;
-        }
     }
 
-    public static class Layout {
+    public class Layout {
 
         private int mVisibleStart;
         private int mVisibleEnd;
@@ -449,22 +411,28 @@
         private int mContentLength;
         private int mScrollPosition;
 
-        private int mVerticalPadding;
-        private int mHorizontalPadding;
+        private IntegerAnimation mVerticalPadding = new IntegerAnimation();
+        private IntegerAnimation mHorizontalPadding = new IntegerAnimation();
 
         public void setSlotSpec(Spec spec) {
             mSpec = spec;
         }
 
         public boolean setSlotCount(int slotCount) {
+            if (slotCount == mSlotCount) return false;
+            if (mSlotCount != 0) {
+                mHorizontalPadding.setEnabled(true);
+                mVerticalPadding.setEnabled(true);
+            }
             mSlotCount = slotCount;
-            int hPadding = mHorizontalPadding;
-            int vPadding = mVerticalPadding;
+            int hPadding = mHorizontalPadding.getTarget();
+            int vPadding = mVerticalPadding.getTarget();
             initLayoutParameters();
-            return vPadding != mVerticalPadding || hPadding != mHorizontalPadding;
+            return vPadding != mVerticalPadding.getTarget()
+                    || hPadding != mHorizontalPadding.getTarget();
         }
 
-        public Rect getSlotRect(int index) {
+        public Rect getSlotRect(int index, Rect rect) {
             int col, row;
             if (WIDE) {
                 col = index / mUnitCount;
@@ -474,9 +442,10 @@
                 col = index - row * mUnitCount;
             }
 
-            int x = mHorizontalPadding + col * (mSlotWidth + mSlotGap);
-            int y = mVerticalPadding + row * (mSlotHeight + mSlotGap);
-            return new Rect(x, y, x + mSlotWidth, y + mSlotHeight);
+            int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap);
+            int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap);
+            rect.set(x, y, x + mSlotWidth, y + mSlotHeight);
+            return rect;
         }
 
         public int getSlotWidth() {
@@ -487,10 +456,6 @@
             return mSlotHeight;
         }
 
-        public int getContentLength() {
-            return mContentLength;
-        }
-
         // Calculate
         // (1) mUnitCount: the number of slots we can fit into one column (or row).
         // (2) mContentLength: the width (or height) we need to display all the
@@ -539,15 +504,19 @@
                 mSlotWidth = mSlotHeight;
             }
 
+            if (mRenderer != null) {
+                mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight);
+            }
+
             int[] padding = new int[2];
             if (WIDE) {
                 initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
-                mVerticalPadding = padding[0];
-                mHorizontalPadding = padding[1];
+                mVerticalPadding.startAnimateTo(padding[0]);
+                mHorizontalPadding.startAnimateTo(padding[1]);
             } else {
                 initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
-                mVerticalPadding = padding[1];
-                mHorizontalPadding = padding[0];
+                mVerticalPadding.startAnimateTo(padding[1]);
+                mHorizontalPadding.startAnimateTo(padding[0]);
             }
             updateVisibleSlotRange();
         }
@@ -592,6 +561,9 @@
             } else {
                 mVisibleStart = mVisibleEnd = 0;
             }
+            if (mRenderer != null) {
+                mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd);
+            }
         }
 
         public int getVisibleStart() {
@@ -606,8 +578,8 @@
             int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0);
             int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition);
 
-            absoluteX -= mHorizontalPadding;
-            absoluteY -= mVerticalPadding;
+            absoluteX -= mHorizontalPadding.get();
+            absoluteY -= mVerticalPadding.get();
 
             if (absoluteX < 0 || absoluteY < 0) {
                 return INDEX_NONE;
@@ -643,28 +615,38 @@
             int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
             return limit <= 0 ? 0 : limit;
         }
+
+        public boolean advanceAnimation(long animTime) {
+            // use '|' to make sure both sides will be executed
+            return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime);
+        }
     }
 
-    private class MyGestureListener implements
-            GestureDetector.OnGestureListener {
+    private class MyGestureListener implements GestureDetector.OnGestureListener {
         private boolean isDown;
 
         // We call the listener's onDown() when our onShowPress() is called and
         // call the listener's onUp() when we receive any further event.
         @Override
         public void onShowPress(MotionEvent e) {
-            if (isDown) return;
-            int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
-            if (index != INDEX_NONE) {
-                isDown = true;
-                mListener.onDown(index);
+            GLRoot root = getGLRoot();
+            root.lockRenderThread();
+            try {
+                if (isDown) return;
+                int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+                if (index != INDEX_NONE) {
+                    isDown = true;
+                    mListener.onDown(index);
+                }
+            } finally {
+                root.unlockRenderThread();
             }
         }
 
-        private void cancelDown() {
+        private void cancelDown(boolean byLongPress) {
             if (!isDown) return;
             isDown = false;
-            mListener.onUp();
+            mListener.onUp(byLongPress);
         }
 
         @Override
@@ -675,7 +657,7 @@
         @Override
         public boolean onFling(MotionEvent e1,
                 MotionEvent e2, float velocityX, float velocityY) {
-            cancelDown();
+            cancelDown(false);
             int scrollLimit = mLayout.getScrollLimit();
             if (scrollLimit == 0) return false;
             float velocity = WIDE ? velocityX : velocityY;
@@ -688,7 +670,7 @@
         @Override
         public boolean onScroll(MotionEvent e1,
                 MotionEvent e2, float distanceX, float distanceY) {
-            cancelDown();
+            cancelDown(false);
             float distance = WIDE ? distanceX : distanceY;
             int overDistance = mScroller.startScroll(
                     Math.round(distance), 0, mLayout.getScrollLimit());
@@ -701,7 +683,7 @@
 
         @Override
         public boolean onSingleTapUp(MotionEvent e) {
-            cancelDown();
+            cancelDown(false);
             if (mDownInScrolling) return true;
             int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
             if (index != INDEX_NONE) mListener.onSingleTapUp(index);
@@ -710,7 +692,7 @@
 
         @Override
         public void onLongPress(MotionEvent e) {
-            cancelDown();
+            cancelDown(true);
             if (mDownInScrolling) return;
             lockRendering();
             try {
@@ -735,7 +717,8 @@
             setCenterIndex(mStartIndex);
             mStartIndex = INDEX_NONE;
         }
-        updateScrollPosition(WIDE ? mScrollX : mScrollY, true);
+        // Reset the scroll position to avoid scrolling over the updated limit.
+        setScrollPosition(WIDE ? mScrollX : mScrollY);
         return changed;
     }
 
@@ -754,4 +737,42 @@
     public int getScrollY() {
         return mScrollY;
     }
+
+    private static class IntegerAnimation extends Animation {
+        private int mTarget;
+        private int mCurrent = 0;
+        private int mFrom = 0;
+        private boolean mEnabled = false;
+
+        public void setEnabled(boolean enabled) {
+            mEnabled = enabled;
+        }
+
+        public void startAnimateTo(int target) {
+            if (!mEnabled) {
+                mTarget = mCurrent = target;
+                return;
+            }
+            if (target == mTarget) return;
+
+            mFrom = mCurrent;
+            mTarget = target;
+            setDuration(180);
+            start();
+        }
+
+        public int get() {
+            return mCurrent;
+        }
+
+        public int getTarget() {
+            return mTarget;
+        }
+
+        @Override
+        protected void onCalculate(float progress) {
+            mCurrent = Math.round(mFrom + progress * (mTarget - mFrom));
+            if (progress == 1f) mEnabled = false;
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/ui/StaticBackground.java b/src/com/android/gallery3d/ui/StaticBackground.java
deleted file mode 100644
index 08c55c3..0000000
--- a/src/com/android/gallery3d/ui/StaticBackground.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import android.content.Context;
-
-public class StaticBackground extends GLView {
-
-    private Context mContext;
-    private int mLandscapeResource;
-    private int mPortraitResource;
-
-    private BasicTexture mBackground;
-    private boolean mIsLandscape = false;
-
-    public StaticBackground(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
-        setOrientation(getWidth() >= getHeight());
-    }
-
-    private void setOrientation(boolean isLandscape) {
-        if (mIsLandscape == isLandscape) return;
-        mIsLandscape = isLandscape;
-        if (mBackground != null) mBackground.recycle();
-        mBackground = new ResourceTexture(
-                mContext, mIsLandscape ? mLandscapeResource : mPortraitResource);
-        invalidate();
-    }
-
-    public void setImage(int landscapeId, int portraitId) {
-        mLandscapeResource = landscapeId;
-        mPortraitResource = portraitId;
-        if (mBackground != null) mBackground.recycle();
-        mBackground = new ResourceTexture(
-                mContext, mIsLandscape ? landscapeId : portraitId);
-        invalidate();
-    }
-
-    @Override
-    protected void render(GLCanvas canvas) {
-        //mBackground.draw(canvas, 0, 0, getWidth(), getHeight());
-        canvas.fillRect(0, 0, getWidth(), getHeight(), 0xFF000000);
-    }
-}
diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java
index f576c01..2db2de4 100644
--- a/src/com/android/gallery3d/ui/StringTexture.java
+++ b/src/com/android/gallery3d/ui/StringTexture.java
@@ -23,6 +23,7 @@
 import android.graphics.Typeface;
 import android.text.TextPaint;
 import android.text.TextUtils;
+import android.util.FloatMath;
 
 // StringTexture is a texture shows the content of a specified String.
 //
@@ -69,7 +70,7 @@
 
     private static StringTexture newInstance(String text, TextPaint paint) {
         FontMetricsInt metrics = paint.getFontMetricsInt();
-        int width = (int) Math.ceil(paint.measureText(text));
+        int width = (int) FloatMath.ceil(paint.measureText(text));
         int height = metrics.bottom - metrics.top;
         // The texture size needs to be at least 1x1.
         if (width <= 0) width = 1;
diff --git a/src/com/android/gallery3d/ui/StripDrawer.java b/src/com/android/gallery3d/ui/StripDrawer.java
deleted file mode 100644
index 5941392..0000000
--- a/src/com/android/gallery3d/ui/StripDrawer.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.ui;
-
-import com.android.gallery3d.R;
-import com.android.gallery3d.data.Path;
-
-import android.content.Context;
-import android.graphics.Rect;
-
-public class StripDrawer extends SelectionDrawer {
-    private NinePatchTexture mFramePressed;
-    private NinePatchTexture mFocusBox;
-    private Rect mFocusBoxPadding;
-    private Path mPressedPath;
-
-    public StripDrawer(Context context) {
-        mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
-        mFocusBox = new NinePatchTexture(context, R.drawable.thumb_selected);
-        mFocusBoxPadding = mFocusBox.getPaddings();
-    }
-
-    public void setPressedPath(Path path) {
-        mPressedPath = path;
-    }
-
-    private boolean isPressedPath(Path path) {
-        return path != null && path == mPressedPath;
-    }
-
-    @Override
-    public void prepareDrawing() {
-    }
-
-    @Override
-    public void draw(GLCanvas canvas, Texture content,
-            int width, int height, int rotation, Path path,
-            int dataSourceType, int mediaType, boolean isPanorama,
-            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
-
-        int x = -width / 2;
-        int y = -height / 2;
-
-        drawWithRotation(canvas, content, x, y, width, height, rotation);
-
-        if (isPressedPath(path)) {
-            drawFrame(canvas, mFramePressed, x, y, width, height);
-        }
-    }
-
-    @Override
-    public void drawFocus(GLCanvas canvas, int width, int height) {
-        int x = -width / 2;
-        int y = -height / 2;
-        Rect p = mFocusBoxPadding;
-        mFocusBox.draw(canvas, x - p.left, y - p.top,
-                width + p.left + p.right, height + p.top + p.bottom);
-    }
-}
diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
new file mode 100644
index 0000000..5f45f16
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.opengl.GLES11Ext;
+
+public abstract class SurfaceTextureScreenNail implements ScreenNail,
+        SurfaceTexture.OnFrameAvailableListener {
+    private static final String TAG = "SurfaceTextureScreenNail";
+    protected ExtTexture mExtTexture;
+    private SurfaceTexture mSurfaceTexture;
+    private int mWidth, mHeight;
+    private float[] mTransform = new float[16];
+    private boolean mHasTexture = false;
+
+    public SurfaceTextureScreenNail() {
+    }
+
+    public void acquireSurfaceTexture() {
+        mExtTexture = new ExtTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
+        mExtTexture.setSize(mWidth, mHeight);
+        mSurfaceTexture = new SurfaceTexture(mExtTexture.getId());
+        mSurfaceTexture.setDefaultBufferSize(mWidth, mHeight);
+        mSurfaceTexture.setOnFrameAvailableListener(this);
+        synchronized (this) {
+            mHasTexture = true;
+        }
+    }
+
+    public SurfaceTexture getSurfaceTexture() {
+        return mSurfaceTexture;
+    }
+
+    public void releaseSurfaceTexture() {
+        synchronized (this) {
+            mHasTexture = false;
+        }
+        mExtTexture.recycle();
+        mExtTexture = null;
+        mSurfaceTexture.release();
+        mSurfaceTexture = null;
+    }
+
+    public void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        synchronized (this) {
+            if (!mHasTexture) return;
+            mSurfaceTexture.updateTexImage();
+            mSurfaceTexture.getTransformMatrix(mTransform);
+
+            // Flip vertically.
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+            int cx = x + width / 2;
+            int cy = y + height / 2;
+            canvas.translate(cx, cy);
+            canvas.scale(1, -1, 1);
+            canvas.translate(-cx, -cy);
+            canvas.drawTexture(mExtTexture, mTransform, x, y, width, height);
+            canvas.restore();
+        }
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    abstract public void noDraw();
+
+    @Override
+    abstract public void recycle();
+
+    @Override
+    abstract public void onFrameAvailable(SurfaceTexture surfaceTexture);
+}
diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java
index bd494a3..ba10357 100644
--- a/src/com/android/gallery3d/ui/SynchronizedHandler.java
+++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java
@@ -16,11 +16,11 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-
 import android.os.Handler;
 import android.os.Message;
 
+import com.android.gallery3d.common.Utils;
+
 public class SynchronizedHandler extends Handler {
 
     private final GLRoot mRoot;
diff --git a/src/com/android/gallery3d/ui/Texture.java b/src/com/android/gallery3d/ui/Texture.java
index 4d1749b..2c426f9 100644
--- a/src/com/android/gallery3d/ui/Texture.java
+++ b/src/com/android/gallery3d/ui/Texture.java
@@ -26,14 +26,12 @@
 // -- ColorTexture
 // -- FadeInTexture
 // -- BasicTexture
-//    -- RawTexture
 //    -- UploadedTexture
 //       -- BitmapTexture
 //       -- Tile
 //       -- ResourceTexture
 //          -- NinePatchTexture
 //       -- CanvasTexture
-//          -- DrawableTexture
 //          -- StringTexture
 //
 public interface Texture {
diff --git a/src/com/android/gallery3d/ui/TextureUploader.java b/src/com/android/gallery3d/ui/TextureUploader.java
new file mode 100644
index 0000000..714b097
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TextureUploader.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+
+public class TextureUploader implements OnGLIdleListener {
+    private static final int INIT_CAPACITY = 64;
+    private static final int QUOTA_PER_FRAME = 1;
+
+    private final ArrayDeque<UploadedTexture> mFgTextures =
+            new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
+    private final ArrayDeque<UploadedTexture> mBgTextures =
+            new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
+    private final GLRoot mGLRoot;
+    private transient boolean mIsQueued = false;
+
+    public TextureUploader(GLRoot root) {
+        mGLRoot = root;
+    }
+
+    public synchronized void clear() {
+        while (!mFgTextures.isEmpty()) {
+            mFgTextures.pop().setIsUploading(false);
+        }
+        while (!mBgTextures.isEmpty()) {
+            mBgTextures.pop().setIsUploading(false);
+        }
+    }
+
+    // caller should hold synchronized on "this"
+    private void queueSelfIfNeed() {
+        if (mIsQueued) return;
+        mIsQueued = true;
+        mGLRoot.addOnGLIdleListener(this);
+    }
+
+    public synchronized void addBgTexture(UploadedTexture t) {
+        if (t.isContentValid()) return;
+        mBgTextures.addLast(t);
+        t.setIsUploading(true);
+        queueSelfIfNeed();
+    }
+
+    public synchronized void addFgTexture(UploadedTexture t) {
+        if (t.isContentValid()) return;
+        mFgTextures.addLast(t);
+        t.setIsUploading(true);
+        queueSelfIfNeed();
+    }
+
+    private int upload(GLCanvas canvas, ArrayDeque<UploadedTexture> deque,
+            int uploadQuota, boolean isBackground) {
+        while (uploadQuota > 0) {
+            UploadedTexture t;
+            synchronized (this) {
+                if (deque.isEmpty()) break;
+                t = deque.removeFirst();
+                t.setIsUploading(false);
+                if (t.isContentValid()) continue;
+
+                // this has to be protected by the synchronized block
+                // to prevent the inner bitmap get recycled
+                t.updateContent(canvas);
+            }
+
+            // It will took some more time for a texture to be drawn for
+            // the first time.
+            // Thus, when scrolling, if a new column appears on screen,
+            // it may cause a UI jank even these textures are uploaded.
+            if (isBackground) t.draw(canvas, 0, 0);
+            --uploadQuota;
+        }
+        return uploadQuota;
+    }
+
+    @Override
+    public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+        int uploadQuota = QUOTA_PER_FRAME;
+        uploadQuota = upload(canvas, mFgTextures, uploadQuota, false);
+        if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender();
+        upload(canvas, mBgTextures, uploadQuota, true);
+        synchronized (this) {
+            mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty();
+            return mIsQueued;
+        }
+    }
+}
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
index 5c9f3f4..0c6086d 100644
--- a/src/com/android/gallery3d/ui/TileImageView.java
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -20,18 +20,18 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.FloatMath;
+import android.util.LongSparseArray;
 
 import com.android.gallery3d.app.GalleryContext;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BitmapPool;
 import com.android.gallery3d.data.DecodeUtils;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.ThreadPool;
 import com.android.gallery3d.util.ThreadPool.CancelListener;
 import com.android.gallery3d.util.ThreadPool.JobContext;
 
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 public class TileImageView extends GLView {
@@ -44,8 +44,12 @@
     // texture to avoid seams between tiles.
     private static final int TILE_SIZE = 254;
     private static final int TILE_BORDER = 1;
+    private static final int BITMAP_SIZE = TILE_SIZE + TILE_BORDER * 2;
     private static final int UPLOAD_LIMIT = 1;
 
+    private static final BitmapPool sTilePool =
+            new BitmapPool(BITMAP_SIZE, BITMAP_SIZE, 128);
+
     /*
      *  This is the tile state in the CPU side.
      *  Life of a Tile:
@@ -72,7 +76,7 @@
     private static final int STATE_RECYCLED = 0x40;
 
     private Model mModel;
-    protected BitmapTexture mBackupImage;
+    private ScreenNail mScreenNail;
     protected int mLevelCount;  // cache the value of mScaledBitmaps.length
 
     // The mLevel variable indicates which level of bitmap we should use.
@@ -80,7 +84,7 @@
     // a smaller scaled bitmap (The width and height of each scaled bitmap is
     // half size of the previous one). If the value is in [0, mLevelCount), we
     // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
-    // is mLevelCount, and that means we use mBackupTexture for display.
+    // is mLevelCount, and that means we use mScreenNail for display.
     private int mLevel = 0;
 
     // The offsets of the (left, top) of the upper-left tile to the (left, top)
@@ -94,12 +98,12 @@
     private final RectF mSourceRect = new RectF();
     private final RectF mTargetRect = new RectF();
 
-    private final HashMap<Long, Tile> mActiveTiles = new HashMap<Long, Tile>();
+    private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
 
     // The following three queue is guarded by TileImageView.this
-    private TileQueue mRecycledQueue = new TileQueue();
-    private TileQueue mUploadQueue = new TileQueue();
-    private TileQueue mDecodeQueue = new TileQueue();
+    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;
@@ -109,7 +113,6 @@
     protected int mCenterY;
     protected float mScale;
     protected int mRotation;
-    protected float mAlpha = 1.0f;
 
     // Temp variables to avoid memory allocation
     private final Rect mTileRange = new Rect();
@@ -118,12 +121,12 @@
     private final TileUploader mTileUploader = new TileUploader();
     private boolean mIsTextureFreed;
     private Future<Void> mTileDecoder;
-    private ThreadPool mThreadPool;
+    private final ThreadPool mThreadPool;
     private boolean mBackgroundTileUploaded;
 
     public static interface Model {
         public int getLevelCount();
-        public Bitmap getBackupImage();
+        public ScreenNail getScreenNail();
         public int getImageWidth();
         public int getImageHeight();
 
@@ -140,8 +143,7 @@
         //
         // The method would be called in another thread.
         public Bitmap getTile(int level, int x, int y, int tileSize,
-                int borderSize);
-        public boolean isFailedToLoad();
+                int borderSize, BitmapPool pool);
     }
 
     public TileImageView(GalleryContext context) {
@@ -154,31 +156,19 @@
         if (model != null) notifyModelInvalidated();
     }
 
-    private void updateBackupTexture(Bitmap backup) {
-        if (backup == null) {
-            if (mBackupImage != null) mBackupImage.recycle();
-            mBackupImage = null;
-        } else {
-            if (mBackupImage != null) {
-                if (mBackupImage.getBitmap() != backup) {
-                    mBackupImage.recycle();
-                    mBackupImage = new BitmapTexture(backup);
-                }
-            } else {
-                mBackupImage = new BitmapTexture(backup);
-            }
-        }
+    public void setScreenNail(ScreenNail s) {
+        mScreenNail = s;
     }
 
     public void notifyModelInvalidated() {
         invalidateTiles();
         if (mModel == null) {
-            mBackupImage = null;
+            mScreenNail = null;
             mImageWidth = 0;
             mImageHeight = 0;
             mLevelCount = 0;
         } else {
-            updateBackupTexture(mModel.getBackupImage());
+            setScreenNail(mModel.getScreenNail());
             mImageWidth = mModel.getImageWidth();
             mImageHeight = mModel.getImageHeight();
             mLevelCount = mModel.getLevelCount();
@@ -245,19 +235,20 @@
             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.
-        Iterator<Map.Entry<Long, Tile>>
-                iter = mActiveTiles.entrySet().iterator();
-        while (iter.hasNext()) {
-            Tile tile = iter.next().getValue();
-            int level = tile.mTileLevel;
-            if (level < fromLevel || level >= endLevel
-                    || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
-                iter.remove();
-                recycleTile(tile);
+            // 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);
+                }
             }
         }
 
@@ -277,7 +268,9 @@
         mDecodeQueue.clean();
         mUploadQueue.clean();
         // TODO disable decoder
-        for (Tile tile : mActiveTiles.values()) {
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile tile = mActiveTiles.valueAt(i);
             recycleTile(tile);
         }
         mActiveTiles.clear();
@@ -307,10 +300,10 @@
         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);
+        int left = (int) FloatMath.floor(cX - width / (2f * scale));
+        int top = (int) FloatMath.floor(cY - height / (2f * scale));
+        int right = (int) FloatMath.ceil(left + width / scale);
+        int bottom = (int) FloatMath.ceil(top + height / scale);
 
         // align the rectangle to tile boundary
         int size = TILE_SIZE << level;
@@ -347,8 +340,8 @@
     }
 
     public boolean setPosition(int centerX, int centerY, float scale, int rotation) {
-        if (mCenterX == centerX
-                && mCenterY == centerY && mScale == scale) return false;
+        if (mCenterX == centerX && mCenterY == centerY
+                && mScale == scale && mRotation == rotation) return false;
         mCenterX = centerX;
         mCenterY = centerY;
         mScale = scale;
@@ -358,13 +351,6 @@
         return true;
     }
 
-    public boolean setAlpha(float alpha) {
-        if (mAlpha == alpha) return false;
-        mAlpha = alpha;
-        invalidate();
-        return true;
-    }
-
     public void freeTextures() {
         mIsTextureFreed = true;
 
@@ -374,11 +360,13 @@
             mTileDecoder = null;
         }
 
-        for (Tile texture : mActiveTiles.values()) {
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile texture = mActiveTiles.valueAt(i);
             texture.recycle();
         }
-        mTileRange.set(0, 0, 0, 0);
         mActiveTiles.clear();
+        mTileRange.set(0, 0, 0, 0);
 
         synchronized (this) {
             mUploadQueue.clean();
@@ -389,7 +377,8 @@
                 tile = mRecycledQueue.pop();
             }
         }
-        updateBackupTexture(null);
+        setScreenNail(null);
+        sTilePool.clear();
     }
 
     public void prepareTextures() {
@@ -399,7 +388,7 @@
         if (mIsTextureFreed) {
             layoutTiles(mCenterX, mCenterY, mScale, mRotation);
             mIsTextureFreed = false;
-            updateBackupTexture(mModel != null ? mModel.getBackupImage() : null);
+            setScreenNail(mModel == null ? null : mModel.getScreenNail());
         }
     }
 
@@ -412,20 +401,22 @@
         int rotation = mRotation;
         int flags = 0;
         if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
-        if (mAlpha != 1.0f) flags |= GLCanvas.SAVE_FLAG_ALPHA;
 
         if (flags != 0) {
             canvas.save(flags);
             if (rotation != 0) {
                 int centerX = getWidth() / 2, centerY = getHeight() / 2;
-                canvas.translate(centerX, centerY, 0);
+                canvas.translate(centerX, centerY);
                 canvas.rotate(rotation, 0, 0, 1);
-                canvas.translate(-centerX, -centerY, 0);
+                canvas.translate(-centerX, -centerY);
             }
-            if (mAlpha != 1.0f) canvas.multiplyAlpha(mAlpha);
         }
         try {
-            if (level != mLevelCount) {
+            if (level != mLevelCount && !isScreenNailAnimating()) {
+                if (mScreenNail != null) {
+                    mScreenNail.noDraw();
+                }
+
                 int size = (TILE_SIZE << level);
                 float length = size * mScale;
                 Rect r = mTileRange;
@@ -437,10 +428,13 @@
                         drawTile(canvas, tx, ty, level, x, y, length);
                     }
                 }
-            } else if (mBackupImage != null) {
-                mBackupImage.draw(canvas, mOffsetX, mOffsetY,
+            } else if (mScreenNail != null) {
+                mScreenNail.draw(canvas, mOffsetX, mOffsetY,
                         Math.round(mImageWidth * mScale),
                         Math.round(mImageHeight * mScale));
+                if (isScreenNailAnimating()) {
+                    invalidate();
+                }
             }
         } finally {
             if (flags != 0) canvas.restore();
@@ -453,10 +447,17 @@
         }
     }
 
+    private boolean isScreenNailAnimating() {
+        return (mScreenNail instanceof BitmapScreenNail)
+                && ((BitmapScreenNail) mScreenNail).isAnimating();
+    }
+
     private void uploadBackgroundTiles(GLCanvas canvas) {
         mBackgroundTileUploaded = true;
-        for (Tile tile : mActiveTiles.values()) {
-            if (!tile.isContentValid(canvas)) queueForDecode(tile);
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile tile = mActiveTiles.valueAt(i);
+            if (!tile.isContentValid()) queueForDecode(tile);
         }
     }
 
@@ -485,7 +486,10 @@
         synchronized (this) {
             if (tile.mTileState == STATE_RECYCLING) {
                 tile.mTileState = STATE_RECYCLED;
-                tile.mDecodedTile = null;
+                if (tile.mDecodedTile != null) {
+                    sTilePool.recycle(tile.mDecodedTile);
+                    tile.mDecodedTile = null;
+                }
                 mRecycledQueue.push(tile);
                 return false;
             }
@@ -510,12 +514,15 @@
             return;
         }
         tile.mTileState = STATE_RECYCLED;
-        tile.mDecodedTile = null;
+        if (tile.mDecodedTile != null) {
+            sTilePool.recycle(tile.mDecodedTile);
+            tile.mDecodedTile = null;
+        }
         mRecycledQueue.push(tile);
     }
 
     private void activateTile(int x, int y, int level) {
-        Long key = makeTileKey(x, y, level);
+        long key = makeTileKey(x, y, level);
         Tile tile = mActiveTiles.get(key);
         if (tile != null) {
             if (tile.mTileState == STATE_IN_QUEUE) {
@@ -531,18 +538,19 @@
         return mActiveTiles.get(makeTileKey(x, y, level));
     }
 
-    private static Long makeTileKey(int x, int y, int level) {
+    private static long makeTileKey(int x, int y, int level) {
         long result = x;
         result = (result << 16) | y;
         result = (result << 16) | level;
-        return Long.valueOf(result);
+        return result;
     }
 
     private class TileUploader implements GLRoot.OnGLIdleListener {
         AtomicBoolean mActive = new AtomicBoolean(false);
 
         @Override
-        public boolean onGLIdle(GLRoot root, GLCanvas canvas) {
+        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+            if (renderRequested) return false;
             int quota = UPLOAD_LIMIT;
             Tile tile;
             while (true) {
@@ -550,7 +558,7 @@
                     tile = mUploadQueue.pop();
                 }
                 if (tile == null || quota <= 0) break;
-                if (!tile.isContentValid(canvas)) {
+                if (!tile.isContentValid()) {
                     Utils.assertTrue(tile.mTileState == STATE_DECODED);
                     tile.updateContent(canvas);
                     --quota;
@@ -572,7 +580,7 @@
 
         Tile tile = getTile(tx, ty, level);
         if (tile != null) {
-            if (!tile.isContentValid(canvas)) {
+            if (!tile.isContentValid()) {
                 if (tile.mTileState == STATE_DECODED) {
                     if (mUploadQuota > 0) {
                         --mUploadQuota;
@@ -587,14 +595,13 @@
             }
             if (drawTile(tile, canvas, source, target)) return;
         }
-        if (mBackupImage != null) {
-            BasicTexture backup = mBackupImage;
+        if (mScreenNail != null) {
             int size = TILE_SIZE << level;
-            float scaleX = (float) backup.getWidth() / mImageWidth;
-            float scaleY = (float) backup.getHeight() / mImageHeight;
+            float scaleX = (float) mScreenNail.getWidth() / mImageWidth;
+            float scaleY = (float) mScreenNail.getHeight() / mImageHeight;
             source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
                     (ty + size) * scaleY);
-            canvas.drawTexture(backup, source, target);
+            mScreenNail.draw(canvas, source, target);
         }
     }
 
@@ -602,7 +609,7 @@
     static boolean drawTile(
             Tile tile, GLCanvas canvas, RectF source, RectF target) {
         while (true) {
-            if (tile.isContentValid(canvas)) {
+            if (tile.isContentValid()) {
                 // offset source rectangle for the texture border.
                 source.offset(TILE_BORDER, TILE_BORDER);
                 canvas.drawTexture(tile, source, target);
@@ -631,12 +638,12 @@
     }
 
     private class Tile extends UploadedTexture {
-        int mX;
-        int mY;
-        int mTileLevel;
-        Tile mNext;
-        Bitmap mDecodedTile;
-        volatile int mTileState = STATE_ACTIVATED;
+        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;
@@ -646,7 +653,7 @@
 
         @Override
         protected void onFreeBitmap(Bitmap bitmap) {
-            bitmap.recycle();
+            sTilePool.recycle(bitmap);
         }
 
         boolean decode() {
@@ -654,7 +661,7 @@
             // by (1 << mTilelevel) from a region in the original image.
             try {
                 mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile(
-                        mTileLevel, mX, mY, TILE_SIZE, TILE_BORDER));
+                        mTileLevel, mX, mY, TILE_SIZE, TILE_BORDER, sTilePool));
             } catch (Throwable t) {
                 Log.w(TAG, "fail to decode tile", t);
             }
@@ -664,6 +671,13 @@
         @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) + TILE_BORDER;
+            int bottomEdge = ((mImageHeight - mY) >> mTileLevel) + TILE_BORDER;
+            setSize(Math.min(BITMAP_SIZE, rightEdge), Math.min(BITMAP_SIZE, bottomEdge));
+
             Bitmap bitmap = mDecodedTile;
             mDecodedTile = null;
             mTileState = STATE_ACTIVATED;
diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
index be255d2..0b4ac03 100644
--- a/src/com/android/gallery3d/ui/TileImageViewAdapter.java
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -20,25 +20,27 @@
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
-import android.graphics.Canvas;
 import android.graphics.Rect;
 
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BitmapPool;
 
 public class TileImageViewAdapter implements TileImageView.Model {
     private static final String TAG = "TileImageViewAdapter";
+    protected ScreenNail mScreenNail;
+    protected boolean mOwnScreenNail;
     protected BitmapRegionDecoder mRegionDecoder;
     protected int mImageWidth;
     protected int mImageHeight;
-    protected Bitmap mBackupImage;
     protected int mLevelCount;
-    protected boolean mFailedToLoad;
 
     public TileImageViewAdapter() {
     }
 
-    public TileImageViewAdapter(Bitmap backup, BitmapRegionDecoder regionDecoder) {
-        mBackupImage = Utils.checkNotNull(backup);
+    public TileImageViewAdapter(
+            Bitmap bitmap, BitmapRegionDecoder regionDecoder) {
+        Utils.checkNotNull(bitmap);
+        updateScreenNail(new BitmapScreenNail(bitmap), true);
         mRegionDecoder = regionDecoder;
         mImageWidth = regionDecoder.getWidth();
         mImageHeight = regionDecoder.getHeight();
@@ -46,21 +48,38 @@
     }
 
     public synchronized void clear() {
-        mBackupImage = null;
+        updateScreenNail(null, false);
         mImageWidth = 0;
         mImageHeight = 0;
         mLevelCount = 0;
         mRegionDecoder = null;
-        mFailedToLoad = false;
     }
 
-    public synchronized void setBackupImage(Bitmap backup, int width, int height) {
-        mBackupImage = Utils.checkNotNull(backup);
+    public synchronized void setScreenNail(Bitmap bitmap, int width, int height) {
+        Utils.checkNotNull(bitmap);
+        updateScreenNail(new BitmapScreenNail(bitmap), true);
         mImageWidth = width;
         mImageHeight = height;
         mRegionDecoder = null;
         mLevelCount = 0;
-        mFailedToLoad = false;
+    }
+
+    public synchronized void setScreenNail(
+            ScreenNail screenNail, int width, int height) {
+        Utils.checkNotNull(screenNail);
+        updateScreenNail(screenNail, false);
+        mImageWidth = width;
+        mImageHeight = height;
+        mRegionDecoder = null;
+        mLevelCount = 0;
+    }
+
+    private void updateScreenNail(ScreenNail screenNail, boolean own) {
+        if (mScreenNail != null && mOwnScreenNail) {
+            mScreenNail.recycle();
+        }
+        mScreenNail = screenNail;
+        mOwnScreenNail = own;
     }
 
     public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
@@ -68,79 +87,80 @@
         mImageWidth = decoder.getWidth();
         mImageHeight = decoder.getHeight();
         mLevelCount = calculateLevelCount();
-        mFailedToLoad = false;
     }
 
     private int calculateLevelCount() {
         return Math.max(0, Utils.ceilLog2(
-                (float) mImageWidth / mBackupImage.getWidth()));
+                (float) mImageWidth / mScreenNail.getWidth()));
     }
 
+    // Gets a sub image on a rectangle of the current photo. For example,
+    // getTile(1, 50, 50, 100, 3, pool) means to get the region located
+    // at (50, 50) with sample level 1 (ie, down sampled by 2^1) and the
+    // target tile size (after sampling) 100 with border 3.
+    //
+    // From this spec, we can infer the actual tile size to be
+    // 100 + 3x2 = 106, and the size of the region to be extracted from the
+    // photo to be 200 with border 6.
+    //
+    // As a result, we should decode region (50-6, 50-6, 250+6, 250+6) or
+    // (44, 44, 256, 256) from the original photo and down sample it to 106.
     @Override
-    public synchronized Bitmap getTile(int level, int x, int y, int tileSize,
-            int borderSize) {
-        if (mRegionDecoder == null) return null;
-
-        // wantRegion is the rectangle on the original image we want. askRegion
-        // is the rectangle on the original image that we will ask from
-        // mRegionDecoder. Both are in the coordinates of the original image,
-        // not the coordinates of the scaled-down images.
-        Rect wantRegion = new Rect();
-        Rect askRegion = new Rect();
-
+    public Bitmap getTile(int level, int x, int y, int tileSize,
+            int borderSize, BitmapPool pool) {
         int b = borderSize << level;
-        wantRegion.set(x - b, y - b, x + (tileSize << level) + b,
-                y + (tileSize << level) + b);
+        int t = tileSize << level;
 
-        // askRegion is the intersection of wantRegion and the original image.
-        askRegion.set(0, 0, mImageWidth, mImageHeight);
-        Utils.assertTrue(askRegion.intersect(wantRegion));
+        Rect wantRegion = new Rect(x - b, y - b, x + t + b, y + t + b);
+
+        boolean needClear;
+        BitmapRegionDecoder regionDecoder = null;
+
+        synchronized (this) {
+            regionDecoder = mRegionDecoder;
+            if (regionDecoder == null) return null;
+
+            // We need to clear a reused bitmap, if wantRegion is not fully
+            // within the image.
+            needClear = !new Rect(0, 0, mImageWidth, mImageHeight)
+                    .contains(wantRegion);
+        }
+
+        Bitmap bitmap = pool == null ? null : pool.getBitmap();
+        if (bitmap != null) {
+            if (needClear) bitmap.eraseColor(0);
+        } else {
+            int s = tileSize + 2 * borderSize;
+            bitmap = Bitmap.createBitmap(s, s, Config.ARGB_8888);
+        }
 
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inPreferredConfig = Config.ARGB_8888;
         options.inPreferQualityOverSpeed = true;
         options.inSampleSize =  (1 << level);
+        options.inBitmap = bitmap;
 
-        Bitmap bitmap;
-
-        // In CropImage, we may call the decodeRegion() concurrently.
-        synchronized (mRegionDecoder) {
-            bitmap = mRegionDecoder.decodeRegion(askRegion, options);
+        try {
+            // In CropImage, we may call the decodeRegion() concurrently.
+            synchronized (regionDecoder) {
+                bitmap = regionDecoder.decodeRegion(wantRegion, options);
+            }
+        } finally {
+            if (options.inBitmap != bitmap && options.inBitmap != null) {
+                if (pool != null) pool.recycle(options.inBitmap);
+                options.inBitmap = null;
+            }
         }
 
         if (bitmap == null) {
             Log.w(TAG, "fail in decoding region");
-            return null;
         }
-
-        if (wantRegion.equals(askRegion)) return bitmap;
-
-        // Now the wantRegion does not match the askRegion. This means we are at
-        // a boundary tile, and we need to add paddings. Create a new Bitmap
-        // and copy over.
-        int size = tileSize + 2 * borderSize;
-        Bitmap result = Bitmap.createBitmap(size, size, Config.ARGB_8888);
-        Canvas canvas = new Canvas(result);
-        int offsetX = (askRegion.left - wantRegion.left) >> level;
-        int offsetY = (askRegion.top - wantRegion.top) >> level;
-        canvas.drawBitmap(bitmap, offsetX, offsetY, null);
-
-        // If the valid region (covered by bitmap or border) is smaller than the
-        // result bitmap, subset it.
-        int endX = offsetX + bitmap.getWidth() + borderSize;
-        int endY = offsetY + bitmap.getHeight() + borderSize;
-        bitmap.recycle();
-        if (endX < size || endY < size) {
-            return Bitmap.createBitmap(result, 0, 0, Math.min(size, endX),
-                    Math.min(size, endY));
-        } else {
-            return result;
-        }
+        return bitmap;
     }
 
     @Override
-    public Bitmap getBackupImage() {
-        return mBackupImage;
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
     }
 
     @Override
@@ -157,13 +177,4 @@
     public int getLevelCount() {
         return mLevelCount;
     }
-
-    public void setFailedToLoad() {
-        mFailedToLoad = true;
-    }
-
-    @Override
-    public boolean isFailedToLoad() {
-        return mFailedToLoad;
-    }
 }
diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java
index 1777048..0fe5067 100644
--- a/src/com/android/gallery3d/ui/UploadedTexture.java
+++ b/src/com/android/gallery3d/ui/UploadedTexture.java
@@ -16,13 +16,14 @@
 
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.common.Utils;
-
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.opengl.GLUtils;
 
+import com.android.gallery3d.common.Utils;
+
 import java.util.HashMap;
+
 import javax.microedition.khronos.opengles.GL11;
 import javax.microedition.khronos.opengles.GL11Ext;
 
@@ -51,6 +52,9 @@
     @SuppressWarnings("unused")
     private static final String TAG = "Texture";
     private boolean mContentValid = true;
+
+    // indicate this textures is being uploaded in background
+    private boolean mIsUploading = false;
     private boolean mOpaque = true;
     private boolean mThrottled = false;
     private static int sUploadedCount;
@@ -71,6 +75,14 @@
         }
     }
 
+    protected void setIsUploading(boolean uploading) {
+        mIsUploading = uploading;
+    }
+
+    public boolean isUploading() {
+        return mIsUploading;
+    }
+
     private static class BorderKey implements Cloneable {
         public boolean vertical;
         public Config config;
@@ -127,10 +139,6 @@
             int h = mBitmap.getHeight() + mBorder * 2;
             if (mWidth == UNSPECIFIED) {
                 setSize(w, h);
-            } else if (mWidth != w || mHeight != h) {
-                throw new IllegalStateException(String.format(
-                        "cannot change size: this = %s, orig = %sx%s, new = %sx%s",
-                        toString(), mWidth, mHeight, w, h));
             }
         }
         return mBitmap;
@@ -168,8 +176,8 @@
     /**
      * Whether the content on GPU is valid.
      */
-    public boolean isContentValid(GLCanvas canvas) {
-        return isLoaded(canvas) && mContentValid;
+    public boolean isContentValid() {
+        return isLoaded() && mContentValid;
     }
 
     /**
@@ -177,7 +185,7 @@
      * @param canvas
      */
     public void updateContent(GLCanvas canvas) {
-        if (!isLoaded(canvas)) {
+        if (!isLoaded()) {
             if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
                 return;
             }
@@ -217,6 +225,9 @@
                 int height = bHeight + mBorder * 2;
                 int texWidth = getTextureWidth();
                 int texHeight = getTextureHeight();
+
+                Utils.assertTrue(bWidth <= texWidth && bHeight <= texHeight);
+
                 // Define a vertically flipped crop rectangle for
                 // OES_draw_texture.
                 // The four values in sCropRect are: left, bottom, width, and
@@ -227,7 +238,7 @@
                 sCropRect[3] = -bHeight;
 
                 // Upload the bitmap to a new texture.
-                gl.glGenTextures(1, sTextureId, 0);
+                GLId.glGenTextures(1, sTextureId, 0);
                 gl.glBindTexture(GL11.GL_TEXTURE_2D, sTextureId[0]);
                 gl.glTexParameterfv(GL11.GL_TEXTURE_2D,
                         GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
@@ -284,7 +295,7 @@
             // Update texture state.
             setAssociatedCanvas(canvas);
             mId = sTextureId[0];
-            mState = UploadedTexture.STATE_LOADED;
+            mState = STATE_LOADED;
             mContentValid = true;
         } else {
             mState = STATE_ERROR;
@@ -295,7 +306,12 @@
     @Override
     protected boolean onBind(GLCanvas canvas) {
         updateContent(canvas);
-        return isContentValid(canvas);
+        return isContentValid();
+    }
+
+    @Override
+    protected int getTarget() {
+        return GL11.GL_TEXTURE_2D;
     }
 
     public void setOpaque(boolean isOpaque) {
diff --git a/src/com/android/gallery3d/ui/PositionProvider.java b/src/com/android/gallery3d/util/BucketNames.java
similarity index 65%
rename from src/com/android/gallery3d/ui/PositionProvider.java
rename to src/com/android/gallery3d/util/BucketNames.java
index 930c61e..043dd3d 100644
--- a/src/com/android/gallery3d/ui/PositionProvider.java
+++ b/src/com/android/gallery3d/util/BucketNames.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010 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.
@@ -14,10 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.gallery3d.ui;
+package com.android.gallery3d.util;
 
-import com.android.gallery3d.ui.PositionRepository.Position;
+/**
+ * Bucket names for buckets that are created and used in the Gallery.
+ */
+public class BucketNames {
 
-public interface PositionProvider {
-    public Position getPosition(long identity, Position target);
+    public static final String IMPORTED = "Imported";
+    public static final String DOWNLOAD = "download";
 }
diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java
index fcc444e..ba466f7 100644
--- a/src/com/android/gallery3d/util/CacheManager.java
+++ b/src/com/android/gallery3d/util/CacheManager.java
@@ -16,12 +16,12 @@
 
 package com.android.gallery3d.util;
 
-import com.android.gallery3d.common.BlobCache;
-
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
 
+import com.android.gallery3d.common.BlobCache;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.HashMap;
diff --git a/src/com/android/gallery3d/util/FutureTask.java b/src/com/android/gallery3d/util/FutureTask.java
deleted file mode 100644
index 9cfab27..0000000
--- a/src/com/android/gallery3d/util/FutureTask.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.util;
-
-import java.util.concurrent.Callable;
-
-// NOTE: If the Callable throws any Throwable, the result value will be null.
-public class FutureTask<T> implements Runnable, Future<T> {
-    private static final String TAG = "FutureTask";
-    private Callable<T> mCallable;
-    private FutureListener<T> mListener;
-    private volatile boolean mIsCancelled;
-    private boolean mIsDone;
-    private T mResult;
-
-    public FutureTask(Callable<T> callable, FutureListener<T> listener) {
-        mCallable = callable;
-        mListener = listener;
-    }
-
-    public FutureTask(Callable<T> callable) {
-        this(callable, null);
-    }
-
-    public void cancel() {
-        mIsCancelled = true;
-    }
-
-    public synchronized T get() {
-        while (!mIsDone) {
-            try {
-                wait();
-            } catch (InterruptedException t) {
-                // ignore.
-            }
-        }
-        return mResult;
-    }
-
-    public void waitDone() {
-        get();
-    }
-
-    public synchronized boolean isDone() {
-        return mIsDone;
-    }
-
-    public boolean isCancelled() {
-        return mIsCancelled;
-    }
-
-    public void run() {
-        T result = null;
-
-        if (!mIsCancelled) {
-            try {
-                result = mCallable.call();
-            } catch (Throwable ex) {
-                Log.w(TAG, "Exception in running a task", ex);
-            }
-        }
-
-        synchronized(this) {
-            mResult = result;
-            mIsDone = true;
-            if (mListener != null) {
-                mListener.onFutureDone(this);
-            }
-            notifyAll();
-        }
-    }
-}
diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
index fcb27ba..1d70914 100644
--- a/src/com/android/gallery3d/util/GalleryUtils.java
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -16,13 +16,6 @@
 
 package com.android.gallery3d.util;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.app.PackagesMonitor;
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.util.ThreadPool.CancelListener;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
@@ -31,6 +24,7 @@
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 import android.net.Uri;
 import android.os.ConditionVariable;
 import android.os.Environment;
@@ -41,6 +35,13 @@
 import android.util.Log;
 import android.view.WindowManager;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.PackagesMonitor;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
@@ -49,6 +50,7 @@
     private static final String TAG = "GalleryUtils";
     private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
     private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity";
+    private static final String CAMERA_LAUNCHER_NAME = "com.android.camera.CameraLauncher";
 
     private static final String MIME_TYPE_IMAGE = "image/*";
     private static final String MIME_TYPE_VIDEO = "video/*";
@@ -62,13 +64,11 @@
     private static final String KEY_CAMERA_UPDATE = "camera-update";
     private static final String KEY_HAS_CAMERA = "has-camera";
 
-    private static Context sContext;
-
-
-    static float sPixelDensity = -1f;
+    private static float sPixelDensity = -1f;
+    private static boolean sCameraAvailableInitialized = false;
+    private static boolean sCameraAvailable;
 
     public static void initialize(Context context) {
-        sContext = context;
         if (sPixelDensity < 0) {
             DisplayMetrics metrics = new DisplayMetrics();
             WindowManager wm = (WindowManager)
@@ -191,7 +191,7 @@
         return prefs.getBoolean(hasKey, true);
     }
 
-    public static boolean isCameraAvailable(Context context) {
+    public static boolean isAnyCameraAvailable(Context context) {
         int version = PackagesMonitor.getPackagesVersion(context);
         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
         if (prefs.getInt(KEY_CAMERA_UPDATE, 0) != version) {
@@ -205,6 +205,25 @@
         return prefs.getBoolean(KEY_HAS_CAMERA, true);
     }
 
+    public static boolean isCameraAvailable(Context context) {
+        if (sCameraAvailableInitialized) return sCameraAvailable;
+        PackageManager pm = context.getPackageManager();
+        ComponentName name = new ComponentName(context, CAMERA_LAUNCHER_NAME);
+        int state = pm.getComponentEnabledSetting(name);
+        sCameraAvailableInitialized = true;
+        sCameraAvailable =
+            (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+             || (state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+        return sCameraAvailable;
+    }
+
+    public static void startCameraActivity(Context context) {
+        Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+                        | Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+    }
+
     public static boolean isValidLocation(double latitude, double longitude) {
         // TODO: change || to && after we fix the default location issue
         return (latitude != MediaItem.INVALID_LATLNG || longitude != MediaItem.INVALID_LATLNG);
@@ -271,11 +290,6 @@
         return durationValue;
     }
 
-    public static void setSpinnerVisibility(final Activity activity,
-            final boolean visible) {
-        SpinnerVisibilitySetter.getInstance(activity).setSpinnerVisibility(visible);
-    }
-
     public static int determineTypeBits(Context context, Intent intent) {
         int typeBits = 0;
         String type = intent.resolveType(context);
@@ -324,31 +338,6 @@
         return false;
     }
 
-    public static void assertInMainThread() {
-        if (Thread.currentThread() == sContext.getMainLooper().getThread()) {
-            throw new AssertionError();
-        }
-    }
-
-    public static void doubleToRational(double value, long[] output) {
-        // error is a magic number to control the tollerance of error
-        doubleToRational(value, output, 0.00001);
-    }
-
-    private static void doubleToRational(double value, long[] output, double error) {
-        long number = (long) value;
-        value -= number;
-        if (value < 0.000001 || error > 1) {
-            output[0] = (int) (number + value + 0.5);
-            output[1] = 1;
-        } else {
-            doubleToRational(1.0 / value, output, error / value);
-            number = number * output[0] + output[1];
-            output[1] = output[0];
-            output[0] = number;
-        }
-    }
-
     public static boolean isPanorama(MediaItem item) {
         if (item == null) return false;
         int w = item.getWidth();
diff --git a/src/com/android/gallery3d/util/HelpUtils.java b/src/com/android/gallery3d/util/HelpUtils.java
new file mode 100644
index 0000000..2f02642
--- /dev/null
+++ b/src/com/android/gallery3d/util/HelpUtils.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+
+import java.util.Locale;
+
+/**
+ * Functions to easily prepare contextual help menu option items with an intent that opens up the
+ * browser to a particular URL, while taking into account the preferred language and app version.
+ */
+public class HelpUtils {
+    private final static String TAG = HelpUtils.class.getName();
+
+    /**
+     * Help URL query parameter key for the preferred language.
+     */
+    private final static String PARAM_LANGUAGE_CODE = "hl";
+
+    /**
+     * Help URL query parameter key for the app version.
+     */
+    private final static String PARAM_VERSION = "version";
+
+    /**
+     * Cached version code to prevent repeated calls to the package manager.
+     */
+    private static String sCachedVersionCode = null;
+
+    /** Static helper that is not instantiable*/
+    private HelpUtils() { }
+
+    /**
+     * Prepares the help menu item by doing the following.
+     * - If the string corresponding to the helpUrlResourceId is empty or null, then the help menu
+     *   item is made invisible.
+     * - Otherwise, this makes the help menu item visible and sets the intent for the help menu
+     *   item to view the URL.
+     *
+     * @return returns whether the help menu item has been made visible.
+     */
+    public static boolean prepareHelpMenuItem(Context context, MenuItem helpMenuItem,
+            int helpUrlResourceId) {
+        String helpUrlString = context.getResources().getString(helpUrlResourceId);
+        return prepareHelpMenuItem(context, helpMenuItem, helpUrlString);
+    }
+
+    /**
+     * Prepares the help menu item by doing the following.
+     * - If the helpUrlString is empty or null, the help menu item is made invisible.
+     * - Otherwise, this makes the help menu item visible and sets the intent for the help menu
+     *   item to view the URL.
+     *
+     * @return returns whether the help menu item has been made visible.
+     */
+    public static boolean prepareHelpMenuItem(Context context, MenuItem helpMenuItem,
+            String helpUrlString) {
+        if (TextUtils.isEmpty(helpUrlString)) {
+            // The help url string is empty or null, so set the help menu item to be invisible.
+            helpMenuItem.setVisible(false);
+
+            // return that the help menu item is not visible (i.e. false)
+            return false;
+        } else {
+            // The help url string exists, so first add in some extra query parameters.
+            final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUrlString));
+
+            // Then, create an intent that will be fired when the user
+            // selects this help menu item.
+            Intent intent = new Intent(Intent.ACTION_VIEW, fullUri);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                    | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+            // Set the intent to the help menu item, show the help menu item in the overflow
+            // menu, and make it visible.
+            helpMenuItem.setIntent(intent);
+            helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+            helpMenuItem.setVisible(true);
+
+            // return that the help menu item is visible (i.e., true)
+            return true;
+        }
+    }
+
+    /**
+     * Adds two query parameters into the Uri, namely the language code and the version code
+     * of the app's package as gotten via the context.
+     * @return the uri with added query parameters
+     */
+    private static Uri uriWithAddedParameters(Context context, Uri baseUri) {
+        Uri.Builder builder = baseUri.buildUpon();
+
+        // Add in the preferred language
+        builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString());
+
+        // Add in the package version code
+        if (sCachedVersionCode == null) {
+            // There is no cached version code, so try to get it from the package manager.
+            try {
+                // cache the version code
+                PackageInfo info = context.getPackageManager().getPackageInfo(
+                        context.getPackageName(), 0);
+                sCachedVersionCode = Integer.toString(info.versionCode);
+
+                // append the version code to the uri
+                builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
+            } catch (NameNotFoundException e) {
+                // Cannot find the package name, so don't add in the version parameter
+                // This shouldn't happen.
+                Log.wtf(TAG, "Invalid package name for context", e);
+            }
+        } else {
+            builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
+        }
+
+        // Build the full uri and return it
+        return builder.build();
+    }
+}
diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java
index 02a46ae..3edc424 100644
--- a/src/com/android/gallery3d/util/IdentityCache.java
+++ b/src/com/android/gallery3d/util/IdentityCache.java
@@ -18,8 +18,8 @@
 
 import java.lang.ref.ReferenceQueue;
 import java.lang.ref.WeakReference;
-import java.util.HashMap;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Set;
 
 public class IdentityCache<K, V> {
@@ -61,11 +61,15 @@
         return entry == null ? null : entry.get();
     }
 
+    // This is currently unused.
+    /*
     public synchronized void clear() {
         mWeakMap.clear();
         mQueue = new ReferenceQueue<V>();
     }
+    */
 
+    // This is for debugging only
     public synchronized ArrayList<K> keys() {
         Set<K> set = mWeakMap.keySet();
         ArrayList<K> result = new ArrayList<K>(set);
diff --git a/src/com/android/gallery3d/util/IntArray.java b/src/com/android/gallery3d/util/IntArray.java
index 88657bb..082089a 100644
--- a/src/com/android/gallery3d/util/IntArray.java
+++ b/src/com/android/gallery3d/util/IntArray.java
@@ -35,6 +35,7 @@
         return mSize;
     }
 
+    // For testing only
     public int[] toArray(int[] result) {
         if (result == null || result.length < mSize) {
             result = new int[mSize];
diff --git a/src/com/android/gallery3d/util/JobLimiter.java b/src/com/android/gallery3d/util/JobLimiter.java
index 5abdfd8..42b7541 100644
--- a/src/com/android/gallery3d/util/JobLimiter.java
+++ b/src/com/android/gallery3d/util/JobLimiter.java
@@ -140,10 +140,6 @@
         return future;
     }
 
-    public <T> Future<T> submit(Job<T> job) {
-        return submit(job, null);
-    }
-
     @SuppressWarnings({"rawtypes", "unchecked"})
     private void submitTasksIfAllowed() {
         while (mLimit > 0 && !mJobs.isEmpty()) {
diff --git a/src/com/android/gallery3d/util/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java
index 8554acd..4cfc3cd 100644
--- a/src/com/android/gallery3d/util/LinkedNode.java
+++ b/src/com/android/gallery3d/util/LinkedNode.java
@@ -43,10 +43,6 @@
     public static class List<T extends LinkedNode> {
         private LinkedNode mHead = new LinkedNode();
 
-        public void insertFirst(T node) {
-            mHead.insert(node);
-        }
-
         public void insertLast(T node) {
             mHead.mPrev.insert(node);
         }
diff --git a/src/com/android/gallery3d/util/MediaSetUtils.java b/src/com/android/gallery3d/util/MediaSetUtils.java
index 817ffed..3bb3f8b 100644
--- a/src/com/android/gallery3d/util/MediaSetUtils.java
+++ b/src/com/android/gallery3d/util/MediaSetUtils.java
@@ -16,12 +16,11 @@
 
 package com.android.gallery3d.util;
 
-import com.android.gallery3d.data.MediaSet;
-import com.android.gallery3d.data.MtpContext;
-import com.android.gallery3d.data.Path;
-
 import android.os.Environment;
 
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
 import java.util.Comparator;
 
 public class MediaSetUtils {
@@ -30,10 +29,14 @@
     public static final int CAMERA_BUCKET_ID = GalleryUtils.getBucketId(
             Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera");
     public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId(
-            Environment.getExternalStorageDirectory().toString() + "/download");
+            Environment.getExternalStorageDirectory().toString() + "/"
+            + BucketNames.DOWNLOAD);
     public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId(
             Environment.getExternalStorageDirectory().toString() + "/"
-            + MtpContext.NAME_IMPORTED_FOLDER);
+            + BucketNames.IMPORTED);
+    public static final int SNAPSHOT_BUCKET_ID = GalleryUtils.getBucketId(
+            Environment.getExternalStorageDirectory().toString() +
+            "/Pictures/Screenshots");
 
     private static final Path[] CAMERA_PATHS = {
             Path.fromString("/local/all/" + CAMERA_BUCKET_ID),
diff --git a/src/com/android/gallery3d/util/Profile.java b/src/com/android/gallery3d/util/Profile.java
new file mode 100644
index 0000000..6b6e5c3
--- /dev/null
+++ b/src/com/android/gallery3d/util/Profile.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+// The Profile class is used to collect profiling information for a thread. It
+// samples stack traces for a thread periodically. enable() and disable() is
+// used to enable and disable profiling for the calling thread. The profiling
+// information can then be dumped to a file using the dumpToFile() method.
+//
+// The disableAll() method can be used to disable profiling for all threads and
+// can be called in onPause() to ensure all profiling is disabled when an
+// activity is paused.
+public class Profile {
+    private static final String TAG = "Profile";
+    private static final int NS_PER_MS = 1000000;
+
+    // This is a watchdog entry for one thread.
+    // For every cycleTime period, we dump the stack of the thread.
+    private static class WatchEntry {
+        Thread thread;
+
+        // Both are in milliseconds
+        int cycleTime;
+        int wakeTime;
+
+        boolean isHolding;
+        ArrayList<String[]> holdingStacks = new ArrayList<String[]>();
+    }
+
+    // This is a watchdog thread which dumps stacks of other threads periodically.
+    private static Watchdog sWatchdog = new Watchdog();
+
+    private static class Watchdog {
+        private ArrayList<WatchEntry> mList = new ArrayList<WatchEntry>();
+        private HandlerThread mHandlerThread;
+        private Handler mHandler;
+        private Runnable mProcessRunnable = new Runnable() {
+            public void run() {
+                synchronized (Watchdog.this) {
+                    processList();
+                }
+            }
+        };
+        private Random mRandom = new Random();
+        private ProfileData mProfileData = new ProfileData();
+
+        public Watchdog() {
+            mHandlerThread = new HandlerThread("Watchdog Handler",
+                    Process.THREAD_PRIORITY_FOREGROUND);
+            mHandlerThread.start();
+            mHandler = new Handler(mHandlerThread.getLooper());
+        }
+
+        public synchronized void addWatchEntry(Thread thread, int cycleTime) {
+            WatchEntry e = new WatchEntry();
+            e.thread = thread;
+            e.cycleTime = cycleTime;
+            int firstDelay = 1 + mRandom.nextInt(cycleTime);
+            e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay;
+            mList.add(e);
+            processList();
+        }
+
+        public synchronized void removeWatchEntry(Thread thread) {
+            for (int i = 0; i < mList.size(); i++) {
+                if (mList.get(i).thread == thread) {
+                    mList.remove(i);
+                    break;
+                }
+            }
+            processList();
+        }
+
+        public synchronized void removeAllWatchEntries() {
+            mList.clear();
+            processList();
+        }
+
+        private void processList() {
+            mHandler.removeCallbacks(mProcessRunnable);
+            if (mList.size() == 0) return;
+
+            int currentTime = (int) (System.nanoTime() / NS_PER_MS);
+            int nextWakeTime = 0;
+
+            for (WatchEntry entry : mList) {
+                if (currentTime > entry.wakeTime) {
+                    entry.wakeTime += entry.cycleTime;
+                    Thread thread = entry.thread;
+                    sampleStack(entry);
+                }
+
+                if (entry.wakeTime > nextWakeTime) {
+                    nextWakeTime = entry.wakeTime;
+                }
+            }
+
+            long delay = nextWakeTime - currentTime;
+            mHandler.postDelayed(mProcessRunnable, delay);
+        }
+
+        private void sampleStack(WatchEntry entry) {
+            Thread thread = entry.thread;
+            StackTraceElement[] stack = thread.getStackTrace();
+            String[] lines = new String[stack.length];
+            for (int i = 0; i < stack.length; i++) {
+                lines[i] = stack[i].toString();
+            }
+            if (entry.isHolding) {
+                entry.holdingStacks.add(lines);
+            } else {
+                mProfileData.addSample(lines);
+            }
+        }
+
+        private WatchEntry findEntry(Thread thread) {
+            for (int i = 0; i < mList.size(); i++) {
+                WatchEntry entry = mList.get(i);
+                if (entry.thread == thread) return entry;
+            }
+            return null;
+        }
+
+        public synchronized void dumpToFile(String filename) {
+            mProfileData.dumpToFile(filename);
+        }
+
+        public synchronized void reset() {
+            mProfileData.reset();
+        }
+
+        public synchronized void hold(Thread t) {
+            WatchEntry entry = findEntry(t);
+
+            // This can happen if the profiling is disabled (probably from
+            // another thread). Same check is applied in commit() and drop()
+            // below.
+            if (entry == null) return;
+
+            entry.isHolding = true;
+        }
+
+        public synchronized void commit(Thread t) {
+            WatchEntry entry = findEntry(t);
+            if (entry == null) return;
+            ArrayList<String[]> stacks = entry.holdingStacks;
+            for (int i = 0; i < stacks.size(); i++) {
+                mProfileData.addSample(stacks.get(i));
+            }
+            entry.isHolding = false;
+            entry.holdingStacks.clear();
+        }
+
+        public synchronized void drop(Thread t) {
+            WatchEntry entry = findEntry(t);
+            if (entry == null) return;
+            entry.isHolding = false;
+            entry.holdingStacks.clear();
+        }
+    }
+
+    // Enable profiling for the calling thread. Periodically (every cycleTimeInMs
+    // milliseconds) sample the stack trace of the calling thread.
+    public static void enable(int cycleTimeInMs) {
+        Thread t = Thread.currentThread();
+        sWatchdog.addWatchEntry(t, cycleTimeInMs);
+    }
+
+    // Disable profiling for the calling thread.
+    public static void disable() {
+        sWatchdog.removeWatchEntry(Thread.currentThread());
+    }
+
+    // Disable profiling for all threads.
+    public static void disableAll() {
+        sWatchdog.removeAllWatchEntries();
+    }
+
+    // Dump the profiling data to a file.
+    public static void dumpToFile(String filename) {
+        sWatchdog.dumpToFile(filename);
+    }
+
+    // Reset the collected profiling data.
+    public static void reset() {
+        sWatchdog.reset();
+    }
+
+    // Hold the future samples coming from current thread until commit() or
+    // drop() is called, and those samples are recorded or ignored as a result.
+    // This must called after enable() to be effective.
+    public static void hold() {
+        sWatchdog.hold(Thread.currentThread());
+    }
+
+    public static void commit() {
+        sWatchdog.commit(Thread.currentThread());
+    }
+
+    public static void drop() {
+        sWatchdog.drop(Thread.currentThread());
+    }
+}
diff --git a/src/com/android/gallery3d/util/ProfileData.java b/src/com/android/gallery3d/util/ProfileData.java
new file mode 100644
index 0000000..02eb37a
--- /dev/null
+++ b/src/com/android/gallery3d/util/ProfileData.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.DataOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+// ProfileData keeps profiling samples in a tree structure.
+// The addSample() method adds a sample. The dumpToFile() method saves the data
+// to a file. The reset() method clears all samples.
+public class ProfileData {
+    private static final String TAG = "ProfileData";
+
+    private static class Node {
+        public int id;  // this is the name of this node, mapped from mNameToId
+        public Node parent;
+        public int sampleCount;
+        public ArrayList<Node> children;
+        public Node(Node parent, int id) {
+            this.parent = parent;
+            this.id = id;
+        }
+    }
+
+    private Node mRoot;
+    private int mNextId;
+    private HashMap<String, Integer> mNameToId;
+    private DataOutputStream mOut;
+    private byte mScratch[] = new byte[4];  // scratch space for writeInt()
+
+    public ProfileData() {
+        mRoot = new Node(null, -1);  // The id of the root node is unused.
+        mNameToId = new HashMap<String, Integer>();
+    }
+
+    public void reset() {
+        mRoot = new Node(null, -1);
+        mNameToId.clear();
+        mNextId = 0;
+    }
+
+    private int nameToId(String name) {
+        Integer id = mNameToId.get(name);
+        if (id == null) {
+            id = ++mNextId;  // The tool doesn't want id=0, so we start from 1.
+            mNameToId.put(name, id);
+        }
+        return id;
+    }
+
+    public void addSample(String[] stack) {
+        int[] ids = new int[stack.length];
+        for (int i = 0; i < stack.length; i++) {
+            ids[i] = nameToId(stack[i]);
+        }
+
+        Node node = mRoot;
+        for (int i = stack.length - 1; i >= 0; i--) {
+            if (node.children == null) {
+                node.children = new ArrayList<Node>();
+            }
+
+            int id = ids[i];
+            ArrayList<Node> children = node.children;
+            int j;
+            for (j = 0; j < children.size(); j++) {
+                if (children.get(j).id == id) break;
+            }
+            if (j == children.size()) {
+                children.add(new Node(node, id));
+            }
+
+            node = children.get(j);
+        }
+
+        node.sampleCount++;
+    }
+
+    public void dumpToFile(String filename) {
+        try {
+            mOut = new DataOutputStream(new FileOutputStream(filename));
+            // Start record
+            writeInt(0);
+            writeInt(3);
+            writeInt(1);
+            writeInt(20000);  // Sampling period: 20ms
+            writeInt(0);
+
+            // Samples
+            writeAllStacks(mRoot, 0);
+
+            // End record
+            writeInt(0);
+            writeInt(1);
+            writeInt(0);
+            writeAllSymbols();
+        } catch (IOException ex) {
+            Log.w("Failed to dump to file", ex);
+        } finally {
+            Utils.closeSilently(mOut);
+        }
+    }
+
+    // Writes out one stack, consisting of N+2 words:
+    // first word: sample count
+    // second word: depth of the stack (N)
+    // N words: each word is the id of one address in the stack
+    private void writeOneStack(Node node, int depth) throws IOException {
+        writeInt(node.sampleCount);
+        writeInt(depth);
+        while (depth-- > 0) {
+            writeInt(node.id);
+            node = node.parent;
+        }
+    }
+
+    private void writeAllStacks(Node node, int depth) throws IOException {
+        if (node.sampleCount > 0) {
+            writeOneStack(node, depth);
+        }
+
+        ArrayList<Node> children = node.children;
+        if (children != null) {
+            for (int i = 0; i < children.size(); i++) {
+                writeAllStacks(children.get(i), depth + 1);
+            }
+        }
+    }
+
+    // Writes out the symbol table. Each line is like:
+    // 0x17e java.util.ArrayList.isEmpty(ArrayList.java:319)
+    private void writeAllSymbols() throws IOException {
+        for (Entry<String, Integer> entry : mNameToId.entrySet()) {
+            mOut.writeBytes(String.format("0x%x %s\n", entry.getValue(), entry.getKey()));
+        }
+    }
+
+    private void writeInt(int v) throws IOException {
+        mScratch[0] = (byte) v;
+        mScratch[1] = (byte) (v >> 8);
+        mScratch[2] = (byte) (v >> 16);
+        mScratch[3] = (byte) (v >> 24);
+        mOut.write(mScratch);
+    }
+}
diff --git a/src/com/android/gallery3d/util/RangeArray.java b/src/com/android/gallery3d/util/RangeArray.java
new file mode 100644
index 0000000..8e61348
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeArray.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeArray<T> {
+    private T[] mData;
+    private int mOffset;
+
+    public RangeArray(int min, int max) {
+        mData = (T[]) new Object[max - min + 1];
+        mOffset = min;
+    }
+
+    // Wraps around an existing array
+    public RangeArray(T[] src, int min, int max) {
+        if (max - min + 1 != src.length) {
+            throw new AssertionError();
+        }
+        mData = src;
+        mOffset = min;
+    }
+
+    public void put(int i, T object) {
+        mData[i - mOffset] = object;
+    }
+
+    public T get(int i) {
+        return mData[i - mOffset];
+    }
+
+    public int indexOf(T object) {
+        for (int i = 0; i < mData.length; i++) {
+            if (mData[i] == object) return i + mOffset;
+        }
+        return Integer.MAX_VALUE;
+    }
+}
diff --git a/src/com/android/gallery3d/util/RangeBoolArray.java b/src/com/android/gallery3d/util/RangeBoolArray.java
new file mode 100644
index 0000000..035fc40
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeBoolArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeBoolArray {
+    private boolean[] mData;
+    private int mOffset;
+
+    public RangeBoolArray(int min, int max) {
+        mData = new boolean[max - min + 1];
+        mOffset = min;
+    }
+
+    // Wraps around an existing array
+    public RangeBoolArray(boolean[] src, int min, int max) {
+        mData = src;
+        mOffset = min;
+    }
+
+    public void put(int i, boolean object) {
+        mData[i - mOffset] = object;
+    }
+
+    public boolean get(int i) {
+        return mData[i - mOffset];
+    }
+
+    public int indexOf(boolean object) {
+        for (int i = 0; i < mData.length; i++) {
+            if (mData[i] == object) return i + mOffset;
+        }
+        return Integer.MAX_VALUE;
+    }
+}
diff --git a/src/com/android/gallery3d/util/RangeIntArray.java b/src/com/android/gallery3d/util/RangeIntArray.java
new file mode 100644
index 0000000..9dbb99f
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeIntArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeIntArray {
+    private int[] mData;
+    private int mOffset;
+
+    public RangeIntArray(int min, int max) {
+        mData = new int[max - min + 1];
+        mOffset = min;
+    }
+
+    // Wraps around an existing array
+    public RangeIntArray(int[] src, int min, int max) {
+        mData = src;
+        mOffset = min;
+    }
+
+    public void put(int i, int object) {
+        mData[i - mOffset] = object;
+    }
+
+    public int get(int i) {
+        return mData[i - mOffset];
+    }
+
+    public int indexOf(int object) {
+        for (int i = 0; i < mData.length; i++) {
+            if (mData[i] == object) return i + mOffset;
+        }
+        return Integer.MAX_VALUE;
+    }
+}
diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java
index d253b4b..3df4c49 100644
--- a/src/com/android/gallery3d/util/ReverseGeocoder.java
+++ b/src/com/android/gallery3d/util/ReverseGeocoder.java
@@ -16,8 +16,6 @@
 
 package com.android.gallery3d.util;
 
-import com.android.gallery3d.common.BlobCache;
-
 import android.content.Context;
 import android.location.Address;
 import android.location.Geocoder;
@@ -26,6 +24,8 @@
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 
+import com.android.gallery3d.common.BlobCache;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
diff --git a/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java b/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java
deleted file mode 100644
index 6ccc264..0000000
--- a/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.gallery3d.util;
-
-import android.app.Activity;
-import android.os.Handler;
-import android.os.Message;
-import android.os.SystemClock;
-
-import java.util.WeakHashMap;
-
-/**
- * This class manages the visibility of the progress spinner in the action bar for an
- * Activity. It filters out short-lived appearances of the progress spinner by only
- * showing the spinner if it hasn't been hidden again before the end of a specified
- * delay period. It also enforces a minimum display time once the spinner is made visible.
- * This meant to cut down on the frequent "flashes" of the progress spinner.
- */
-public class SpinnerVisibilitySetter {
-
-    private static final int MSG_SHOW_SPINNER = 1;
-    private static final int MSG_HIDE_SPINNER = 2;
-
-    // Amount of time after a show request that the progress spinner is actually made visible.
-    // This means that any show/hide requests that happen subsequently within this period
-    // of time will be ignored.
-    private static final long SPINNER_DISPLAY_DELAY = 1000;
-
-    // The minimum amount of time the progress spinner must be visible before it can be hidden.
-    private static final long MIN_SPINNER_DISPLAY_TIME = 2000;
-
-    static final WeakHashMap<Activity, SpinnerVisibilitySetter> sInstanceMap =
-            new WeakHashMap<Activity, SpinnerVisibilitySetter>();
-
-    private long mSpinnerVisibilityStartTime = -1;
-    private Activity mActivity;
-
-    private Handler mHandler = new Handler() {
-        @Override
-        public void handleMessage(Message msg) {
-            switch(msg.what) {
-                case MSG_SHOW_SPINNER:
-                    removeMessages(MSG_SHOW_SPINNER);
-                    if (mSpinnerVisibilityStartTime >= 0) break;
-                    mSpinnerVisibilityStartTime = SystemClock.elapsedRealtime();
-                    mActivity.setProgressBarIndeterminateVisibility(true);
-                    break;
-                case MSG_HIDE_SPINNER:
-                    removeMessages(MSG_HIDE_SPINNER);
-                    if (mSpinnerVisibilityStartTime < 0) break;
-                    long t = SystemClock.elapsedRealtime() - mSpinnerVisibilityStartTime;
-                    if (t >= MIN_SPINNER_DISPLAY_TIME) {
-                        mSpinnerVisibilityStartTime = -1;
-                        mActivity.setProgressBarIndeterminateVisibility(false);
-                    } else {
-                        sendEmptyMessageDelayed(MSG_HIDE_SPINNER, MIN_SPINNER_DISPLAY_TIME - t);
-                    }
-                    break;
-            }
-        }
-    };
-
-    /**
-     *  Gets the <code>SpinnerVisibilitySetter</code> for the given <code>activity</code>.
-     *
-     *  This method must be called from the main thread.
-     */
-    public static SpinnerVisibilitySetter getInstance(Activity activity) {
-        synchronized(sInstanceMap) {
-            SpinnerVisibilitySetter setter = sInstanceMap.get(activity);
-            if (setter == null) {
-                setter = new SpinnerVisibilitySetter(activity);
-                sInstanceMap.put(activity, setter);
-            }
-            return setter;
-        }
-    }
-
-    private SpinnerVisibilitySetter(Activity activity) {
-        mActivity = activity;
-    }
-
-    public void setSpinnerVisibility(boolean visible) {
-        if (visible) {
-            mHandler.removeMessages(MSG_HIDE_SPINNER);
-            mHandler.sendEmptyMessageDelayed(MSG_SHOW_SPINNER, SPINNER_DISPLAY_DELAY);
-        } else {
-            mHandler.removeMessages(MSG_SHOW_SPINNER);
-            mHandler.sendEmptyMessage(MSG_HIDE_SPINNER);
-        }
-    }
-}
diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java
index 9fdade6..f76705d 100644
--- a/src/com/android/gallery3d/util/UpdateHelper.java
+++ b/src/com/android/gallery3d/util/UpdateHelper.java
@@ -45,14 +45,6 @@
         return original;
     }
 
-    public double update(float original, float update) {
-        if (original != update) {
-            mUpdated = true;
-            original = update;
-        }
-        return original;
-    }
-
     public <T> T update(T original, T update) {
         if (!Utils.equals(original, update)) {
             mUpdated = true;
diff --git a/src_pd/com/android/gallery3d/picasasource/PicasaSource.java b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
index 8cfdac3..f849067 100644
--- a/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
+++ b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java
@@ -118,6 +118,14 @@
         throw new UnsupportedOperationException();
     }
 
+    public static long getPicasaId(MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
+    public static String getUserAccount(Context context, MediaObject image) {
+        throw new UnsupportedOperationException();
+    }
+
     public static ParcelFileDescriptor openFile(Context context, MediaObject image, String mode)
             throws FileNotFoundException {
         throw new UnsupportedOperationException();
diff --git a/tests/Android.mk b/tests/Android.mk
index 602f693..cfd0791 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -10,7 +10,6 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 LOCAL_PACKAGE_NAME := Gallery2Tests
-LOCAL_CERTIFICATE := media
 
 LOCAL_INSTRUMENTATION_FOR := Gallery2
 
diff --git a/tests/src/com/android/gallery3d/common/UtilsTest.java b/tests/src/com/android/gallery3d/common/UtilsTest.java
index b355244..a20ebeb 100644
--- a/tests/src/com/android/gallery3d/common/UtilsTest.java
+++ b/tests/src/com/android/gallery3d/common/UtilsTest.java
@@ -151,30 +151,6 @@
         assertFalse(Utils.equals(a, b));
     }
 
-    public void testIsPowerOf2() {
-        for (int i = 0; i < 31; i++) {
-            int v = (1 << i);
-            assertTrue(Utils.isPowerOf2(v));
-        }
-
-        int[] f = new int[] {3, 5, 6, 7, 9, 10, 65535, Integer.MAX_VALUE - 1,
-                Integer.MAX_VALUE };
-        for (int v : f) {
-            assertFalse(Utils.isPowerOf2(v));
-        }
-
-        int[] e = new int[] {0, -1, -2, -4, -65536, Integer.MIN_VALUE + 1,
-                Integer.MIN_VALUE };
-        for (int v : e) {
-            try {
-                Utils.isPowerOf2(v);
-                fail();
-            } catch (IllegalArgumentException ex) {
-                // expected.
-            }
-        }
-    }
-
     public void testNextPowerOf2() {
         int[] q = new int[] {1, 2, 3, 4, 5, 6, 10, 65535, (1 << 30) - 1, (1 << 30)};
         int[] a = new int[] {1, 2, 4, 4, 8, 8, 16, 65536, (1 << 30)    , (1 << 30)};
@@ -195,16 +171,6 @@
         }
     }
 
-    public void testDistance() {
-        assertFloatEq(0f, Utils.distance(0, 0, 0, 0));
-        assertFloatEq(1f, Utils.distance(0, 1, 0, 0));
-        assertFloatEq(1f, Utils.distance(0, 0, 0, 1));
-        assertFloatEq(2f, Utils.distance(1, 2, 3, 2));
-        assertFloatEq(5f, Utils.distance(1, 2, 1 + 3, 2 + 4));
-        assertFloatEq(5f, Utils.distance(1, 2, 1 + 3, 2 + 4));
-        assertFloatEq(Float.MAX_VALUE, Utils.distance(Float.MAX_VALUE, 0, 0, 0));
-    }
-
     public void testClamp() {
         assertEquals(1000, Utils.clamp(300, 1000, 2000));
         assertEquals(1300, Utils.clamp(1300, 1000, 2000));
@@ -227,14 +193,6 @@
         assertFalse(Utils.isOpaque(0xAA0000FF));
     }
 
-    public static void testSwap() {
-        Integer[] a = {1, 2, 3};
-        Utils.swap(a, 0, 2);
-        assertEquals(a[0].intValue(), 3);
-        assertEquals(a[1].intValue(), 2);
-        assertEquals(a[2].intValue(), 1);
-    }
-
     public static void assertFloatEq(float expected, float actual) {
         if (Math.abs(actual - expected) > 1e-6) {
             Log.v(TAG, "expected: " + expected + ", actual: " + actual);
diff --git a/tests/src/com/android/gallery3d/data/GalleryAppStub.java b/tests/src/com/android/gallery3d/data/GalleryAppStub.java
index 36075f4..47693d2 100644
--- a/tests/src/com/android/gallery3d/data/GalleryAppStub.java
+++ b/tests/src/com/android/gallery3d/data/GalleryAppStub.java
@@ -19,7 +19,6 @@
 import com.android.gallery3d.app.GalleryApp;
 import com.android.gallery3d.app.StateManager;
 import com.android.gallery3d.ui.GLRoot;
-import com.android.gallery3d.ui.PositionRepository;
 import com.android.gallery3d.util.ThreadPool;
 
 import android.content.ContentResolver;
@@ -35,7 +34,6 @@
     public DecodeUtils getDecodeService() { return null; }
 
     public GLRoot getGLRoot() { return null; }
-    public PositionRepository getPositionRepository() { return null; }
 
     public Context getAndroidContext() { return null; }
 
diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
index f1663f4..5f749d8 100644
--- a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
+++ b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java
@@ -33,15 +33,16 @@
     }
     public void multiplyAlpha(float alpha) {}
     public void translate(float x, float y, float z) {}
+    public void translate(float x, float y) {}
     public void scale(float sx, float sy, float sz) {}
     public void rotate(float angle, float x, float y, float z) {}
     public boolean clipRect(int left, int top, int right, int bottom) {
         throw new UnsupportedOperationException();
     }
-    public int save() {
+    public void save() {
         throw new UnsupportedOperationException();
     }
-    public int save(int saveFlags) {
+    public void save(int saveFlags) {
         throw new UnsupportedOperationException();
     }
     public void setBlendEnabled(boolean enabled) {}
@@ -56,6 +57,8 @@
     public void drawTexture(BasicTexture texture,
             int x, int y, int width, int height, float alpha) {}
     public void drawTexture(BasicTexture texture, RectF source, RectF target) {}
+    public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+            int x, int y, int w, int h) {}
     public void drawMixed(BasicTexture from, BasicTexture to,
             float ratio, int x, int y, int w, int h) {}
     public void drawMixed(BasicTexture from, int to,
@@ -76,4 +79,7 @@
     }
     public void deleteRecycledResources() {}
     public void multiplyMatrix(float[] mMatrix, int offset) {}
+    public void dumpStatisticsAndClear() {}
+    public void beginRenderTarget(RawTexture texture) {}
+    public void endRenderTarget() {}
 }
diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasTest.java b/tests/src/com/android/gallery3d/ui/GLCanvasTest.java
index 528b04f..72ccbfb 100644
--- a/tests/src/com/android/gallery3d/ui/GLCanvasTest.java
+++ b/tests/src/com/android/gallery3d/ui/GLCanvasTest.java
@@ -66,26 +66,6 @@
     }
 
     @SmallTest
-    public void testAnimationTime() {
-        GL11 glStub = new GLStub();
-        GLCanvas canvas = new GLCanvasImpl(glStub);
-
-        long[] testData = {0, 1, 2, 1000, 10000, Long.MAX_VALUE};
-
-        for (long v : testData) {
-            canvas.setCurrentAnimationTimeMillis(v);
-            assertEquals(v, canvas.currentAnimationTimeMillis());
-        }
-
-        try {
-            canvas.setCurrentAnimationTimeMillis(-1);
-            fail();
-        } catch (Throwable ex) {
-            // expected.
-        }
-    }
-
-    @SmallTest
     public void testSetColor() {
         new SetColorTest().run();
     }
@@ -380,370 +360,6 @@
     }
 
     @SmallTest
-    public void testClipRect() {
-        // The test is currently broken, waiting for the fix
-        // new ClipRectTest().run();
-    }
-
-    private static class ClipRectTest extends GLStub {
-        int mX, mY, mWidth, mHeight;
-
-        @Override
-        public void glScissor(int x, int y, int width, int height) {
-            mX = x;
-            mY = 100 - y - height;  // flip in Y direction
-            mWidth = width;
-            mHeight = height;
-        }
-
-        private void assertClipRect(int x, int y, int width, int height) {
-            assertEquals(x, mX);
-            assertEquals(y, mY);
-            assertEquals(width, mWidth);
-            assertEquals(height, mHeight);
-        }
-
-        private void assertEmptyClipRect() {
-            assertEquals(0, mWidth);
-            assertEquals(0, mHeight);
-        }
-
-        void run() {
-            GLCanvas canvas = new GLCanvasImpl(this);
-            canvas.setSize(100, 100);
-            canvas.save();
-            assertClipRect(0, 0, 100, 100);
-
-            assertTrue(canvas.clipRect(10, 10, 70, 70));
-            canvas.save();
-            assertClipRect(10, 10, 60, 60);
-
-            assertTrue(canvas.clipRect(30, 30, 90, 90));
-            canvas.save();
-            assertClipRect(30, 30, 40, 40);
-
-            assertTrue(canvas.clipRect(40, 40, 60, 90));
-            assertClipRect(40, 40, 20, 30);
-
-            assertFalse(canvas.clipRect(30, 30, 70, 40));
-            assertEmptyClipRect();
-            assertFalse(canvas.clipRect(0, 0, 100, 100));
-            assertEmptyClipRect();
-
-            canvas.restore();
-            assertClipRect(30, 30, 40, 40);
-
-            canvas.restore();
-            assertClipRect(10, 10, 60, 60);
-
-            canvas.restore();
-            assertClipRect(0, 0, 100, 100);
-
-            canvas.translate(10, 20, 30);
-            assertTrue(canvas.clipRect(10, 10, 70, 70));
-            canvas.save();
-            assertClipRect(20, 30, 60, 60);
-        }
-    }
-
-    @SmallTest
-    public void testSaveRestore() {
-        new SaveRestoreTest().run();
-    }
-
-    private static class SaveRestoreTest extends GLStub {
-        int mX, mY, mWidth, mHeight;
-
-        @Override
-        public void glScissor(int x, int y, int width, int height) {
-            mX = x;
-            mY = 100 - y - height;  // flip in Y direction
-            mWidth = width;
-            mHeight = height;
-        }
-
-        private void assertClipRect(int x, int y, int width, int height) {
-            assertEquals(x, mX);
-            assertEquals(y, mY);
-            assertEquals(width, mWidth);
-            assertEquals(height, mHeight);
-        }
-
-        void run() {
-            GLCanvas canvas = new GLCanvasImpl(this);
-            canvas.setSize(100, 100);
-
-            canvas.setAlpha(0.7f);
-            assertTrue(canvas.clipRect(10, 10, 70, 70));
-
-            canvas.save(canvas.SAVE_FLAG_CLIP);
-            canvas.setAlpha(0.6f);
-            assertTrue(canvas.clipRect(30, 30, 90, 90));
-
-            canvas.save(canvas.SAVE_FLAG_CLIP | canvas.SAVE_FLAG_ALPHA);
-            canvas.setAlpha(0.5f);
-            assertTrue(canvas.clipRect(40, 40, 60, 90));
-
-            assertEquals(0.5f, canvas.getAlpha());
-            assertClipRect(40, 40, 20, 30);
-
-            canvas.restore();  // now both clipping rect and alpha are restored.
-            assertEquals(0.6f, canvas.getAlpha());
-            assertClipRect(30, 30, 40, 40);
-
-            canvas.restore();  // now only clipping rect is restored.
-
-            canvas.save(0);
-            canvas.save(0);
-            canvas.restore();
-            canvas.restore();
-
-            assertEquals(0.6f, canvas.getAlpha());
-            assertTrue(canvas.clipRect(10, 10, 60, 60));
-        }
-    }
-
-    @SmallTest
-    public void testDrawTexture() {
-        new DrawTextureTest().run();
-        new DrawTextureMixedTest().run();
-    }
-
-    private static class MyTexture extends BasicTexture {
-        boolean mIsOpaque;
-        int mBindCalled;
-
-        MyTexture(GLCanvas canvas, int id, boolean isOpaque) {
-            super(canvas, id, STATE_LOADED);
-            setSize(1, 1);
-            mIsOpaque = isOpaque;
-        }
-
-        @Override
-        protected boolean onBind(GLCanvas canvas) {
-            mBindCalled++;
-            return true;
-        }
-
-        public boolean isOpaque() {
-            return mIsOpaque;
-        }
-    }
-
-    private static class DrawTextureTest extends GLMock {
-        int mDrawTexiOESCalled;
-        int mDrawArrayCalled;
-        int[] mResult = new int[4];
-
-        @Override
-        public void glDrawTexiOES(int x, int y, int z,
-                int width, int height) {
-            mDrawTexiOESCalled++;
-        }
-
-        @Override
-        public void glDrawArrays(int mode, int first, int count) {
-            assertNotNull(mGLVertexPointer);
-            assertEquals(GL10.GL_TRIANGLE_STRIP, mode);
-            assertEquals(4, count);
-            mGLVertexPointer.bindByteBuffer();
-
-            double[] coord = new double[4];
-            mGLVertexPointer.getArrayElement(first, coord);
-            mResult[0] = (int) coord[0];
-            mResult[1] = (int) coord[1];
-            mGLVertexPointer.getArrayElement(first + 1, coord);
-            mResult[2] = (int) coord[0];
-            mResult[3] = (int) coord[1];
-            mDrawArrayCalled++;
-        }
-
-        void run() {
-            GLCanvas canvas = new GLCanvasImpl(this);
-            canvas.setSize(400, 300);
-            MyTexture texture = new MyTexture(canvas, 42, false);  // non-opaque
-            MyTexture texture_o = new MyTexture(canvas, 47, true);  // opaque
-
-            // Draw a non-opaque texture
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(42, mGLBindTextureId);
-            assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE));
-            assertPremultipliedBlending(this);
-            assertFalse(mGLStencilEnabled);
-
-            // Draw an opaque texture
-            canvas.drawTexture(texture_o, 100, 200, 300, 400);
-            assertEquals(47, mGLBindTextureId);
-            assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE));
-            assertFalse(mGLBlendEnabled);
-
-            // Draw a non-opaque texture with alpha = 0.5
-            canvas.setAlpha(0.5f);
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(42, mGLBindTextureId);
-            assertEquals(0x80808080, mGLColor);
-            assertEquals(GL_MODULATE, getTexEnvi(GL_TEXTURE_ENV_MODE));
-            assertPremultipliedBlending(this);
-            assertFalse(mGLStencilEnabled);
-
-            // Draw an non-opaque texture with overriden alpha = 1
-            canvas.drawTexture(texture, 100, 200, 300, 400, 1f);
-            assertEquals(42, mGLBindTextureId);
-            assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE));
-            assertPremultipliedBlending(this);
-
-            // Draw an opaque texture with overriden alpha = 1
-            canvas.drawTexture(texture_o, 100, 200, 300, 400, 1f);
-            assertEquals(47, mGLBindTextureId);
-            assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE));
-            assertFalse(mGLBlendEnabled);
-
-            // Draw an opaque texture with overridden alpha = 0.25
-            canvas.drawTexture(texture_o, 100, 200, 300, 400, 0.25f);
-            assertEquals(47, mGLBindTextureId);
-            assertEquals(0x40404040, mGLColor);
-            assertEquals(GL_MODULATE, getTexEnvi(GL_TEXTURE_ENV_MODE));
-            assertPremultipliedBlending(this);
-
-            // Draw an opaque texture with overridden alpha = 0.125
-            // but with some rotation so it will use DrawArray.
-            canvas.save();
-            canvas.rotate(30, 0, 0, 1);
-            canvas.drawTexture(texture_o, 100, 200, 300, 400, 0.125f);
-            canvas.restore();
-            assertEquals(47, mGLBindTextureId);
-            assertEquals(0x20202020, mGLColor);
-            assertEquals(GL_MODULATE, getTexEnvi(GL_TEXTURE_ENV_MODE));
-            assertPremultipliedBlending(this);
-
-            // We have drawn seven textures above.
-            assertEquals(1, mDrawArrayCalled);
-            assertEquals(6, mDrawTexiOESCalled);
-
-            // translate and scale does not affect whether we
-            // can use glDrawTexiOES, but rotate may.
-            canvas.translate(10, 20, 30);
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(7, mDrawTexiOESCalled);
-
-            canvas.scale(10, 20, 30);
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(8, mDrawTexiOESCalled);
-
-            canvas.rotate(90, 1, 2, 3);
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(8, mDrawTexiOESCalled);
-
-            canvas.rotate(-90, 1, 2, 3);
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(9, mDrawTexiOESCalled);
-
-            canvas.rotate(180, 0, 0, 1);
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(9, mDrawTexiOESCalled);
-
-            canvas.rotate(180, 0, 0, 1);
-            canvas.drawTexture(texture, 100, 200, 300, 400);
-            assertEquals(10, mDrawTexiOESCalled);
-
-            assertEquals(3, mDrawArrayCalled);
-
-            assertTrue(texture.isLoaded(canvas));
-            texture.recycle();
-            assertFalse(texture.isLoaded(canvas));
-            canvas.deleteRecycledResources();
-
-            assertTrue(texture_o.isLoaded(canvas));
-            texture_o.recycle();
-            assertFalse(texture_o.isLoaded(canvas));
-        }
-    }
-
-    private static class DrawTextureMixedTest extends GLMock {
-
-        boolean mTexture2DEnabled0, mTexture2DEnabled1;
-        int mBindTexture0;
-        int mBindTexture1;
-
-        @Override
-        public void glEnable(int cap) {
-            if (cap == GL_TEXTURE_2D) {
-                texture2DEnable(true);
-            }
-        }
-
-        @Override
-        public void glDisable(int cap) {
-            if (cap == GL_TEXTURE_2D) {
-                texture2DEnable(false);
-            }
-        }
-
-        private void texture2DEnable(boolean enable) {
-            if (mGLActiveTexture == GL_TEXTURE0) {
-                mTexture2DEnabled0 = enable;
-            } else if (mGLActiveTexture == GL_TEXTURE1) {
-                mTexture2DEnabled1 = enable;
-            } else {
-                fail();
-            }
-        }
-
-        @Override
-        public void glTexEnvfv(int target, int pname, float[] params, int offset) {
-            if (target == GL_TEXTURE_ENV && pname == GL_TEXTURE_ENV_COLOR) {
-                assertEquals(0.5f, params[offset + 3]);
-            }
-        }
-
-        @Override
-        public void glBindTexture(int target, int texture) {
-            if (target == GL_TEXTURE_2D) {
-                if (mGLActiveTexture == GL_TEXTURE0) {
-                    mBindTexture0 = texture;
-                } else if (mGLActiveTexture == GL_TEXTURE1) {
-                    mBindTexture1 = texture;
-                } else {
-                    fail();
-                }
-            }
-        }
-
-        void run() {
-            GLCanvas canvas = new GLCanvasImpl(this);
-            canvas.setSize(400, 300);
-            MyTexture from = new MyTexture(canvas, 42, false);  // non-opaque
-            MyTexture to = new MyTexture(canvas, 47, true);  // opaque
-
-            canvas.drawMixed(from, to, 0.5f, 100, 200, 300, 400);
-            assertEquals(42, mBindTexture0);
-            assertEquals(47, mBindTexture1);
-            assertTrue(mTexture2DEnabled0);
-            assertFalse(mTexture2DEnabled1);
-
-            assertEquals(GL_COMBINE, getTexEnvi(GL_TEXTURE1, GL_TEXTURE_ENV_MODE));
-            assertEquals(GL_INTERPOLATE, getTexEnvi(GL_TEXTURE1, GL_COMBINE_RGB));
-            assertEquals(GL_INTERPOLATE, getTexEnvi(GL_TEXTURE1, GL_COMBINE_ALPHA));
-            assertEquals(GL_CONSTANT, getTexEnvi(GL_TEXTURE1, GL_SRC2_RGB));
-            assertEquals(GL_CONSTANT, getTexEnvi(GL_TEXTURE1, GL_SRC2_ALPHA));
-            assertEquals(GL_SRC_ALPHA, getTexEnvi(GL_TEXTURE1, GL_OPERAND2_RGB));
-            assertEquals(GL_SRC_ALPHA, getTexEnvi(GL_TEXTURE1, GL_OPERAND2_ALPHA));
-
-            assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE0, GL_TEXTURE_ENV_MODE));
-
-            assertFalse(mGLBlendEnabled);
-
-            canvas.drawMixed(from, to, 0, 100, 200, 300, 400);
-            assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE0, GL_TEXTURE_ENV_MODE));
-            assertEquals(42, mBindTexture0);
-
-            canvas.drawMixed(from, to, 1, 100, 200, 300, 400);
-            assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE0, GL_TEXTURE_ENV_MODE));
-            assertEquals(47, mBindTexture0);
-        }
-    }
-
-    @SmallTest
     public void testGetGLInstance() {
         GL11 glStub = new GLStub();
         GLCanvas canvas = new GLCanvasImpl(glStub);
diff --git a/tests/src/com/android/gallery3d/ui/GLRootMock.java b/tests/src/com/android/gallery3d/ui/GLRootMock.java
index c83e943..467edfc 100644
--- a/tests/src/com/android/gallery3d/ui/GLRootMock.java
+++ b/tests/src/com/android/gallery3d/ui/GLRootMock.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.ui;
 
+import android.graphics.Matrix;
 import com.android.gallery3d.anim.CanvasAnimation;
 
 public class GLRootMock implements GLRoot {
@@ -34,4 +35,11 @@
     public void lockRenderThread() {}
     public void unlockRenderThread() {}
     public void setContentPane(GLView content) {}
+    public void setOrientationSource(OrientationSource source) {}
+    public int getDisplayRotation() { return 0; }
+    public int getCompensation() { return 0; }
+    public Matrix getCompensationMatrix() { return null; }
+    public void freeze() {}
+    public void unfreeze() {}
+    public void setLightsOutMode(boolean enabled) {}
 }
diff --git a/tests/src/com/android/gallery3d/ui/GLRootStub.java b/tests/src/com/android/gallery3d/ui/GLRootStub.java
index d6bc678..0f3a001 100644
--- a/tests/src/com/android/gallery3d/ui/GLRootStub.java
+++ b/tests/src/com/android/gallery3d/ui/GLRootStub.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.ui;
 
+import android.graphics.Matrix;
 import com.android.gallery3d.anim.CanvasAnimation;
 
 public class GLRootStub implements GLRoot {
@@ -27,4 +28,11 @@
     public void lockRenderThread() {}
     public void unlockRenderThread() {}
     public void setContentPane(GLView content) {}
+    public void setOrientationSource(OrientationSource source) {}
+    public int getDisplayRotation() { return 0; }
+    public int getCompensation() { return 0; }
+    public Matrix getCompensationMatrix() { return null; }
+    public void freeze() {}
+    public void unfreeze() {}
+    public void setLightsOutMode(boolean enabled) {}
 }
diff --git a/tests/src/com/android/gallery3d/ui/GLViewTest.java b/tests/src/com/android/gallery3d/ui/GLViewTest.java
index a9377bf..b17b254 100644
--- a/tests/src/com/android/gallery3d/ui/GLViewTest.java
+++ b/tests/src/com/android/gallery3d/ui/GLViewTest.java
@@ -102,32 +102,6 @@
     }
 
     @SmallTest
-    public void testPaddings() {
-        GLView view = new GLView();
-
-        Rect p = view.getPaddings();
-        assertEquals(0, p.left);
-        assertEquals(0, p.top);
-        assertEquals(0, p.right);
-        assertEquals(0, p.bottom);
-
-        view.setPaddings(10, 20, 30, 100);
-        p = view.getPaddings();
-        assertEquals(10, p.left);
-        assertEquals(20, p.top);
-        assertEquals(30, p.right);
-        assertEquals(100, p.bottom);
-
-        p = new Rect(11, 22, 33, 104);
-        view.setPaddings(p);
-        p = view.getPaddings();
-        assertEquals(11, p.left);
-        assertEquals(22, p.top);
-        assertEquals(33, p.right);
-        assertEquals(104, p.bottom);
-    }
-
-    @SmallTest
     public void testParent() {
         GLView a = new GLView();
         GLView b = new GLView();
diff --git a/tests/src/com/android/gallery3d/ui/TextureTest.java b/tests/src/com/android/gallery3d/ui/TextureTest.java
index fb26060..36446b3 100644
--- a/tests/src/com/android/gallery3d/ui/TextureTest.java
+++ b/tests/src/com/android/gallery3d/ui/TextureTest.java
@@ -20,10 +20,10 @@
 import android.graphics.Bitmap.Config;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import javax.microedition.khronos.opengles.GL11;
-
 import junit.framework.TestCase;
 
+import javax.microedition.khronos.opengles.GL11;
+
 @SmallTest
 public class TextureTest extends TestCase {
     @SuppressWarnings("unused")
@@ -34,7 +34,7 @@
         int mOpaqueCalled;
 
         MyBasicTexture(GLCanvas canvas, int id) {
-            super(canvas, id, BasicTexture.STATE_UNLOADED);
+            super(canvas, id, 0);
         }
 
         @Override
@@ -43,6 +43,11 @@
             return true;
         }
 
+        @Override
+        protected int getTarget() {
+            return GL11.GL_TEXTURE_2D;
+        }
+
         public boolean isOpaque() {
             mOpaqueCalled++;
             return true;
@@ -71,13 +76,13 @@
         assertEquals(4, texture.getTextureWidth());
         assertEquals(8, texture.getTextureHeight());
 
-        assertFalse(texture.isLoaded(canvas));
+        assertFalse(texture.isLoaded());
         texture.upload();
-        assertTrue(texture.isLoaded(canvas));
+        assertTrue(texture.isLoaded());
 
         // For a different GL, it's not loaded.
         GLCanvas canvas2 = new GLCanvasImpl(new GLStub());
-        assertFalse(texture.isLoaded(canvas2));
+        assertFalse(texture.isLoaded());
 
         assertEquals(0, texture.mOnBindCalled);
         assertEquals(0, texture.mOpaqueCalled);
@@ -90,24 +95,6 @@
     }
 
     @SmallTest
-    public void testRawTexture() {
-        GL11 glStub = new GLStub();
-        GLCanvas canvas = new GLCanvasImpl(glStub);
-        RawTexture texture = RawTexture.newInstance(canvas);
-        texture.onBind(canvas);
-
-        GLCanvas canvas2 = new GLCanvasImpl(new GLStub());
-        try {
-            texture.onBind(canvas2);
-            fail();
-        } catch (RuntimeException ex) {
-            // expected.
-        }
-
-        assertTrue(texture.isOpaque());
-    }
-
-    @SmallTest
     public void testColorTexture() {
         GLCanvasMock canvas = new GLCanvasMock();
         ColorTexture texture = new ColorTexture(0x12345678);
@@ -156,20 +143,20 @@
         assertEquals(0, texture.mGetCalled);
         texture.draw(canvas, 0, 0);
         assertEquals(1, texture.mGetCalled);
-        assertTrue(texture.isLoaded(canvas));
-        assertTrue(texture.isContentValid(canvas));
+        assertTrue(texture.isLoaded());
+        assertTrue(texture.isContentValid());
 
         // invalidate content and it should be freed.
         texture.invalidateContent();
-        assertFalse(texture.isContentValid(canvas));
+        assertFalse(texture.isContentValid());
         assertEquals(1, texture.mFreeCalled);
-        assertTrue(texture.isLoaded(canvas));  // But it's still loaded
+        assertTrue(texture.isLoaded());  // But it's still loaded
 
         // draw it again and the bitmap should be fetched again.
         texture.draw(canvas, 0, 0);
         assertEquals(2, texture.mGetCalled);
-        assertTrue(texture.isLoaded(canvas));
-        assertTrue(texture.isContentValid(canvas));
+        assertTrue(texture.isLoaded());
+        assertTrue(texture.isContentValid());
 
         // recycle the texture and it should be freed again.
         texture.recycle();
@@ -181,7 +168,7 @@
 
     class MyTextureForMixed extends BasicTexture {
         MyTextureForMixed(GLCanvas canvas, int id) {
-            super(canvas, id, BasicTexture.STATE_UNLOADED);
+            super(canvas, id, 0);
         }
 
         @Override
@@ -189,6 +176,11 @@
             return true;
         }
 
+        @Override
+        protected int getTarget() {
+            return GL11.GL_TEXTURE_2D;
+        }
+
         public boolean isOpaque() {
             return true;
         }
diff --git a/tests/src/com/android/gallery3d/util/ProfileTest.java b/tests/src/com/android/gallery3d/util/ProfileTest.java
new file mode 100644
index 0000000..798b905
--- /dev/null
+++ b/tests/src/com/android/gallery3d/util/ProfileTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+import com.android.gallery3d.util.Profile;
+
+import android.os.Environment;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+@SmallTest
+public class ProfileTest extends TestCase {
+    private static final String TAG = "ProfileTest";
+    private static final String TEST_FILE =
+            Environment.getExternalStorageDirectory().getPath() + "/test.dat";
+
+
+    public void testProfile() throws IOException {
+        ProfileData p = new ProfileData();
+        ParsedProfile q;
+        String[] A = {"A"};
+        String[] B = {"B"};
+        String[] AC = {"A", "C"};
+        String[] AD = {"A", "D"};
+
+        // Empty profile
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertTrue(q.mEntries.isEmpty());
+        assertTrue(q.mSymbols.isEmpty());
+
+        // Only one sample
+        p.addSample(A);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(1, q.mEntries.size());
+        assertEquals(1, q.mSymbols.size());
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+
+        // Two samples at the same place
+        p.addSample(A);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(1, q.mEntries.size());
+        assertEquals(1, q.mSymbols.size());
+        assertEquals(2, q.mEntries.get(0).sampleCount);
+
+        // Two samples at the different places
+        p.reset();
+        p.addSample(A);
+        p.addSample(B);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(2, q.mEntries.size());
+        assertEquals(2, q.mSymbols.size());
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+        assertEquals(1, q.mEntries.get(1).sampleCount);
+
+        // depth > 1
+        p.reset();
+        p.addSample(AC);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(1, q.mEntries.size());
+        assertEquals(2, q.mSymbols.size());
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+
+        // two samples (AC and AD)
+        p.addSample(AD);
+        p.dumpToFile(TEST_FILE);
+        q = new ParsedProfile(TEST_FILE);
+        assertEquals(2, q.mEntries.size());
+        assertEquals(3, q.mSymbols.size());  // three symbols: A, C, D
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+        assertEquals(1, q.mEntries.get(0).sampleCount);
+
+        // Remove the test file
+        new File(TEST_FILE).delete();
+    }
+}
+
+class ParsedProfile {
+    public class Entry {
+        int sampleCount;
+        int stackId[];
+    }
+
+    ArrayList<Entry> mEntries = new ArrayList<Entry>();
+    HashMap<Integer, String> mSymbols = new HashMap<Integer, String>();
+    private DataInputStream mIn;
+    private byte[] mScratch = new byte[4];  // scratch buffer for readInt
+
+    public ParsedProfile(String filename) throws IOException {
+        mIn = new DataInputStream(new FileInputStream(filename));
+
+        Entry entry = parseOneEntry();
+        checkIsFirstEntry(entry);
+
+        while (true) {
+            entry = parseOneEntry();
+            if (entry.sampleCount == 0) {
+                checkIsLastEntry(entry);
+                break;
+            }
+            mEntries.add(entry);
+        }
+
+        // Read symbol table
+        while (true) {
+            String line = mIn.readLine();
+            if (line == null) break;
+            String[] fields = line.split(" +");
+            checkIsValidSymbolLine(fields);
+            mSymbols.put(Integer.decode(fields[0]), fields[1]);
+        }
+    }
+
+    private void checkIsFirstEntry(Entry entry) {
+        Assert.assertEquals(0, entry.sampleCount);
+        Assert.assertEquals(3, entry.stackId.length);
+        Assert.assertEquals(1, entry.stackId[0]);
+        Assert.assertTrue(entry.stackId[1] > 0);  // sampling period
+        Assert.assertEquals(0, entry.stackId[2]);  // padding
+    }
+
+    private void checkIsLastEntry(Entry entry) {
+        Assert.assertEquals(0, entry.sampleCount);
+        Assert.assertEquals(1, entry.stackId.length);
+        Assert.assertEquals(0, entry.stackId[0]);
+    }
+
+    private void checkIsValidSymbolLine(String[] fields) {
+        Assert.assertEquals(2, fields.length);
+        Assert.assertTrue(fields[0].startsWith("0x"));
+    }
+
+    private Entry parseOneEntry() throws IOException {
+        int sampleCount = readInt();
+        int depth = readInt();
+        Entry e = new Entry();
+        e.sampleCount = sampleCount;
+        e.stackId = new int[depth];
+        for (int i = 0; i < depth; i++) {
+            e.stackId[i] = readInt();
+        }
+        return e;
+    }
+
+    private int readInt() throws IOException {
+        mIn.read(mScratch, 0, 4);
+        return (mScratch[0] & 0xff) |
+                ((mScratch[1] & 0xff) << 8) |
+                ((mScratch[2] & 0xff) << 16) |
+                ((mScratch[3] & 0xff) << 24);
+    }
+}